Files
Aislo/tools/est-test/xls-read.cjs
T
eomsangdonandClaude Opus 5.5 6f32263ea3 feat(est-test): 엑셀형 수량산출서 읽기 전용 표시
- 원본 엑셀형 산출서 본문(OLE2 .xls BIFF8) 셀 값을 읽는 xls-read.cjs
- 가져올 때 시트를 붙이고 수량산출 화면에 읽기 전용 격자로 표시

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:49:49 +09:00

236 lines
8.2 KiB
JavaScript

// 원본 엑셀형 수량산출서(TFRMMULEXCEL) 본문 — MulMainDat.Editor 에 든 옛 Excel(.xls, OLE2 복합문서 + BIFF8) 읽기.
// 읽기 전용: 시트 이름과 셀 값(문자·숫자·불·오류), 수식 셀은 저장된 계산값과 수식 여부만 꺼낸다. 서식·그림은 읽지 않는다.
const zlib = require("node:zlib");
function cfb(buffer) {
if (buffer.readUInt32LE(0) !== 0xe011cfd0 || buffer.readUInt32LE(4) !== 0xe11ab1a1)
throw Error("엑셀형 수량산출서 형식(OLE2)이 아닙니다.");
const sectorSize = 1 << buffer.readUInt16LE(30),
miniSize = 1 << buffer.readUInt16LE(32),
fatCount = buffer.readUInt32LE(44),
dirStart = buffer.readInt32LE(48),
miniCutoff = buffer.readUInt32LE(56),
miniFatStart = buffer.readInt32LE(60),
difatStart = buffer.readInt32LE(68);
const sector = (n) => buffer.subarray(512 + n * sectorSize, 512 + (n + 1) * sectorSize);
const difat = [];
for (let i = 0; i < 109; i++) difat.push(buffer.readInt32LE(76 + i * 4));
for (let next = difatStart, guard = 0; next >= 0 && guard < 10000; guard++) {
const s = sector(next);
for (let i = 0; i < sectorSize / 4 - 1; i++) difat.push(s.readInt32LE(i * 4));
next = s.readInt32LE(sectorSize - 4);
}
const fat = [];
for (const n of difat.slice(0, fatCount)) {
if (n < 0) continue;
const s = sector(n);
for (let i = 0; i < sectorSize / 4; i++) fat.push(s.readInt32LE(i * 4));
}
const chain = (start, table) => {
const out = [];
for (let n = start, guard = 0; n >= 0 && guard < table.length + 1; guard++) {
out.push(n);
n = table[n];
}
return out;
};
const read = (start) => Buffer.concat(chain(start, fat).map(sector));
const dir = read(dirStart),
entries = [];
for (let at = 0; at + 128 <= dir.length; at += 128) {
const nameLength = dir.readUInt16LE(at + 64);
entries.push({
name: dir.toString("utf16le", at, at + Math.max(0, nameLength - 2)),
type: dir[at + 66],
start: dir.readInt32LE(at + 116),
size: dir.readUInt32LE(at + 120),
});
}
const root = entries[0],
miniStream = root ? read(root.start) : Buffer.alloc(0),
miniFat = [];
if (miniFatStart >= 0) {
const m = read(miniFatStart);
for (let i = 0; i < m.length / 4; i++) miniFat.push(m.readInt32LE(i * 4));
}
return (name) => {
const entry = entries.find((e) => e.type === 2 && e.name.toLowerCase() === name.toLowerCase());
if (!entry) return null;
if (entry.size < miniCutoff)
return Buffer.concat(
chain(entry.start, miniFat).map((n) =>
miniStream.subarray(n * miniSize, (n + 1) * miniSize),
),
).subarray(0, entry.size);
return read(entry.start).subarray(0, entry.size);
};
}
function rk(value) {
const raw = value & 1 ? 100 : 1;
const number =
value & 2
? value >> 2
: (() => {
const b = Buffer.alloc(8);
b.writeInt32LE(value & 0xfffffffc, 4);
return b.readDoubleLE(0);
})();
return number / raw;
}
// BIFF8 유니코드 문자열(길이·옵션·리치/확장 정보) — CONTINUE 경계는 SST 에서만 따로 처리한다.
function unicode(data, at, lengthBytes = 2) {
const length = lengthBytes === 2 ? data.readUInt16LE(at) : data[at];
let pos = at + lengthBytes;
const flags = data[pos++],
wide = flags & 1;
let runs = 0,
ext = 0;
if (flags & 8) ((runs = data.readUInt16LE(pos)), (pos += 2));
if (flags & 4) ((ext = data.readUInt32LE(pos)), (pos += 4));
const bytes = wide ? length * 2 : length;
const text = wide
? data.toString("utf16le", pos, pos + bytes)
: data.toString("latin1", pos, pos + bytes);
return { text, end: pos + bytes + runs * 4 + ext };
}
function records(stream) {
const out = [];
for (let at = 0; at + 4 <= stream.length;) {
const type = stream.readUInt16LE(at),
size = stream.readUInt16LE(at + 2);
out.push({ type, data: stream.subarray(at + 4, at + 4 + size) });
at += 4 + size;
}
return out;
}
function sst(list, index) {
// SST 와 뒤따르는 CONTINUE 를 문자열 단위로 이어 읽는다.
const parts = [list[index].data];
for (let i = index + 1; i < list.length && list[i].type === 0x3c; i++) parts.push(list[i].data);
const strings = [],
count = parts[0].readUInt32LE(4);
let part = 0,
data = parts[0],
pos = 8;
const next = () => {
part++;
data = parts[part];
pos = 0;
};
while (strings.length < count && data) {
if (pos >= data.length) {
next();
continue;
}
const length = data.readUInt16LE(pos);
let flags = data[pos + 2],
p = pos + 3,
runs = 0,
ext = 0;
if (flags & 8) ((runs = data.readUInt16LE(p)), (p += 2));
if (flags & 4) ((ext = data.readUInt32LE(p)), (p += 4));
let text = "",
left = length;
pos = p;
while (left > 0) {
if (pos >= data.length) {
next();
if (!data) break;
flags = data[pos++];
}
const wide = flags & 1,
room = Math.floor((data.length - pos) / (wide ? 2 : 1)),
take = Math.min(left, room);
text += wide
? data.toString("utf16le", pos, pos + take * 2)
: data.toString("latin1", pos, pos + take);
pos += take * (wide ? 2 : 1);
left -= take;
}
let skip = runs * 4 + ext;
while (skip > 0 && data) {
const step = Math.min(skip, data.length - pos);
pos += step;
skip -= step;
if (skip > 0) next();
}
strings.push(text);
}
return strings;
}
function readXls(input) {
let buffer = Buffer.from(input);
if (buffer[0] === 0x78) buffer = zlib.inflateSync(buffer);
const open = cfb(buffer),
stream = open("Workbook") || open("Book");
if (!stream) throw Error("엑셀형 수량산출서에 Workbook 이 없습니다.");
const list = records(stream);
let strings = [];
const sheets = [];
list.forEach((record, i) => {
if (record.type === 0xfc) strings = sst(list, i);
if (record.type === 0x85)
sheets.push({
offset: record.data.readUInt32LE(0),
name: unicode(record.data, 6, 1).text,
cells: {},
});
});
const byOffset = new Map();
let at = 0;
for (const record of list) {
byOffset.set(at, record);
at += 4 + record.data.length;
}
const offsets = [...byOffset.keys()];
for (const sheet of sheets) {
let index = offsets.indexOf(sheet.offset);
if (index < 0) continue;
let pendingFormula = null;
for (index++; index < offsets.length; index++) {
const { type, data } = byOffset.get(offsets[index]);
if (type === 0x0a) break;
const put = (row, col, value, formula = false) =>
(sheet.cells[`${row},${col}`] = formula ? { value, formula: true } : { value });
if (type === 0xfd)
put(data.readUInt16LE(0), data.readUInt16LE(2), strings[data.readUInt32LE(6)] ?? "");
else if (type === 0x203)
put(data.readUInt16LE(0), data.readUInt16LE(2), data.readDoubleLE(6));
else if (type === 0x27e)
put(data.readUInt16LE(0), data.readUInt16LE(2), rk(data.readInt32LE(6)));
else if (type === 0xbd) {
const row = data.readUInt16LE(0),
first = data.readUInt16LE(2),
last = data.readUInt16LE(data.length - 2);
for (let c = first; c <= last; c++)
put(row, c, rk(data.readInt32LE(4 + (c - first) * 6 + 2)));
} else if (type === 0x204)
put(data.readUInt16LE(0), data.readUInt16LE(2), unicode(data, 6).text);
else if (type === 0x205) {
const v = data[6];
put(data.readUInt16LE(0), data.readUInt16LE(2), data[7] ? `#ERR${v}` : Boolean(v));
} else if (type === 0x06) {
const row = data.readUInt16LE(0),
col = data.readUInt16LE(2);
if (data.readUInt16LE(12) === 0xffff) {
const kind = data[6];
if (kind === 0) pendingFormula = { row, col };
else
put(row, col, kind === 1 ? Boolean(data[8]) : kind === 2 ? `#ERR${data[8]}` : "", true);
} else put(row, col, data.readDoubleLE(6), true);
} else if (type === 0x207 && pendingFormula) {
put(pendingFormula.row, pendingFormula.col, unicode(data, 0).text, true);
pendingFormula = null;
}
}
}
return sheets.map(({ name, cells }) => ({ name, cells }));
}
module.exports = { readXls, cfb };