kordoc 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/dist/{chunk-TFGOV2ML.js → chunk-YDXK5T53.js} +463 -99
- package/dist/chunk-YDXK5T53.js.map +1 -0
- package/dist/cli.js +38 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +466 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -8
- package/dist/index.d.ts +36 -8
- package/dist/index.js +468 -93
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +25 -21
- package/dist/mcp.js.map +1 -1
- package/dist/{watch-WMRLOFYY.js → watch-C5LIQLZC.js} +34 -4
- package/dist/watch-C5LIQLZC.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-TFGOV2ML.js.map +0 -1
- package/dist/watch-WMRLOFYY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -50,6 +50,9 @@ var init_provider = __esm({
|
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// src/index.ts
|
|
54
|
+
import { readFile } from "fs/promises";
|
|
55
|
+
|
|
53
56
|
// src/detect.ts
|
|
54
57
|
function magicBytes(buffer) {
|
|
55
58
|
return new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength));
|
|
@@ -80,6 +83,12 @@ import { inflateRawSync } from "zlib";
|
|
|
80
83
|
import { DOMParser } from "@xmldom/xmldom";
|
|
81
84
|
|
|
82
85
|
// src/table/builder.ts
|
|
86
|
+
var SAFE_HREF_RE = /^(?:https?:|mailto:|tel:|#)/i;
|
|
87
|
+
function sanitizeHref(href) {
|
|
88
|
+
const trimmed = href.trim();
|
|
89
|
+
if (!trimmed || !SAFE_HREF_RE.test(trimmed)) return null;
|
|
90
|
+
return trimmed;
|
|
91
|
+
}
|
|
83
92
|
var MAX_COLS = 200;
|
|
84
93
|
var MAX_ROWS = 1e4;
|
|
85
94
|
function buildTable(rows) {
|
|
@@ -145,6 +154,10 @@ function blocksToMarkdown(blocks) {
|
|
|
145
154
|
lines.push("", `${prefix} ${block.text}`, "");
|
|
146
155
|
continue;
|
|
147
156
|
}
|
|
157
|
+
if (block.type === "image" && block.text) {
|
|
158
|
+
lines.push("", ``, "");
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
148
161
|
if (block.type === "separator") {
|
|
149
162
|
lines.push("", "---", "");
|
|
150
163
|
continue;
|
|
@@ -178,7 +191,8 @@ function blocksToMarkdown(blocks) {
|
|
|
178
191
|
continue;
|
|
179
192
|
}
|
|
180
193
|
if (block.href) {
|
|
181
|
-
|
|
194
|
+
const href = sanitizeHref(block.href);
|
|
195
|
+
if (href) text = `[${text}](${href})`;
|
|
182
196
|
}
|
|
183
197
|
if (block.footnoteText) {
|
|
184
198
|
text += ` (\uC8FC: ${block.footnoteText})`;
|
|
@@ -240,7 +254,13 @@ function tableToMarkdown(table) {
|
|
|
240
254
|
}
|
|
241
255
|
|
|
242
256
|
// src/utils.ts
|
|
243
|
-
var VERSION = true ? "1.
|
|
257
|
+
var VERSION = true ? "1.7.0" : "0.0.0-dev";
|
|
258
|
+
function toArrayBuffer(buf) {
|
|
259
|
+
if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
|
|
260
|
+
return buf.buffer;
|
|
261
|
+
}
|
|
262
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
263
|
+
}
|
|
244
264
|
var KordocError = class extends Error {
|
|
245
265
|
constructor(message) {
|
|
246
266
|
super(message);
|
|
@@ -248,6 +268,7 @@ var KordocError = class extends Error {
|
|
|
248
268
|
}
|
|
249
269
|
};
|
|
250
270
|
function isPathTraversal(name) {
|
|
271
|
+
if (name.includes("\0")) return true;
|
|
251
272
|
const normalized = name.replace(/\\/g, "/");
|
|
252
273
|
return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
|
|
253
274
|
}
|
|
@@ -299,7 +320,16 @@ var MAX_ZIP_ENTRIES = 500;
|
|
|
299
320
|
function clampSpan(val, max) {
|
|
300
321
|
return Math.max(1, Math.min(val, max));
|
|
301
322
|
}
|
|
302
|
-
|
|
323
|
+
var MAX_XML_DEPTH = 200;
|
|
324
|
+
function createXmlParser(warnings) {
|
|
325
|
+
return new DOMParser({
|
|
326
|
+
onError(level, msg) {
|
|
327
|
+
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg}`);
|
|
328
|
+
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg}` });
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
async function extractHwpxStyles(zip, decompressed) {
|
|
303
333
|
const result = {
|
|
304
334
|
charProperties: /* @__PURE__ */ new Map(),
|
|
305
335
|
styles: /* @__PURE__ */ new Map()
|
|
@@ -311,7 +341,11 @@ async function extractHwpxStyles(zip) {
|
|
|
311
341
|
if (!file) continue;
|
|
312
342
|
try {
|
|
313
343
|
const xml = await file.async("text");
|
|
314
|
-
|
|
344
|
+
if (decompressed) {
|
|
345
|
+
decompressed.total += xml.length * 2;
|
|
346
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
347
|
+
}
|
|
348
|
+
const parser = createXmlParser();
|
|
315
349
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
316
350
|
if (!doc.documentElement) continue;
|
|
317
351
|
parseCharProperties(doc, result.charProperties);
|
|
@@ -389,37 +423,128 @@ async function parseHwpxDocument(buffer, options) {
|
|
|
389
423
|
if (actualEntryCount > MAX_ZIP_ENTRIES) {
|
|
390
424
|
throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
391
425
|
}
|
|
426
|
+
const decompressed = { total: 0 };
|
|
392
427
|
const metadata = {};
|
|
393
|
-
await extractHwpxMetadata(zip, metadata);
|
|
394
|
-
const styleMap = await extractHwpxStyles(zip);
|
|
428
|
+
await extractHwpxMetadata(zip, metadata, decompressed);
|
|
429
|
+
const styleMap = await extractHwpxStyles(zip, decompressed);
|
|
395
430
|
const warnings = [];
|
|
396
431
|
const sectionPaths = await resolveSectionPaths(zip);
|
|
397
432
|
if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
398
433
|
metadata.pageCount = sectionPaths.length;
|
|
399
434
|
const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
|
|
400
|
-
|
|
435
|
+
const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
|
|
401
436
|
const blocks = [];
|
|
437
|
+
let parsedSections = 0;
|
|
402
438
|
for (let si = 0; si < sectionPaths.length; si++) {
|
|
403
439
|
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
404
440
|
const file = zip.file(sectionPaths[si]);
|
|
405
441
|
if (!file) continue;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
442
|
+
try {
|
|
443
|
+
const xml = await file.async("text");
|
|
444
|
+
decompressed.total += xml.length * 2;
|
|
445
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
446
|
+
blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
|
|
447
|
+
parsedSections++;
|
|
448
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
449
|
+
} catch (secErr) {
|
|
450
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
451
|
+
warnings.push({ page: si + 1, message: `\uC139\uC158 ${si + 1} \uD30C\uC2F1 \uC2E4\uD328: ${secErr instanceof Error ? secErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
|
|
452
|
+
}
|
|
410
453
|
}
|
|
454
|
+
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
411
455
|
detectHwpxHeadings(blocks, styleMap);
|
|
412
456
|
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
413
457
|
const markdown = blocksToMarkdown(blocks);
|
|
414
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
458
|
+
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0, images: images.length > 0 ? images : void 0 };
|
|
459
|
+
}
|
|
460
|
+
function imageExtToMime(ext) {
|
|
461
|
+
switch (ext.toLowerCase()) {
|
|
462
|
+
case "jpg":
|
|
463
|
+
case "jpeg":
|
|
464
|
+
return "image/jpeg";
|
|
465
|
+
case "png":
|
|
466
|
+
return "image/png";
|
|
467
|
+
case "gif":
|
|
468
|
+
return "image/gif";
|
|
469
|
+
case "bmp":
|
|
470
|
+
return "image/bmp";
|
|
471
|
+
case "tif":
|
|
472
|
+
case "tiff":
|
|
473
|
+
return "image/tiff";
|
|
474
|
+
case "wmf":
|
|
475
|
+
return "image/wmf";
|
|
476
|
+
case "emf":
|
|
477
|
+
return "image/emf";
|
|
478
|
+
case "svg":
|
|
479
|
+
return "image/svg+xml";
|
|
480
|
+
default:
|
|
481
|
+
return "application/octet-stream";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function mimeToExt(mime) {
|
|
485
|
+
if (mime.includes("jpeg")) return "jpg";
|
|
486
|
+
if (mime.includes("png")) return "png";
|
|
487
|
+
if (mime.includes("gif")) return "gif";
|
|
488
|
+
if (mime.includes("bmp")) return "bmp";
|
|
489
|
+
if (mime.includes("tiff")) return "tif";
|
|
490
|
+
if (mime.includes("wmf")) return "wmf";
|
|
491
|
+
if (mime.includes("emf")) return "emf";
|
|
492
|
+
if (mime.includes("svg")) return "svg";
|
|
493
|
+
return "bin";
|
|
494
|
+
}
|
|
495
|
+
async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
|
|
496
|
+
const images = [];
|
|
497
|
+
let imageIndex = 0;
|
|
498
|
+
for (const block of blocks) {
|
|
499
|
+
if (block.type !== "image" || !block.text) continue;
|
|
500
|
+
const ref = block.text;
|
|
501
|
+
const candidates = [
|
|
502
|
+
`BinData/${ref}`,
|
|
503
|
+
`Contents/BinData/${ref}`,
|
|
504
|
+
ref
|
|
505
|
+
// 절대 경로일 수도 있음
|
|
506
|
+
];
|
|
507
|
+
let found = false;
|
|
508
|
+
for (const path of candidates) {
|
|
509
|
+
if (isPathTraversal(path)) continue;
|
|
510
|
+
const file = zip.file(path);
|
|
511
|
+
if (!file) continue;
|
|
512
|
+
try {
|
|
513
|
+
const data = await file.async("uint8array");
|
|
514
|
+
decompressed.total += data.length;
|
|
515
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
516
|
+
const ext = ref.includes(".") ? ref.split(".").pop() : "png";
|
|
517
|
+
const mimeType = imageExtToMime(ext);
|
|
518
|
+
imageIndex++;
|
|
519
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
520
|
+
images.push({ filename, data, mimeType });
|
|
521
|
+
block.text = filename;
|
|
522
|
+
block.imageData = { data, mimeType, filename: ref };
|
|
523
|
+
found = true;
|
|
524
|
+
break;
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err instanceof KordocError) throw err;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!found) {
|
|
530
|
+
warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
|
|
531
|
+
block.type = "paragraph";
|
|
532
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return images;
|
|
415
536
|
}
|
|
416
|
-
async function extractHwpxMetadata(zip, metadata) {
|
|
537
|
+
async function extractHwpxMetadata(zip, metadata, decompressed) {
|
|
417
538
|
try {
|
|
418
539
|
const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
|
|
419
540
|
for (const mp of metaPaths) {
|
|
420
541
|
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
|
|
421
542
|
if (!file) continue;
|
|
422
543
|
const xml = await file.async("text");
|
|
544
|
+
if (decompressed) {
|
|
545
|
+
decompressed.total += xml.length * 2;
|
|
546
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
547
|
+
}
|
|
423
548
|
parseDublinCoreMetadata(xml, metadata);
|
|
424
549
|
if (metadata.title || metadata.author) return;
|
|
425
550
|
}
|
|
@@ -427,7 +552,7 @@ async function extractHwpxMetadata(zip, metadata) {
|
|
|
427
552
|
}
|
|
428
553
|
}
|
|
429
554
|
function parseDublinCoreMetadata(xml, metadata) {
|
|
430
|
-
const parser =
|
|
555
|
+
const parser = createXmlParser();
|
|
431
556
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
432
557
|
if (!doc.documentElement) return;
|
|
433
558
|
const getText = (tagNames) => {
|
|
@@ -491,7 +616,14 @@ function extractFromBrokenZip(buffer) {
|
|
|
491
616
|
let totalDecompressed = 0;
|
|
492
617
|
let entryCount = 0;
|
|
493
618
|
while (pos < data.length - 30) {
|
|
494
|
-
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4)
|
|
619
|
+
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
|
|
620
|
+
pos++;
|
|
621
|
+
while (pos < data.length - 30) {
|
|
622
|
+
if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
|
|
623
|
+
pos++;
|
|
624
|
+
}
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
495
627
|
if (++entryCount > MAX_ZIP_ENTRIES) break;
|
|
496
628
|
const method = view.getUint16(pos + 8, true);
|
|
497
629
|
const compSize = view.getUint32(pos + 18, true);
|
|
@@ -551,7 +683,7 @@ async function resolveSectionPaths(zip) {
|
|
|
551
683
|
return sectionFiles.map((f) => f.name).sort();
|
|
552
684
|
}
|
|
553
685
|
function parseSectionPathsFromManifest(xml) {
|
|
554
|
-
const parser =
|
|
686
|
+
const parser = createXmlParser();
|
|
555
687
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
556
688
|
const items = doc.getElementsByTagName("opf:item");
|
|
557
689
|
const spine = doc.getElementsByTagName("opf:itemref");
|
|
@@ -613,14 +745,33 @@ function detectHwpxHeadings(blocks, styleMap) {
|
|
|
613
745
|
}
|
|
614
746
|
}
|
|
615
747
|
function parseSectionXml(xml, styleMap, warnings, sectionNum) {
|
|
616
|
-
const parser =
|
|
748
|
+
const parser = createXmlParser(warnings);
|
|
617
749
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
618
750
|
if (!doc.documentElement) return [];
|
|
619
751
|
const blocks = [];
|
|
620
752
|
walkSection(doc.documentElement, blocks, null, [], styleMap, warnings, sectionNum);
|
|
621
753
|
return blocks;
|
|
622
754
|
}
|
|
623
|
-
function
|
|
755
|
+
function extractImageRef(el) {
|
|
756
|
+
const children = el.childNodes;
|
|
757
|
+
if (!children) return null;
|
|
758
|
+
for (let i = 0; i < children.length; i++) {
|
|
759
|
+
const child = children[i];
|
|
760
|
+
if (child.nodeType !== 1) continue;
|
|
761
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
762
|
+
if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
763
|
+
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
764
|
+
if (ref) return ref;
|
|
765
|
+
}
|
|
766
|
+
const nested = extractImageRef(child);
|
|
767
|
+
if (nested) return nested;
|
|
768
|
+
}
|
|
769
|
+
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
770
|
+
if (directRef) return directRef;
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
|
|
774
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
624
775
|
const children = node.childNodes;
|
|
625
776
|
if (!children) return;
|
|
626
777
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -632,7 +783,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
632
783
|
case "tbl": {
|
|
633
784
|
if (tableCtx) tableStack.push(tableCtx);
|
|
634
785
|
const newTable = { rows: [], currentRow: [], cell: null };
|
|
635
|
-
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
|
|
786
|
+
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
636
787
|
if (newTable.rows.length > 0) {
|
|
637
788
|
if (tableStack.length > 0) {
|
|
638
789
|
const parentTable = tableStack.pop();
|
|
@@ -653,7 +804,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
653
804
|
case "tr":
|
|
654
805
|
if (tableCtx) {
|
|
655
806
|
tableCtx.currentRow = [];
|
|
656
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
807
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
657
808
|
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
658
809
|
tableCtx.currentRow = [];
|
|
659
810
|
}
|
|
@@ -661,7 +812,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
661
812
|
case "tc":
|
|
662
813
|
if (tableCtx) {
|
|
663
814
|
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
664
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
815
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
665
816
|
if (tableCtx.cell) {
|
|
666
817
|
tableCtx.currentRow.push(tableCtx.cell);
|
|
667
818
|
tableCtx.cell = null;
|
|
@@ -689,24 +840,29 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
689
840
|
blocks.push(block);
|
|
690
841
|
}
|
|
691
842
|
}
|
|
692
|
-
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
843
|
+
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
693
844
|
break;
|
|
694
845
|
}
|
|
695
|
-
// 이미지/그림 — 경고
|
|
846
|
+
// 이미지/그림 — 경로 추출 또는 경고
|
|
696
847
|
case "pic":
|
|
697
848
|
case "shape":
|
|
698
|
-
case "drawingObject":
|
|
699
|
-
|
|
849
|
+
case "drawingObject": {
|
|
850
|
+
const imgRef = extractImageRef(el);
|
|
851
|
+
if (imgRef) {
|
|
852
|
+
blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
|
|
853
|
+
} else if (warnings && sectionNum) {
|
|
700
854
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
701
855
|
}
|
|
702
856
|
break;
|
|
857
|
+
}
|
|
703
858
|
default:
|
|
704
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
859
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
705
860
|
break;
|
|
706
861
|
}
|
|
707
862
|
}
|
|
708
863
|
}
|
|
709
|
-
function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
|
|
864
|
+
function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
|
|
865
|
+
if (depth > MAX_XML_DEPTH) return tableCtx;
|
|
710
866
|
const children = node.childNodes;
|
|
711
867
|
if (!children) return tableCtx;
|
|
712
868
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -717,7 +873,7 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
|
|
|
717
873
|
if (localTag === "tbl") {
|
|
718
874
|
if (tableCtx) tableStack.push(tableCtx);
|
|
719
875
|
const newTable = { rows: [], currentRow: [], cell: null };
|
|
720
|
-
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
|
|
876
|
+
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
721
877
|
if (newTable.rows.length > 0) {
|
|
722
878
|
if (tableStack.length > 0) {
|
|
723
879
|
const parentTable = tableStack.pop();
|
|
@@ -734,7 +890,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
|
|
|
734
890
|
tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
|
|
735
891
|
}
|
|
736
892
|
} else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
|
|
737
|
-
|
|
893
|
+
const imgRef = extractImageRef(el);
|
|
894
|
+
if (imgRef) {
|
|
895
|
+
blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
|
|
896
|
+
} else if (warnings && sectionNum) {
|
|
738
897
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
739
898
|
}
|
|
740
899
|
}
|
|
@@ -950,6 +1109,7 @@ function extractText(data) {
|
|
|
950
1109
|
break;
|
|
951
1110
|
case CHAR_TAB:
|
|
952
1111
|
result += " ";
|
|
1112
|
+
if (i + 14 <= data.length) i += 14;
|
|
953
1113
|
break;
|
|
954
1114
|
case CHAR_HYPHEN:
|
|
955
1115
|
result += "-";
|
|
@@ -1006,24 +1166,34 @@ function parseHwp5Document(buffer, options) {
|
|
|
1006
1166
|
if (sections.length === 0) throw new KordocError("\uC139\uC158 \uC2A4\uD2B8\uB9BC\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
1007
1167
|
metadata.pageCount = sections.length;
|
|
1008
1168
|
const pageFilter = options?.pages ? parsePageRange(options.pages, sections.length) : null;
|
|
1169
|
+
const totalTarget = pageFilter ? pageFilter.size : sections.length;
|
|
1009
1170
|
const blocks = [];
|
|
1010
1171
|
let totalDecompressed = 0;
|
|
1172
|
+
let parsedSections = 0;
|
|
1011
1173
|
for (let si = 0; si < sections.length; si++) {
|
|
1012
1174
|
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1175
|
+
try {
|
|
1176
|
+
const sectionData = sections[si];
|
|
1177
|
+
const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
|
|
1178
|
+
totalDecompressed += data.length;
|
|
1179
|
+
if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
|
|
1180
|
+
const records = readRecords(data);
|
|
1181
|
+
const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
|
|
1182
|
+
blocks.push(...sectionBlocks);
|
|
1183
|
+
parsedSections++;
|
|
1184
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
1185
|
+
} catch (secErr) {
|
|
1186
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
1187
|
+
warnings.push({ page: si + 1, message: `\uC139\uC158 ${si + 1} \uD30C\uC2F1 \uC2E4\uD328: ${secErr instanceof Error ? secErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
const images = extractHwp5Images(cfb, blocks, compressed, warnings);
|
|
1021
1191
|
if (docInfo) {
|
|
1022
1192
|
detectHwp5Headings(blocks, docInfo);
|
|
1023
1193
|
}
|
|
1024
1194
|
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
1025
1195
|
const markdown = blocksToMarkdown(blocks);
|
|
1026
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1196
|
+
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0, images: images.length > 0 ? images : void 0 };
|
|
1027
1197
|
}
|
|
1028
1198
|
function parseDocInfoStream(cfb, compressed) {
|
|
1029
1199
|
try {
|
|
@@ -1133,6 +1303,78 @@ function findSections(cfb) {
|
|
|
1133
1303
|
}
|
|
1134
1304
|
return sections.sort((a, b) => a.idx - b.idx).map((s) => s.content);
|
|
1135
1305
|
}
|
|
1306
|
+
var TAG_SHAPE_COMPONENT = 74;
|
|
1307
|
+
function extractBinDataId(records, ctrlIdx) {
|
|
1308
|
+
const ctrlLevel = records[ctrlIdx].level;
|
|
1309
|
+
for (let j = ctrlIdx + 1; j < records.length && j < ctrlIdx + 50; j++) {
|
|
1310
|
+
const r = records[j];
|
|
1311
|
+
if (r.level <= ctrlLevel) break;
|
|
1312
|
+
if (r.data.length >= 2) {
|
|
1313
|
+
if (r.tagId > TAG_SHAPE_COMPONENT && r.level > ctrlLevel + 1 && r.data.length >= 4) {
|
|
1314
|
+
const possibleId = r.data.readUInt16LE(0);
|
|
1315
|
+
if (possibleId < 1e4) return possibleId;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return -1;
|
|
1320
|
+
}
|
|
1321
|
+
function detectImageMime(data) {
|
|
1322
|
+
if (data.length < 4) return null;
|
|
1323
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
|
|
1324
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
1325
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
|
|
1326
|
+
if (data[0] === 66 && data[1] === 77) return "image/bmp";
|
|
1327
|
+
if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
|
|
1328
|
+
if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
function extractHwp5Images(cfb, blocks, compressed, warnings) {
|
|
1332
|
+
const binDataMap = /* @__PURE__ */ new Map();
|
|
1333
|
+
for (let idx = 0; idx < 1e4; idx++) {
|
|
1334
|
+
const entry = CFB.find(cfb, `/BinData/BIN${String(idx).padStart(4, "0")}`) || CFB.find(cfb, `/BinData/Bin${String(idx).padStart(4, "0")}`);
|
|
1335
|
+
if (!entry?.content) {
|
|
1336
|
+
if (idx > 0) break;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
let data = Buffer.from(entry.content);
|
|
1340
|
+
if (compressed) {
|
|
1341
|
+
try {
|
|
1342
|
+
data = decompressStream(data);
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
|
|
1347
|
+
}
|
|
1348
|
+
if (binDataMap.size === 0) return [];
|
|
1349
|
+
const images = [];
|
|
1350
|
+
let imageIndex = 0;
|
|
1351
|
+
for (const block of blocks) {
|
|
1352
|
+
if (block.type !== "image" || !block.text) continue;
|
|
1353
|
+
const binId = parseInt(block.text, 10);
|
|
1354
|
+
if (isNaN(binId)) continue;
|
|
1355
|
+
const bin = binDataMap.get(binId);
|
|
1356
|
+
if (!bin) {
|
|
1357
|
+
warnings.push({ page: block.pageNumber, message: `BinData ${binId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
|
|
1358
|
+
block.type = "paragraph";
|
|
1359
|
+
block.text = `[\uC774\uBBF8\uC9C0: BinData ${binId}]`;
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const mime = detectImageMime(bin.data);
|
|
1363
|
+
if (!mime) {
|
|
1364
|
+
warnings.push({ page: block.pageNumber, message: `BinData ${binId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
|
|
1365
|
+
block.type = "paragraph";
|
|
1366
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${bin.name}]`;
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
imageIndex++;
|
|
1370
|
+
const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
|
|
1371
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
|
|
1372
|
+
images.push({ filename, data: new Uint8Array(bin.data), mimeType: mime });
|
|
1373
|
+
block.text = filename;
|
|
1374
|
+
block.imageData = { data: new Uint8Array(bin.data), mimeType: mime, filename: bin.name };
|
|
1375
|
+
}
|
|
1376
|
+
return images;
|
|
1377
|
+
}
|
|
1136
1378
|
function parseSection(records, docInfo, warnings, sectionNum) {
|
|
1137
1379
|
const blocks = [];
|
|
1138
1380
|
let i = 0;
|
|
@@ -1160,7 +1402,14 @@ function parseSection(records, docInfo, warnings, sectionNum) {
|
|
|
1160
1402
|
i = nextIdx;
|
|
1161
1403
|
continue;
|
|
1162
1404
|
}
|
|
1163
|
-
if (ctrlId === "gso " || ctrlId === " osg"
|
|
1405
|
+
if (ctrlId === "gso " || ctrlId === " osg") {
|
|
1406
|
+
const binId = extractBinDataId(records, i);
|
|
1407
|
+
if (binId >= 0) {
|
|
1408
|
+
blocks.push({ type: "image", text: String(binId), pageNumber: sectionNum });
|
|
1409
|
+
} else {
|
|
1410
|
+
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
|
|
1411
|
+
}
|
|
1412
|
+
} else if (ctrlId === " elo" || ctrlId === "ole ") {
|
|
1164
1413
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
|
|
1165
1414
|
}
|
|
1166
1415
|
}
|
|
@@ -1250,9 +1499,13 @@ function parseCellBlock(records, startIdx, tableLevel) {
|
|
|
1250
1499
|
const texts = [];
|
|
1251
1500
|
let colSpan = 1;
|
|
1252
1501
|
let rowSpan = 1;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1502
|
+
let colAddr;
|
|
1503
|
+
let rowAddr;
|
|
1504
|
+
if (rec.data.length >= 16) {
|
|
1505
|
+
colAddr = rec.data.readUInt16LE(8);
|
|
1506
|
+
rowAddr = rec.data.readUInt16LE(10);
|
|
1507
|
+
const cs = rec.data.readUInt16LE(12);
|
|
1508
|
+
const rs = rec.data.readUInt16LE(14);
|
|
1256
1509
|
if (cs > 0) colSpan = Math.min(cs, MAX_COLS);
|
|
1257
1510
|
if (rs > 0) rowSpan = Math.min(rs, MAX_ROWS);
|
|
1258
1511
|
}
|
|
@@ -1267,15 +1520,16 @@ function parseCellBlock(records, startIdx, tableLevel) {
|
|
|
1267
1520
|
}
|
|
1268
1521
|
i++;
|
|
1269
1522
|
}
|
|
1270
|
-
return { cell: { text: texts.join("\n"), colSpan, rowSpan }, nextIdx: i };
|
|
1523
|
+
return { cell: { text: texts.join("\n"), colSpan, rowSpan, colAddr, rowAddr }, nextIdx: i };
|
|
1271
1524
|
}
|
|
1272
1525
|
function arrangeCells(rows, cols, cells) {
|
|
1273
1526
|
const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
for (
|
|
1277
|
-
|
|
1278
|
-
const
|
|
1527
|
+
const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
|
|
1528
|
+
if (hasAddr) {
|
|
1529
|
+
for (const cell of cells) {
|
|
1530
|
+
const r = cell.rowAddr ?? 0;
|
|
1531
|
+
const c = cell.colAddr ?? 0;
|
|
1532
|
+
if (r >= rows || c >= cols) continue;
|
|
1279
1533
|
grid[r][c] = cell;
|
|
1280
1534
|
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1281
1535
|
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
@@ -1285,6 +1539,22 @@ function arrangeCells(rows, cols, cells) {
|
|
|
1285
1539
|
}
|
|
1286
1540
|
}
|
|
1287
1541
|
}
|
|
1542
|
+
} else {
|
|
1543
|
+
let cellIdx = 0;
|
|
1544
|
+
for (let r = 0; r < rows && cellIdx < cells.length; r++) {
|
|
1545
|
+
for (let c = 0; c < cols && cellIdx < cells.length; c++) {
|
|
1546
|
+
if (grid[r][c] !== null) continue;
|
|
1547
|
+
const cell = cells[cellIdx++];
|
|
1548
|
+
grid[r][c] = cell;
|
|
1549
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1550
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
1551
|
+
if (dr === 0 && dc === 0) continue;
|
|
1552
|
+
if (r + dr < rows && c + dc < cols)
|
|
1553
|
+
grid[r + dr][c + dc] = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1288
1558
|
}
|
|
1289
1559
|
return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
|
|
1290
1560
|
}
|
|
@@ -1846,13 +2116,26 @@ import { getDocument, GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mj
|
|
|
1846
2116
|
GlobalWorkerOptions.workerSrc = "";
|
|
1847
2117
|
var MAX_PAGES = 5e3;
|
|
1848
2118
|
var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
|
|
1849
|
-
|
|
1850
|
-
|
|
2119
|
+
var PDF_LOAD_TIMEOUT_MS = 3e4;
|
|
2120
|
+
async function loadPdfWithTimeout(buffer) {
|
|
2121
|
+
const loadingTask = getDocument({
|
|
1851
2122
|
data: new Uint8Array(buffer),
|
|
1852
2123
|
useSystemFonts: true,
|
|
1853
2124
|
disableFontFace: true,
|
|
1854
2125
|
isEvalSupported: false
|
|
1855
|
-
})
|
|
2126
|
+
});
|
|
2127
|
+
return Promise.race([
|
|
2128
|
+
loadingTask.promise,
|
|
2129
|
+
new Promise(
|
|
2130
|
+
(_, reject) => setTimeout(() => {
|
|
2131
|
+
loadingTask.destroy();
|
|
2132
|
+
reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
|
|
2133
|
+
}, PDF_LOAD_TIMEOUT_MS)
|
|
2134
|
+
)
|
|
2135
|
+
]);
|
|
2136
|
+
}
|
|
2137
|
+
async function parsePdfDocument(buffer, options) {
|
|
2138
|
+
const doc = await loadPdfWithTimeout(buffer);
|
|
1856
2139
|
try {
|
|
1857
2140
|
const pageCount = doc.numPages;
|
|
1858
2141
|
if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
|
|
@@ -1864,32 +2147,43 @@ async function parsePdfDocument(buffer, options) {
|
|
|
1864
2147
|
let totalTextBytes = 0;
|
|
1865
2148
|
const effectivePageCount = Math.min(pageCount, MAX_PAGES);
|
|
1866
2149
|
const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
|
|
2150
|
+
const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
|
|
1867
2151
|
const allFontSizes = [];
|
|
2152
|
+
const pageHeights = /* @__PURE__ */ new Map();
|
|
2153
|
+
let parsedPages = 0;
|
|
1868
2154
|
for (let i = 1; i <= effectivePageCount; i++) {
|
|
1869
2155
|
if (pageFilter && !pageFilter.has(i)) continue;
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
const
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2156
|
+
try {
|
|
2157
|
+
const page = await doc.getPage(i);
|
|
2158
|
+
const tc = await page.getTextContent();
|
|
2159
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
2160
|
+
pageHeights.set(i, viewport.height);
|
|
2161
|
+
const rawItems = tc.items;
|
|
2162
|
+
const items = normalizeItems(rawItems);
|
|
2163
|
+
const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
|
|
2164
|
+
if (hiddenCount > 0) {
|
|
2165
|
+
warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2166
|
+
}
|
|
2167
|
+
for (const item of visible) {
|
|
2168
|
+
if (item.fontSize > 0) allFontSizes.push(item.fontSize);
|
|
2169
|
+
}
|
|
2170
|
+
const opList = await page.getOperatorList();
|
|
2171
|
+
const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
|
|
2172
|
+
for (const b of pageBlocks) blocks.push(b);
|
|
2173
|
+
for (const b of pageBlocks) {
|
|
2174
|
+
const t = b.text || "";
|
|
2175
|
+
totalChars += t.replace(/\s/g, "").length;
|
|
2176
|
+
totalTextBytes += t.length * 2;
|
|
2177
|
+
}
|
|
2178
|
+
if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
|
|
2179
|
+
parsedPages++;
|
|
2180
|
+
options?.onProgress?.(parsedPages, totalTarget);
|
|
2181
|
+
} catch (pageErr) {
|
|
2182
|
+
if (pageErr instanceof KordocError) throw pageErr;
|
|
2183
|
+
warnings.push({ page: i, message: `\uD398\uC774\uC9C0 ${i} \uD30C\uC2F1 \uC2E4\uD328: ${pageErr instanceof Error ? pageErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
|
|
1893
2187
|
if (totalChars / Math.max(parsedPageCount, 1) < 10) {
|
|
1894
2188
|
if (options?.ocr) {
|
|
1895
2189
|
try {
|
|
@@ -1904,6 +2198,12 @@ async function parsePdfDocument(buffer, options) {
|
|
|
1904
2198
|
}
|
|
1905
2199
|
return { success: false, fileType: "pdf", pageCount, isImageBased: true, error: `\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (${pageCount}\uD398\uC774\uC9C0, ${totalChars}\uC790)`, code: "IMAGE_BASED_PDF" };
|
|
1906
2200
|
}
|
|
2201
|
+
if (options?.removeHeaderFooter && parsedPageCount >= 3) {
|
|
2202
|
+
const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
|
|
2203
|
+
for (let ri = removed.length - 1; ri >= 0; ri--) {
|
|
2204
|
+
blocks.splice(removed[ri], 1);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
1907
2207
|
const medianFontSize = computeMedianFontSize(allFontSizes);
|
|
1908
2208
|
if (medianFontSize > 0) {
|
|
1909
2209
|
detectHeadings(blocks, medianFontSize);
|
|
@@ -2085,8 +2385,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
2085
2385
|
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
2086
2386
|
);
|
|
2087
2387
|
for (const cell of cells) {
|
|
2088
|
-
const
|
|
2089
|
-
const text = cellTextToString(
|
|
2388
|
+
const cellItems = cellTextMap.get(cell) || [];
|
|
2389
|
+
const text = cellTextToString(cellItems);
|
|
2090
2390
|
irGrid[cell.row][cell.col] = {
|
|
2091
2391
|
text,
|
|
2092
2392
|
colSpan: cell.colSpan,
|
|
@@ -2504,6 +2804,7 @@ function detectListBlocks(blocks) {
|
|
|
2504
2804
|
return result;
|
|
2505
2805
|
}
|
|
2506
2806
|
var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
|
|
2807
|
+
var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
|
|
2507
2808
|
function detectSpecialKoreanTables(blocks) {
|
|
2508
2809
|
const result = [];
|
|
2509
2810
|
let kvLines = [];
|
|
@@ -2569,16 +2870,18 @@ function detectSpecialKoreanTables(blocks) {
|
|
|
2569
2870
|
}
|
|
2570
2871
|
continue;
|
|
2571
2872
|
}
|
|
2572
|
-
if (kvLines.length > 0 && text.includes(":")
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2873
|
+
if (kvLines.length > 0 && text.includes(":")) {
|
|
2874
|
+
if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
|
|
2875
|
+
const colonIdx = text.indexOf(":");
|
|
2876
|
+
const key = text.slice(0, colonIdx).trim();
|
|
2877
|
+
if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
|
|
2878
|
+
kvLines.push({
|
|
2879
|
+
key,
|
|
2880
|
+
value: text.slice(colonIdx + 1).trim(),
|
|
2881
|
+
block
|
|
2882
|
+
});
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2582
2885
|
}
|
|
2583
2886
|
}
|
|
2584
2887
|
flushKvTable();
|
|
@@ -2587,6 +2890,62 @@ function detectSpecialKoreanTables(blocks) {
|
|
|
2587
2890
|
flushKvTable();
|
|
2588
2891
|
return result;
|
|
2589
2892
|
}
|
|
2893
|
+
function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
|
|
2894
|
+
const ZONE_RATIO = 0.1;
|
|
2895
|
+
const MIN_REPEAT = 3;
|
|
2896
|
+
const headerTexts = /* @__PURE__ */ new Map();
|
|
2897
|
+
const footerTexts = /* @__PURE__ */ new Map();
|
|
2898
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2899
|
+
const b = blocks[bi];
|
|
2900
|
+
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
2901
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2902
|
+
if (!ph) continue;
|
|
2903
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2904
|
+
const blockBottom = ph - b.bbox.y;
|
|
2905
|
+
if (blockBottom <= ph * ZONE_RATIO) {
|
|
2906
|
+
const arr = footerTexts.get(b.pageNumber) || [];
|
|
2907
|
+
arr.push(b.text.trim());
|
|
2908
|
+
footerTexts.set(b.pageNumber, arr);
|
|
2909
|
+
} else if (blockTop >= ph * (1 - ZONE_RATIO)) {
|
|
2910
|
+
const arr = headerTexts.get(b.pageNumber) || [];
|
|
2911
|
+
arr.push(b.text.trim());
|
|
2912
|
+
headerTexts.set(b.pageNumber, arr);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
const repeatedPatterns = /* @__PURE__ */ new Set();
|
|
2916
|
+
for (const textsMap of [headerTexts, footerTexts]) {
|
|
2917
|
+
const patternCount = /* @__PURE__ */ new Map();
|
|
2918
|
+
for (const [, texts] of textsMap) {
|
|
2919
|
+
for (const t of texts) {
|
|
2920
|
+
const normalized = t.replace(/\d+/g, "#");
|
|
2921
|
+
patternCount.set(normalized, (patternCount.get(normalized) || 0) + 1);
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
for (const [pattern, count] of patternCount) {
|
|
2925
|
+
if (count >= MIN_REPEAT) repeatedPatterns.add(pattern);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
if (repeatedPatterns.size === 0) return [];
|
|
2929
|
+
const removeIndices = [];
|
|
2930
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2931
|
+
const b = blocks[bi];
|
|
2932
|
+
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
2933
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2934
|
+
if (!ph) continue;
|
|
2935
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2936
|
+
const blockBottom = ph - b.bbox.y;
|
|
2937
|
+
const inZone = blockBottom <= ph * ZONE_RATIO || blockTop >= ph * (1 - ZONE_RATIO);
|
|
2938
|
+
if (!inZone) continue;
|
|
2939
|
+
const normalized = b.text.trim().replace(/\d+/g, "#");
|
|
2940
|
+
if (repeatedPatterns.has(normalized)) {
|
|
2941
|
+
removeIndices.push(bi);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
if (removeIndices.length > 0) {
|
|
2945
|
+
warnings.push({ message: `${removeIndices.length}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2946
|
+
}
|
|
2947
|
+
return removeIndices;
|
|
2948
|
+
}
|
|
2590
2949
|
function mergeKoreanLines(text) {
|
|
2591
2950
|
if (!text) return "";
|
|
2592
2951
|
const lines = text.split("\n");
|
|
@@ -2599,7 +2958,7 @@ function mergeKoreanLines(text) {
|
|
|
2599
2958
|
result.push(curr);
|
|
2600
2959
|
continue;
|
|
2601
2960
|
}
|
|
2602
|
-
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
|
|
2961
|
+
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
|
|
2603
2962
|
result[result.length - 1] = prev + " " + curr;
|
|
2604
2963
|
} else {
|
|
2605
2964
|
result.push(curr);
|
|
@@ -2622,7 +2981,9 @@ function normalizedSimilarity(a, b) {
|
|
|
2622
2981
|
function normalize(s) {
|
|
2623
2982
|
return s.replace(/\s+/g, " ").trim();
|
|
2624
2983
|
}
|
|
2984
|
+
var MAX_LEVENSHTEIN_LEN = 1e4;
|
|
2625
2985
|
function levenshtein(a, b) {
|
|
2986
|
+
if (a.length + b.length > MAX_LEVENSHTEIN_LEN) return Math.abs(a.length - b.length);
|
|
2626
2987
|
if (a.length > b.length) [a, b] = [b, a];
|
|
2627
2988
|
const m = a.length;
|
|
2628
2989
|
const n = b.length;
|
|
@@ -2935,7 +3296,7 @@ function parseMarkdownToBlocks(md) {
|
|
|
2935
3296
|
const tableRows = [];
|
|
2936
3297
|
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
2937
3298
|
const row = lines[i];
|
|
2938
|
-
if (
|
|
3299
|
+
if (/^[\s|:\-]+$/.test(row)) {
|
|
2939
3300
|
i++;
|
|
2940
3301
|
continue;
|
|
2941
3302
|
}
|
|
@@ -2999,7 +3360,21 @@ function generateManifest() {
|
|
|
2999
3360
|
}
|
|
3000
3361
|
|
|
3001
3362
|
// src/index.ts
|
|
3002
|
-
async function parse(
|
|
3363
|
+
async function parse(input, options) {
|
|
3364
|
+
let buffer;
|
|
3365
|
+
if (typeof input === "string") {
|
|
3366
|
+
try {
|
|
3367
|
+
const buf = await readFile(input);
|
|
3368
|
+
buffer = toArrayBuffer(buf);
|
|
3369
|
+
} catch (err) {
|
|
3370
|
+
const msg = err instanceof Error && "code" in err && err.code === "ENOENT" ? `\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input}` : `\uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${input}`;
|
|
3371
|
+
return { success: false, fileType: "unknown", error: msg, code: "PARSE_ERROR" };
|
|
3372
|
+
}
|
|
3373
|
+
} else if (Buffer.isBuffer(input)) {
|
|
3374
|
+
buffer = toArrayBuffer(input);
|
|
3375
|
+
} else {
|
|
3376
|
+
buffer = input;
|
|
3377
|
+
}
|
|
3003
3378
|
if (!buffer || buffer.byteLength === 0) {
|
|
3004
3379
|
return { success: false, fileType: "unknown", error: "\uBE48 \uBC84\uD37C\uC774\uAC70\uB098 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC785\uB825\uC785\uB2C8\uB2E4.", code: "EMPTY_INPUT" };
|
|
3005
3380
|
}
|
|
@@ -3017,16 +3392,16 @@ async function parse(buffer, options) {
|
|
|
3017
3392
|
}
|
|
3018
3393
|
async function parseHwpx(buffer, options) {
|
|
3019
3394
|
try {
|
|
3020
|
-
const { markdown, blocks, metadata, outline, warnings } = await parseHwpxDocument(buffer, options);
|
|
3021
|
-
return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings };
|
|
3395
|
+
const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
|
|
3396
|
+
return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
|
|
3022
3397
|
} catch (err) {
|
|
3023
3398
|
return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
|
|
3024
3399
|
}
|
|
3025
3400
|
}
|
|
3026
3401
|
async function parseHwp(buffer, options) {
|
|
3027
3402
|
try {
|
|
3028
|
-
const { markdown, blocks, metadata, outline, warnings } = parseHwp5Document(Buffer.from(buffer), options);
|
|
3029
|
-
return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings };
|
|
3403
|
+
const { markdown, blocks, metadata, outline, warnings, images } = parseHwp5Document(Buffer.from(buffer), options);
|
|
3404
|
+
return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
|
|
3030
3405
|
} catch (err) {
|
|
3031
3406
|
return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
|
|
3032
3407
|
}
|