140 lines
5.3 KiB
JavaScript
140 lines
5.3 KiB
JavaScript
// 원본 인쇄양식 파일(.pfot — mnuPrintFormatSave 0x10ed000 · Load 0x10ed318): 인쇄정보표(DatPrintOption) MIDAS 이진 한 벌.
|
|
// 읽기는 import-print.cjs 와 같은 꼴. 쓰기는 같은 꼴의 바탕 파일(필드 설명자)을 받아 행 부분만 새로 만든다.
|
|
// .gfot(화면 격자 열 배치)는 웹 격자와 호환되지 않아 다루지 않는다.
|
|
const { readPrintOptions } = require("./import-print.cjs");
|
|
|
|
function layout(buffer) {
|
|
if (buffer.length < 24 || buffer.readUInt32LE(0) !== 0xbde01996)
|
|
throw Error("인쇄양식 파일 헤더 오류");
|
|
const count = buffer.readUInt16LE(12),
|
|
start = buffer.readUInt16LE(22);
|
|
let at = 24;
|
|
const fields = [];
|
|
for (let i = 0; i < count; i++) {
|
|
const length = buffer[at];
|
|
const name = buffer.subarray(at + 1, at + 1 + length).toString("ascii");
|
|
at += 1 + length;
|
|
const width = buffer.readUInt16LE(at),
|
|
kind = buffer.readUInt16LE(at + 2),
|
|
attributes = buffer.readUInt16LE(at + 6);
|
|
at += 8;
|
|
for (let j = 0; j < attributes; j++) {
|
|
at += 1 + buffer[at];
|
|
const size = buffer.readUInt16LE(at),
|
|
type = buffer.readUInt16LE(at + 2);
|
|
at += 4;
|
|
at += type === 73 ? 2 + buffer.readUInt16LE(at) : size;
|
|
}
|
|
fields.push({ name, width, kind });
|
|
}
|
|
return { count, start, fields, flag: buffer[start] };
|
|
}
|
|
|
|
// rows: 인쇄정보표 행들(읽은 값). BLOB(75) 칸은 비워 씀.
|
|
function write(template, rows) {
|
|
const { count, start, fields, flag } = layout(template);
|
|
const encoder = (s) => {
|
|
const bytes = [];
|
|
for (const ch of String(s)) {
|
|
const code = ch.codePointAt(0);
|
|
if (code < 0x80) bytes.push(code);
|
|
else {
|
|
const found = cp949Table().get(ch);
|
|
if (found == null) throw Error(`인쇄양식 글자를 cp949 로 바꿀 수 없습니다: ${ch}`);
|
|
bytes.push(found >> 8, found & 0xff);
|
|
}
|
|
}
|
|
return Buffer.from(bytes);
|
|
};
|
|
const parts = [Buffer.from(template.subarray(0, start))];
|
|
parts[0].writeUInt16LE(rows.length, 14);
|
|
for (const row of rows) {
|
|
const status = Buffer.alloc(Math.ceil(count / 4)),
|
|
values = [];
|
|
fields.forEach(({ name, width, kind }, i) => {
|
|
const v = row[name];
|
|
if (kind === 75 && v?.binary != null) {
|
|
const data = Buffer.from(v.binary, "base64"),
|
|
length = Buffer.alloc(4);
|
|
length.writeUInt32LE(data.length);
|
|
values.push(length, data);
|
|
return;
|
|
}
|
|
if (v == null || kind === 75) {
|
|
status[i >> 2] |= (row.__nulls?.[name] || 1) << ((i % 4) * 2);
|
|
return;
|
|
}
|
|
if (kind === 1 && width === 4) {
|
|
const b = Buffer.alloc(4);
|
|
b.writeInt32LE(Number(v) | 0);
|
|
values.push(b);
|
|
} else if (kind === 3 && width === 2) {
|
|
const b = Buffer.alloc(2);
|
|
b.writeInt16LE(Number(v) | 0);
|
|
values.push(b);
|
|
} else if (kind === 4 && width === 8) {
|
|
const b = Buffer.alloc(8);
|
|
b.writeDoubleLE(Number(v));
|
|
values.push(b);
|
|
} else if (kind === 73 && width === 1) {
|
|
const text = encoder(v);
|
|
if (text.length > 255) throw Error(`인쇄양식 글이 너무 깁니다: ${name}`);
|
|
values.push(Buffer.from([text.length]), text);
|
|
} else throw Error(`인쇄양식 필드형 오류: ${name}/${kind}/${width}`);
|
|
});
|
|
parts.push(Buffer.from([flag]), status, ...values);
|
|
}
|
|
return Buffer.concat(parts);
|
|
}
|
|
|
|
let table = null;
|
|
function cp949Table() {
|
|
if (table) return table;
|
|
table = new Map();
|
|
const decoder = new TextDecoder("euc-kr");
|
|
for (let hi = 0x81; hi <= 0xfe; hi++)
|
|
for (let lo = 0x41; lo <= 0xfe; lo++) {
|
|
const ch = decoder.decode(Buffer.from([hi, lo]));
|
|
if (ch.length === 1 && ch !== "�" && !table.has(ch)) table.set(ch, (hi << 8) | lo);
|
|
}
|
|
return table;
|
|
}
|
|
|
|
// BLOB(설명 등)까지 담아 읽기 — 그대로 다시 쓰면 원본과 같은 바이트
|
|
function readFull(buffer) {
|
|
const { count, start, fields } = layout(buffer),
|
|
rows = buffer.readUInt16LE(14),
|
|
decoder = new TextDecoder("euc-kr");
|
|
let at = start;
|
|
const out = [];
|
|
for (let r = 0; r < rows; r++) {
|
|
at += 1;
|
|
const status = buffer.subarray(at, at + Math.ceil(count / 4));
|
|
at += status.length;
|
|
const row = {};
|
|
fields.forEach(({ name, width, kind }, i) => {
|
|
const state = (status[i >> 2] >> ((i % 4) * 2)) & 3;
|
|
if (state) {
|
|
if (state !== 1) (row.__nulls ||= {})[name] = state; // 빈 칸 표지(1·2)도 그대로 되살리게
|
|
return;
|
|
}
|
|
if (kind === 1 && width === 4) ((row[name] = buffer.readInt32LE(at)), (at += 4));
|
|
else if (kind === 3 && width === 2) ((row[name] = buffer.readInt16LE(at)), (at += 2));
|
|
else if (kind === 4 && width === 8) ((row[name] = buffer.readDoubleLE(at)), (at += 8));
|
|
else if (kind === 73 && width === 1) {
|
|
const n = buffer[at];
|
|
row[name] = decoder.decode(buffer.subarray(at + 1, at + 1 + n));
|
|
at += 1 + n;
|
|
} else if (kind === 75) {
|
|
const n = buffer.readUInt32LE(at);
|
|
row[name] = { binary: buffer.subarray(at + 4, at + 4 + n).toString("base64") };
|
|
at += 4 + n;
|
|
} else throw Error(`인쇄양식 필드형 오류: ${name}/${kind}/${width}`);
|
|
});
|
|
out.push(row);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
module.exports = { read: (buffer) => readPrintOptions(buffer), readFull, write, layout };
|