feat(est-test): 엑셀형 수량산출서 읽기 전용 표시
- 원본 엑셀형 산출서 본문(OLE2 .xls BIFF8) 셀 값을 읽는 xls-read.cjs - 가져올 때 시트를 붙이고 수량산출 화면에 읽기 전용 격자로 표시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -469,6 +469,10 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 메모 편집(원본 서식 저장), 위치정리 드래그 이동.
|
||||
- [x] 통합 검색(원본 TFRMMULTISERCH 자료 7종·검색어·검색위치) — 검색위치는 이 앱 저장 작업(`search-store.cjs`, `/api/search`), 고른 자료를 하위 연결째 현재 공사로 가져옴(EstTransfer). 원본 「Main Data에서 검색」·폴더 고르기는 웹 서버에 원본 자료 폴더가 없어 저장 작업으로 대신함.
|
||||
- 검증완료: `test_est_multi_search.py`(낱말 모두 포함·자료 종류·제외 작업·깨진 파일 무시·빈 검색어/종류 거부) 통과. ORCA 브라우저에서 저장 작업 59개 대상 「콘크리트」 단가산출 85건 → 없는 D0000000140 가져오기 → 연결 자료 3건·단가산출 21행 추가·단가 28,878 원본과 같음·오류 0, 샘플 복구.
|
||||
- [ ] 엑셀형 수량산출서(원본 TFRMMULEXCEL, MulMainDat.Editor = OLE2 .xls BIFF8)
|
||||
- [x] 1단계 읽기 — `xls-read.cjs`(복합문서·SST/CONTINUE·LABELSST·NUMBER·RK·MULRK·LABEL·BOOLERR·FORMULA 저장값), 가져올 때 `sheet.excel`, 수량산출 화면 읽기 전용 격자(수식 셀 굵게).
|
||||
- 검증완료: `test_est_xls_read.py` 2개(설치 main.estx 엑셀형 6개·토적계산표 B2·No.1·20·수식 16개, OLE 아님 거부·격자 그리기), 수량 시험 포함 22개 통과. 엑셀형 붙인 뒤에도 ESTX 원본 그대로 저장 동일. ORCA 브라우저에서 토적계산표 격자 표시·수식 칸 16개 확인 후 샘플 복구.
|
||||
- 남음: 셀 편집·수식 계산·4방향 합계·품명/규격/수량 지정(항목과 셀 대응 근거 미확인), .xls 저장.
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄.
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
|
||||
`편집 → 찾기·다음찾기`(Ctrl+F)는 원본 찾기 창처럼 검색어를 처음부터 또는 현재 위치부터 찾아 해당 행으로 이동합니다. 일위대가·단가산출·기계경비는 찾은 행의 호표로 넘어갑니다. 단가산출에서는 찾은 행의 문구를 한 건씩 바꾸고 다음 위치로 이동하며, 모두 바꾸기는 `산출근거 문구 바꾸기`를 씁니다. 표 위 검색창은 걸러 보기로 그대로 남습니다.
|
||||
|
||||
원본의 엑셀형 수량산출서(본문이 옛 Excel .xls)는 가져올 때 셀 값을 읽어 수량산출 화면 아래에 읽기 전용 격자로 보입니다. 수식 셀은 Excel이 저장해 둔 계산값을 굵게 보이며, 셀 편집·다시 계산은 아직 하지 않습니다.
|
||||
|
||||
산출 줄 옆 `수량 지정`은 줄 글에서 드래그해 고른 숫자를 결과 표지로 감싸고 수량 항목을 새로 만듭니다. 품명·규격·단위 표지가 있으면 항목 이름에 옮깁니다. `지정 해제`는 커서가 놓인 결과 표지를 풀고, 다른 줄에서 쓰지 않는 항목을 지웁니다. 내역에 연결된 항목은 먼저 연결을 비워야 합니다. 이 변경이 있는 작업은 ESTX 수정 저장을 거부합니다.
|
||||
|
||||
수량산출 화면의 `산출서 복사`는 현재 산출서를 바로 아래에 복사하고, 내역에 두 번 반영되지 않도록 복사본의 내역 연결을 비웁니다. `산출서 위로`·`아래로`로 순서를 바꿉니다. 산출서 추가·순서 변경이 있는 작업은 ESTX 수정 저장을 거부하고 JSON 저장을 안내합니다.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Read-only ESTX import. Layout: knowledge/다산소프트/ESTX_파일구조.md.
|
||||
const { readXls } = require("./xls-read.cjs");
|
||||
const zlib = require("node:zlib");
|
||||
const quantity = require("./native-quantity.cjs");
|
||||
const { readPrintOptions } = require("./import-print.cjs");
|
||||
@@ -366,12 +367,14 @@ function importEstx(buffer, name = "가져온 내역서") {
|
||||
const text = new TextDecoder("euc-kr").decode(bytes);
|
||||
const items = memoryRows(Buffer.from(row.MulList.binary, "base64"));
|
||||
const name = tables.MulTitleDat?.find((r) => r.Code === row.Code)?.Name || row.Code;
|
||||
quantitySheets.push(
|
||||
quantity.fromWpt(row.Code, name, text, items, {
|
||||
digits: props.TMulLangOption?.Diget ?? 2,
|
||||
roundMode: (props.TMulLangOption?.CalcMethod ?? 1) + 1,
|
||||
}),
|
||||
);
|
||||
const sheet = quantity.fromWpt(row.Code, name, text, items, {
|
||||
digits: props.TMulLangOption?.Diget ?? 2,
|
||||
roundMode: (props.TMulLangOption?.CalcMethod ?? 1) + 1,
|
||||
});
|
||||
// 엑셀형 수량산출서(OLE2 .xls)는 읽기 전용 셀 값으로 붙인다.
|
||||
if (bytes.length > 8 && Buffer.from(bytes).readUInt32LE(0) === 0xe011cfd0)
|
||||
sheet.excel = readXls(Buffer.from(bytes));
|
||||
quantitySheets.push(sheet);
|
||||
} catch (error) {
|
||||
warnings.add(`수량산출 ${row.Code}: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -137,5 +137,35 @@ const EstQuantitySheets = (() => {
|
||||
return true;
|
||||
}
|
||||
|
||||
return { copy, move, mark, free, handle };
|
||||
// 엑셀형 수량산출서(원본 TFRMMULEXCEL) — 가져온 셀 값을 읽기 전용 격자로 보인다. 수식 셀은 저장된 계산값(굵게).
|
||||
function excelView(sheet) {
|
||||
if (!sheet?.excel?.length) return "";
|
||||
const { esc, number } = EstApp;
|
||||
const letter = (n) => {
|
||||
let s = "";
|
||||
for (n += 1; n > 0; n = Math.floor((n - 1) / 26))
|
||||
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
|
||||
return s;
|
||||
};
|
||||
return sheet.excel
|
||||
.map(({ name, cells }) => {
|
||||
const keys = Object.keys(cells).map((k) => k.split(",").map(Number));
|
||||
if (!keys.length) return "";
|
||||
const rows = Math.max(...keys.map(([r]) => r)) + 1,
|
||||
cols = Math.min(40, Math.max(...keys.map(([, c]) => c)) + 1);
|
||||
const body = Array.from({ length: rows }, (_, r) => {
|
||||
const cellsHtml = Array.from({ length: cols }, (_, c) => {
|
||||
const cell = cells[`${r},${c}`];
|
||||
if (!cell) return "<td></td>";
|
||||
const value = typeof cell.value === "number" ? number(cell.value) : esc(cell.value);
|
||||
return `<td${cell.formula ? ' style="font-weight:bold" title="수식(저장된 계산값)"' : ""}>${value}</td>`;
|
||||
}).join("");
|
||||
return `<tr><th>${r + 1}</th>${cellsHtml}</tr>`;
|
||||
}).join("");
|
||||
return `<h4>엑셀형 산출서 · ${esc(name)} (읽기 전용)</h4><div style="overflow:auto;max-height:420px"><table class="sheet" style="font-size:11px"><thead><tr><th></th>${Array.from({ length: cols }, (_, c) => `<th>${letter(c)}</th>`).join("")}</tr></thead><tbody>${body}</tbody></table></div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
return { copy, move, mark, free, excelView, handle };
|
||||
})();
|
||||
|
||||
@@ -315,7 +315,7 @@ const EstQuantity = (() => {
|
||||
{ esc, number, byId } = EstApp;
|
||||
byId("work-content").innerHTML =
|
||||
selector(state) +
|
||||
`<div class="business-tools"><label>산출서명 <input data-quantity-name value="${esc(sheet.Name)}"></label><button data-quantity-add-item>수량 항목 추가</button><button data-quantity-add>산출 행 추가</button><button data-quantity-apply>내역 수량 반영</button><button data-action="quantity-sheet-copy">산출서 복사</button><button data-action="quantity-sheet-up">산출서 위로</button><button data-action="quantity-sheet-down">산출서 아래로</button><button data-quantity-delete-sheet>산출서 삭제</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>단위</th><th>저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th><th>끝수 처리</th><th>작업</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td><input data-quantity-item="Name" data-index="${index}" value="${esc(item.Name || "")}" aria-label="품명"></td><td><input data-quantity-item="Size" data-index="${index}" value="${esc(item.Size || "")}" aria-label="규격"></td><td><input data-quantity-item="Unite" data-index="${index}" value="${esc(item.Unite || "")}" aria-label="단위"></td><td>${number(sheet.items[index]?.GN ?? 0)}</td><td>${number(item.GN)}</td><td style="white-space:normal"><input data-quantity-target="${index}" value="${esc(item.TargetCode ?? item.LinkCode ?? "")}" aria-label="연결 내역코드">${targetSelect(state.project, item, "sheet", index)}</td><td>${formatSelect(sheet.items[index], index)}</td><td><button data-quantity-delete-item="${index}">삭제</button></td></tr>`).join("")}</tbody></table><p class="business-note">${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th><th>작업</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td><td><button data-quantity-add data-index="${index}">아래 추가</button><button data-quantity-move="-1" data-index="${index}" ${index === 0 ? "disabled" : ""}>↑</button><button data-quantity-move="1" data-index="${index}" ${index === sheet.rows.length - 1 ? "disabled" : ""}>↓</button><button data-action="quantity-mark" data-index="${index}">수량 지정</button><button data-action="quantity-free" data-index="${index}">지정 해제</button><button data-quantity-delete-row="${index}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||
`<div class="business-tools"><label>산출서명 <input data-quantity-name value="${esc(sheet.Name)}"></label><button data-quantity-add-item>수량 항목 추가</button><button data-quantity-add>산출 행 추가</button><button data-quantity-apply>내역 수량 반영</button><button data-action="quantity-sheet-copy">산출서 복사</button><button data-action="quantity-sheet-up">산출서 위로</button><button data-action="quantity-sheet-down">산출서 아래로</button><button data-quantity-delete-sheet>산출서 삭제</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>단위</th><th>저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th><th>끝수 처리</th><th>작업</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td><input data-quantity-item="Name" data-index="${index}" value="${esc(item.Name || "")}" aria-label="품명"></td><td><input data-quantity-item="Size" data-index="${index}" value="${esc(item.Size || "")}" aria-label="규격"></td><td><input data-quantity-item="Unite" data-index="${index}" value="${esc(item.Unite || "")}" aria-label="단위"></td><td>${number(sheet.items[index]?.GN ?? 0)}</td><td>${number(item.GN)}</td><td style="white-space:normal"><input data-quantity-target="${index}" value="${esc(item.TargetCode ?? item.LinkCode ?? "")}" aria-label="연결 내역코드">${targetSelect(state.project, item, "sheet", index)}</td><td>${formatSelect(sheet.items[index], index)}</td><td><button data-quantity-delete-item="${index}">삭제</button></td></tr>`).join("")}</tbody></table>${typeof EstQuantitySheets !== "undefined" ? EstQuantitySheets.excelView(sheet) : ""}<p class="business-note">${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th><th>작업</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td><td><button data-quantity-add data-index="${index}">아래 추가</button><button data-quantity-move="-1" data-index="${index}" ${index === 0 ? "disabled" : ""}>↑</button><button data-quantity-move="1" data-index="${index}" ${index === sheet.rows.length - 1 ? "disabled" : ""}>↓</button><button data-action="quantity-mark" data-index="${index}">수량 지정</button><button data-action="quantity-free" data-index="${index}">지정 해제</button><button data-quantity-delete-row="${index}">삭제</button></td></tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
// 원본 수량목록 자리수 메뉴(mnuAuto·mnuTrunc·mnuUp·mnuRound): 결과 코드 뒤 방식(1 반올림·2 절사·3 올림)+자리수.
|
||||
function resultFormat(code) {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// 원본 엑셀형 수량산출서(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 };
|
||||
Reference in New Issue
Block a user