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
|
@@ -25,6 +25,12 @@ function detectFormat(buffer) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// src/table/builder.ts
|
|
28
|
+
var SAFE_HREF_RE = /^(?:https?:|mailto:|tel:|#)/i;
|
|
29
|
+
function sanitizeHref(href) {
|
|
30
|
+
const trimmed = href.trim();
|
|
31
|
+
if (!trimmed || !SAFE_HREF_RE.test(trimmed)) return null;
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
28
34
|
var MAX_COLS = 200;
|
|
29
35
|
var MAX_ROWS = 1e4;
|
|
30
36
|
function buildTable(rows) {
|
|
@@ -90,6 +96,10 @@ function blocksToMarkdown(blocks) {
|
|
|
90
96
|
lines.push("", `${prefix} ${block.text}`, "");
|
|
91
97
|
continue;
|
|
92
98
|
}
|
|
99
|
+
if (block.type === "image" && block.text) {
|
|
100
|
+
lines.push("", ``, "");
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
93
103
|
if (block.type === "separator") {
|
|
94
104
|
lines.push("", "---", "");
|
|
95
105
|
continue;
|
|
@@ -123,7 +133,8 @@ function blocksToMarkdown(blocks) {
|
|
|
123
133
|
continue;
|
|
124
134
|
}
|
|
125
135
|
if (block.href) {
|
|
126
|
-
|
|
136
|
+
const href = sanitizeHref(block.href);
|
|
137
|
+
if (href) text = `[${text}](${href})`;
|
|
127
138
|
}
|
|
128
139
|
if (block.footnoteText) {
|
|
129
140
|
text += ` (\uC8FC: ${block.footnoteText})`;
|
|
@@ -185,7 +196,7 @@ function tableToMarkdown(table) {
|
|
|
185
196
|
}
|
|
186
197
|
|
|
187
198
|
// src/utils.ts
|
|
188
|
-
var VERSION = true ? "1.
|
|
199
|
+
var VERSION = true ? "1.7.0" : "0.0.0-dev";
|
|
189
200
|
function toArrayBuffer(buf) {
|
|
190
201
|
if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
|
|
191
202
|
return buf.buffer;
|
|
@@ -203,6 +214,7 @@ function sanitizeError(err) {
|
|
|
203
214
|
return "\uBB38\uC11C \uCC98\uB9AC \uC911 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4";
|
|
204
215
|
}
|
|
205
216
|
function isPathTraversal(name) {
|
|
217
|
+
if (name.includes("\0")) return true;
|
|
206
218
|
const normalized = name.replace(/\\/g, "/");
|
|
207
219
|
return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
|
|
208
220
|
}
|
|
@@ -259,7 +271,16 @@ var MAX_ZIP_ENTRIES = 500;
|
|
|
259
271
|
function clampSpan(val, max) {
|
|
260
272
|
return Math.max(1, Math.min(val, max));
|
|
261
273
|
}
|
|
262
|
-
|
|
274
|
+
var MAX_XML_DEPTH = 200;
|
|
275
|
+
function createXmlParser(warnings) {
|
|
276
|
+
return new DOMParser({
|
|
277
|
+
onError(level, msg) {
|
|
278
|
+
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg}`);
|
|
279
|
+
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg}` });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function extractHwpxStyles(zip, decompressed) {
|
|
263
284
|
const result = {
|
|
264
285
|
charProperties: /* @__PURE__ */ new Map(),
|
|
265
286
|
styles: /* @__PURE__ */ new Map()
|
|
@@ -271,7 +292,11 @@ async function extractHwpxStyles(zip) {
|
|
|
271
292
|
if (!file) continue;
|
|
272
293
|
try {
|
|
273
294
|
const xml = await file.async("text");
|
|
274
|
-
|
|
295
|
+
if (decompressed) {
|
|
296
|
+
decompressed.total += xml.length * 2;
|
|
297
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
298
|
+
}
|
|
299
|
+
const parser = createXmlParser();
|
|
275
300
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
276
301
|
if (!doc.documentElement) continue;
|
|
277
302
|
parseCharProperties(doc, result.charProperties);
|
|
@@ -349,37 +374,128 @@ async function parseHwpxDocument(buffer, options) {
|
|
|
349
374
|
if (actualEntryCount > MAX_ZIP_ENTRIES) {
|
|
350
375
|
throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
351
376
|
}
|
|
377
|
+
const decompressed = { total: 0 };
|
|
352
378
|
const metadata = {};
|
|
353
|
-
await extractHwpxMetadata(zip, metadata);
|
|
354
|
-
const styleMap = await extractHwpxStyles(zip);
|
|
379
|
+
await extractHwpxMetadata(zip, metadata, decompressed);
|
|
380
|
+
const styleMap = await extractHwpxStyles(zip, decompressed);
|
|
355
381
|
const warnings = [];
|
|
356
382
|
const sectionPaths = await resolveSectionPaths(zip);
|
|
357
383
|
if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
358
384
|
metadata.pageCount = sectionPaths.length;
|
|
359
385
|
const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
|
|
360
|
-
|
|
386
|
+
const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
|
|
361
387
|
const blocks = [];
|
|
388
|
+
let parsedSections = 0;
|
|
362
389
|
for (let si = 0; si < sectionPaths.length; si++) {
|
|
363
390
|
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
364
391
|
const file = zip.file(sectionPaths[si]);
|
|
365
392
|
if (!file) continue;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
393
|
+
try {
|
|
394
|
+
const xml = await file.async("text");
|
|
395
|
+
decompressed.total += xml.length * 2;
|
|
396
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
397
|
+
blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
|
|
398
|
+
parsedSections++;
|
|
399
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
400
|
+
} catch (secErr) {
|
|
401
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
402
|
+
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" });
|
|
403
|
+
}
|
|
370
404
|
}
|
|
405
|
+
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
371
406
|
detectHwpxHeadings(blocks, styleMap);
|
|
372
407
|
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
373
408
|
const markdown = blocksToMarkdown(blocks);
|
|
374
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
409
|
+
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 };
|
|
410
|
+
}
|
|
411
|
+
function imageExtToMime(ext) {
|
|
412
|
+
switch (ext.toLowerCase()) {
|
|
413
|
+
case "jpg":
|
|
414
|
+
case "jpeg":
|
|
415
|
+
return "image/jpeg";
|
|
416
|
+
case "png":
|
|
417
|
+
return "image/png";
|
|
418
|
+
case "gif":
|
|
419
|
+
return "image/gif";
|
|
420
|
+
case "bmp":
|
|
421
|
+
return "image/bmp";
|
|
422
|
+
case "tif":
|
|
423
|
+
case "tiff":
|
|
424
|
+
return "image/tiff";
|
|
425
|
+
case "wmf":
|
|
426
|
+
return "image/wmf";
|
|
427
|
+
case "emf":
|
|
428
|
+
return "image/emf";
|
|
429
|
+
case "svg":
|
|
430
|
+
return "image/svg+xml";
|
|
431
|
+
default:
|
|
432
|
+
return "application/octet-stream";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function mimeToExt(mime) {
|
|
436
|
+
if (mime.includes("jpeg")) return "jpg";
|
|
437
|
+
if (mime.includes("png")) return "png";
|
|
438
|
+
if (mime.includes("gif")) return "gif";
|
|
439
|
+
if (mime.includes("bmp")) return "bmp";
|
|
440
|
+
if (mime.includes("tiff")) return "tif";
|
|
441
|
+
if (mime.includes("wmf")) return "wmf";
|
|
442
|
+
if (mime.includes("emf")) return "emf";
|
|
443
|
+
if (mime.includes("svg")) return "svg";
|
|
444
|
+
return "bin";
|
|
445
|
+
}
|
|
446
|
+
async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
|
|
447
|
+
const images = [];
|
|
448
|
+
let imageIndex = 0;
|
|
449
|
+
for (const block of blocks) {
|
|
450
|
+
if (block.type !== "image" || !block.text) continue;
|
|
451
|
+
const ref = block.text;
|
|
452
|
+
const candidates = [
|
|
453
|
+
`BinData/${ref}`,
|
|
454
|
+
`Contents/BinData/${ref}`,
|
|
455
|
+
ref
|
|
456
|
+
// 절대 경로일 수도 있음
|
|
457
|
+
];
|
|
458
|
+
let found = false;
|
|
459
|
+
for (const path of candidates) {
|
|
460
|
+
if (isPathTraversal(path)) continue;
|
|
461
|
+
const file = zip.file(path);
|
|
462
|
+
if (!file) continue;
|
|
463
|
+
try {
|
|
464
|
+
const data = await file.async("uint8array");
|
|
465
|
+
decompressed.total += data.length;
|
|
466
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
467
|
+
const ext = ref.includes(".") ? ref.split(".").pop() : "png";
|
|
468
|
+
const mimeType = imageExtToMime(ext);
|
|
469
|
+
imageIndex++;
|
|
470
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
471
|
+
images.push({ filename, data, mimeType });
|
|
472
|
+
block.text = filename;
|
|
473
|
+
block.imageData = { data, mimeType, filename: ref };
|
|
474
|
+
found = true;
|
|
475
|
+
break;
|
|
476
|
+
} catch (err) {
|
|
477
|
+
if (err instanceof KordocError) throw err;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!found) {
|
|
481
|
+
warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
|
|
482
|
+
block.type = "paragraph";
|
|
483
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return images;
|
|
375
487
|
}
|
|
376
|
-
async function extractHwpxMetadata(zip, metadata) {
|
|
488
|
+
async function extractHwpxMetadata(zip, metadata, decompressed) {
|
|
377
489
|
try {
|
|
378
490
|
const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
|
|
379
491
|
for (const mp of metaPaths) {
|
|
380
492
|
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
|
|
381
493
|
if (!file) continue;
|
|
382
494
|
const xml = await file.async("text");
|
|
495
|
+
if (decompressed) {
|
|
496
|
+
decompressed.total += xml.length * 2;
|
|
497
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
498
|
+
}
|
|
383
499
|
parseDublinCoreMetadata(xml, metadata);
|
|
384
500
|
if (metadata.title || metadata.author) return;
|
|
385
501
|
}
|
|
@@ -387,7 +503,7 @@ async function extractHwpxMetadata(zip, metadata) {
|
|
|
387
503
|
}
|
|
388
504
|
}
|
|
389
505
|
function parseDublinCoreMetadata(xml, metadata) {
|
|
390
|
-
const parser =
|
|
506
|
+
const parser = createXmlParser();
|
|
391
507
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
392
508
|
if (!doc.documentElement) return;
|
|
393
509
|
const getText = (tagNames) => {
|
|
@@ -464,7 +580,14 @@ function extractFromBrokenZip(buffer) {
|
|
|
464
580
|
let totalDecompressed = 0;
|
|
465
581
|
let entryCount = 0;
|
|
466
582
|
while (pos < data.length - 30) {
|
|
467
|
-
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4)
|
|
583
|
+
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
|
|
584
|
+
pos++;
|
|
585
|
+
while (pos < data.length - 30) {
|
|
586
|
+
if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
|
|
587
|
+
pos++;
|
|
588
|
+
}
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
468
591
|
if (++entryCount > MAX_ZIP_ENTRIES) break;
|
|
469
592
|
const method = view.getUint16(pos + 8, true);
|
|
470
593
|
const compSize = view.getUint32(pos + 18, true);
|
|
@@ -524,7 +647,7 @@ async function resolveSectionPaths(zip) {
|
|
|
524
647
|
return sectionFiles.map((f) => f.name).sort();
|
|
525
648
|
}
|
|
526
649
|
function parseSectionPathsFromManifest(xml) {
|
|
527
|
-
const parser =
|
|
650
|
+
const parser = createXmlParser();
|
|
528
651
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
529
652
|
const items = doc.getElementsByTagName("opf:item");
|
|
530
653
|
const spine = doc.getElementsByTagName("opf:itemref");
|
|
@@ -586,14 +709,33 @@ function detectHwpxHeadings(blocks, styleMap) {
|
|
|
586
709
|
}
|
|
587
710
|
}
|
|
588
711
|
function parseSectionXml(xml, styleMap, warnings, sectionNum) {
|
|
589
|
-
const parser =
|
|
712
|
+
const parser = createXmlParser(warnings);
|
|
590
713
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
591
714
|
if (!doc.documentElement) return [];
|
|
592
715
|
const blocks = [];
|
|
593
716
|
walkSection(doc.documentElement, blocks, null, [], styleMap, warnings, sectionNum);
|
|
594
717
|
return blocks;
|
|
595
718
|
}
|
|
596
|
-
function
|
|
719
|
+
function extractImageRef(el) {
|
|
720
|
+
const children = el.childNodes;
|
|
721
|
+
if (!children) return null;
|
|
722
|
+
for (let i = 0; i < children.length; i++) {
|
|
723
|
+
const child = children[i];
|
|
724
|
+
if (child.nodeType !== 1) continue;
|
|
725
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
726
|
+
if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
727
|
+
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
728
|
+
if (ref) return ref;
|
|
729
|
+
}
|
|
730
|
+
const nested = extractImageRef(child);
|
|
731
|
+
if (nested) return nested;
|
|
732
|
+
}
|
|
733
|
+
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
734
|
+
if (directRef) return directRef;
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
|
|
738
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
597
739
|
const children = node.childNodes;
|
|
598
740
|
if (!children) return;
|
|
599
741
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -605,7 +747,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
605
747
|
case "tbl": {
|
|
606
748
|
if (tableCtx) tableStack.push(tableCtx);
|
|
607
749
|
const newTable = { rows: [], currentRow: [], cell: null };
|
|
608
|
-
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
|
|
750
|
+
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
609
751
|
if (newTable.rows.length > 0) {
|
|
610
752
|
if (tableStack.length > 0) {
|
|
611
753
|
const parentTable = tableStack.pop();
|
|
@@ -626,7 +768,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
626
768
|
case "tr":
|
|
627
769
|
if (tableCtx) {
|
|
628
770
|
tableCtx.currentRow = [];
|
|
629
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
771
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
630
772
|
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
631
773
|
tableCtx.currentRow = [];
|
|
632
774
|
}
|
|
@@ -634,7 +776,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
634
776
|
case "tc":
|
|
635
777
|
if (tableCtx) {
|
|
636
778
|
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
637
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
779
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
638
780
|
if (tableCtx.cell) {
|
|
639
781
|
tableCtx.currentRow.push(tableCtx.cell);
|
|
640
782
|
tableCtx.cell = null;
|
|
@@ -662,24 +804,29 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
662
804
|
blocks.push(block);
|
|
663
805
|
}
|
|
664
806
|
}
|
|
665
|
-
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
807
|
+
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
666
808
|
break;
|
|
667
809
|
}
|
|
668
|
-
// 이미지/그림 — 경고
|
|
810
|
+
// 이미지/그림 — 경로 추출 또는 경고
|
|
669
811
|
case "pic":
|
|
670
812
|
case "shape":
|
|
671
|
-
case "drawingObject":
|
|
672
|
-
|
|
813
|
+
case "drawingObject": {
|
|
814
|
+
const imgRef = extractImageRef(el);
|
|
815
|
+
if (imgRef) {
|
|
816
|
+
blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
|
|
817
|
+
} else if (warnings && sectionNum) {
|
|
673
818
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
674
819
|
}
|
|
675
820
|
break;
|
|
821
|
+
}
|
|
676
822
|
default:
|
|
677
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
823
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
678
824
|
break;
|
|
679
825
|
}
|
|
680
826
|
}
|
|
681
827
|
}
|
|
682
|
-
function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
|
|
828
|
+
function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
|
|
829
|
+
if (depth > MAX_XML_DEPTH) return tableCtx;
|
|
683
830
|
const children = node.childNodes;
|
|
684
831
|
if (!children) return tableCtx;
|
|
685
832
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -690,7 +837,7 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
|
|
|
690
837
|
if (localTag === "tbl") {
|
|
691
838
|
if (tableCtx) tableStack.push(tableCtx);
|
|
692
839
|
const newTable = { rows: [], currentRow: [], cell: null };
|
|
693
|
-
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
|
|
840
|
+
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
694
841
|
if (newTable.rows.length > 0) {
|
|
695
842
|
if (tableStack.length > 0) {
|
|
696
843
|
const parentTable = tableStack.pop();
|
|
@@ -707,7 +854,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
|
|
|
707
854
|
tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
|
|
708
855
|
}
|
|
709
856
|
} else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
|
|
710
|
-
|
|
857
|
+
const imgRef = extractImageRef(el);
|
|
858
|
+
if (imgRef) {
|
|
859
|
+
blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
|
|
860
|
+
} else if (warnings && sectionNum) {
|
|
711
861
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
712
862
|
}
|
|
713
863
|
}
|
|
@@ -923,6 +1073,7 @@ function extractText(data) {
|
|
|
923
1073
|
break;
|
|
924
1074
|
case CHAR_TAB:
|
|
925
1075
|
result += " ";
|
|
1076
|
+
if (i + 14 <= data.length) i += 14;
|
|
926
1077
|
break;
|
|
927
1078
|
case CHAR_HYPHEN:
|
|
928
1079
|
result += "-";
|
|
@@ -979,24 +1130,34 @@ function parseHwp5Document(buffer, options) {
|
|
|
979
1130
|
if (sections.length === 0) throw new KordocError("\uC139\uC158 \uC2A4\uD2B8\uB9BC\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
980
1131
|
metadata.pageCount = sections.length;
|
|
981
1132
|
const pageFilter = options?.pages ? parsePageRange(options.pages, sections.length) : null;
|
|
1133
|
+
const totalTarget = pageFilter ? pageFilter.size : sections.length;
|
|
982
1134
|
const blocks = [];
|
|
983
1135
|
let totalDecompressed = 0;
|
|
1136
|
+
let parsedSections = 0;
|
|
984
1137
|
for (let si = 0; si < sections.length; si++) {
|
|
985
1138
|
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1139
|
+
try {
|
|
1140
|
+
const sectionData = sections[si];
|
|
1141
|
+
const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
|
|
1142
|
+
totalDecompressed += data.length;
|
|
1143
|
+
if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
|
|
1144
|
+
const records = readRecords(data);
|
|
1145
|
+
const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
|
|
1146
|
+
blocks.push(...sectionBlocks);
|
|
1147
|
+
parsedSections++;
|
|
1148
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
1149
|
+
} catch (secErr) {
|
|
1150
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
1151
|
+
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" });
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const images = extractHwp5Images(cfb, blocks, compressed, warnings);
|
|
994
1155
|
if (docInfo) {
|
|
995
1156
|
detectHwp5Headings(blocks, docInfo);
|
|
996
1157
|
}
|
|
997
1158
|
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
998
1159
|
const markdown = blocksToMarkdown(blocks);
|
|
999
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1160
|
+
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 };
|
|
1000
1161
|
}
|
|
1001
1162
|
function parseDocInfoStream(cfb, compressed) {
|
|
1002
1163
|
try {
|
|
@@ -1120,6 +1281,78 @@ function findSections(cfb) {
|
|
|
1120
1281
|
}
|
|
1121
1282
|
return sections.sort((a, b) => a.idx - b.idx).map((s) => s.content);
|
|
1122
1283
|
}
|
|
1284
|
+
var TAG_SHAPE_COMPONENT = 74;
|
|
1285
|
+
function extractBinDataId(records, ctrlIdx) {
|
|
1286
|
+
const ctrlLevel = records[ctrlIdx].level;
|
|
1287
|
+
for (let j = ctrlIdx + 1; j < records.length && j < ctrlIdx + 50; j++) {
|
|
1288
|
+
const r = records[j];
|
|
1289
|
+
if (r.level <= ctrlLevel) break;
|
|
1290
|
+
if (r.data.length >= 2) {
|
|
1291
|
+
if (r.tagId > TAG_SHAPE_COMPONENT && r.level > ctrlLevel + 1 && r.data.length >= 4) {
|
|
1292
|
+
const possibleId = r.data.readUInt16LE(0);
|
|
1293
|
+
if (possibleId < 1e4) return possibleId;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return -1;
|
|
1298
|
+
}
|
|
1299
|
+
function detectImageMime(data) {
|
|
1300
|
+
if (data.length < 4) return null;
|
|
1301
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
|
|
1302
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
1303
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
|
|
1304
|
+
if (data[0] === 66 && data[1] === 77) return "image/bmp";
|
|
1305
|
+
if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
|
|
1306
|
+
if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
function extractHwp5Images(cfb, blocks, compressed, warnings) {
|
|
1310
|
+
const binDataMap = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (let idx = 0; idx < 1e4; idx++) {
|
|
1312
|
+
const entry = CFB.find(cfb, `/BinData/BIN${String(idx).padStart(4, "0")}`) || CFB.find(cfb, `/BinData/Bin${String(idx).padStart(4, "0")}`);
|
|
1313
|
+
if (!entry?.content) {
|
|
1314
|
+
if (idx > 0) break;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
let data = Buffer.from(entry.content);
|
|
1318
|
+
if (compressed) {
|
|
1319
|
+
try {
|
|
1320
|
+
data = decompressStream(data);
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
|
|
1325
|
+
}
|
|
1326
|
+
if (binDataMap.size === 0) return [];
|
|
1327
|
+
const images = [];
|
|
1328
|
+
let imageIndex = 0;
|
|
1329
|
+
for (const block of blocks) {
|
|
1330
|
+
if (block.type !== "image" || !block.text) continue;
|
|
1331
|
+
const binId = parseInt(block.text, 10);
|
|
1332
|
+
if (isNaN(binId)) continue;
|
|
1333
|
+
const bin = binDataMap.get(binId);
|
|
1334
|
+
if (!bin) {
|
|
1335
|
+
warnings.push({ page: block.pageNumber, message: `BinData ${binId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
|
|
1336
|
+
block.type = "paragraph";
|
|
1337
|
+
block.text = `[\uC774\uBBF8\uC9C0: BinData ${binId}]`;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const mime = detectImageMime(bin.data);
|
|
1341
|
+
if (!mime) {
|
|
1342
|
+
warnings.push({ page: block.pageNumber, message: `BinData ${binId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
|
|
1343
|
+
block.type = "paragraph";
|
|
1344
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${bin.name}]`;
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
imageIndex++;
|
|
1348
|
+
const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
|
|
1349
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
|
|
1350
|
+
images.push({ filename, data: new Uint8Array(bin.data), mimeType: mime });
|
|
1351
|
+
block.text = filename;
|
|
1352
|
+
block.imageData = { data: new Uint8Array(bin.data), mimeType: mime, filename: bin.name };
|
|
1353
|
+
}
|
|
1354
|
+
return images;
|
|
1355
|
+
}
|
|
1123
1356
|
function parseSection(records, docInfo, warnings, sectionNum) {
|
|
1124
1357
|
const blocks = [];
|
|
1125
1358
|
let i = 0;
|
|
@@ -1147,7 +1380,14 @@ function parseSection(records, docInfo, warnings, sectionNum) {
|
|
|
1147
1380
|
i = nextIdx;
|
|
1148
1381
|
continue;
|
|
1149
1382
|
}
|
|
1150
|
-
if (ctrlId === "gso " || ctrlId === " osg"
|
|
1383
|
+
if (ctrlId === "gso " || ctrlId === " osg") {
|
|
1384
|
+
const binId = extractBinDataId(records, i);
|
|
1385
|
+
if (binId >= 0) {
|
|
1386
|
+
blocks.push({ type: "image", text: String(binId), pageNumber: sectionNum });
|
|
1387
|
+
} else {
|
|
1388
|
+
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
|
|
1389
|
+
}
|
|
1390
|
+
} else if (ctrlId === " elo" || ctrlId === "ole ") {
|
|
1151
1391
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
|
|
1152
1392
|
}
|
|
1153
1393
|
}
|
|
@@ -1237,9 +1477,13 @@ function parseCellBlock(records, startIdx, tableLevel) {
|
|
|
1237
1477
|
const texts = [];
|
|
1238
1478
|
let colSpan = 1;
|
|
1239
1479
|
let rowSpan = 1;
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1480
|
+
let colAddr;
|
|
1481
|
+
let rowAddr;
|
|
1482
|
+
if (rec.data.length >= 16) {
|
|
1483
|
+
colAddr = rec.data.readUInt16LE(8);
|
|
1484
|
+
rowAddr = rec.data.readUInt16LE(10);
|
|
1485
|
+
const cs = rec.data.readUInt16LE(12);
|
|
1486
|
+
const rs = rec.data.readUInt16LE(14);
|
|
1243
1487
|
if (cs > 0) colSpan = Math.min(cs, MAX_COLS);
|
|
1244
1488
|
if (rs > 0) rowSpan = Math.min(rs, MAX_ROWS);
|
|
1245
1489
|
}
|
|
@@ -1254,15 +1498,16 @@ function parseCellBlock(records, startIdx, tableLevel) {
|
|
|
1254
1498
|
}
|
|
1255
1499
|
i++;
|
|
1256
1500
|
}
|
|
1257
|
-
return { cell: { text: texts.join("\n"), colSpan, rowSpan }, nextIdx: i };
|
|
1501
|
+
return { cell: { text: texts.join("\n"), colSpan, rowSpan, colAddr, rowAddr }, nextIdx: i };
|
|
1258
1502
|
}
|
|
1259
1503
|
function arrangeCells(rows, cols, cells) {
|
|
1260
1504
|
const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
for (
|
|
1264
|
-
|
|
1265
|
-
const
|
|
1505
|
+
const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
|
|
1506
|
+
if (hasAddr) {
|
|
1507
|
+
for (const cell of cells) {
|
|
1508
|
+
const r = cell.rowAddr ?? 0;
|
|
1509
|
+
const c = cell.colAddr ?? 0;
|
|
1510
|
+
if (r >= rows || c >= cols) continue;
|
|
1266
1511
|
grid[r][c] = cell;
|
|
1267
1512
|
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1268
1513
|
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
@@ -1272,6 +1517,22 @@ function arrangeCells(rows, cols, cells) {
|
|
|
1272
1517
|
}
|
|
1273
1518
|
}
|
|
1274
1519
|
}
|
|
1520
|
+
} else {
|
|
1521
|
+
let cellIdx = 0;
|
|
1522
|
+
for (let r = 0; r < rows && cellIdx < cells.length; r++) {
|
|
1523
|
+
for (let c = 0; c < cols && cellIdx < cells.length; c++) {
|
|
1524
|
+
if (grid[r][c] !== null) continue;
|
|
1525
|
+
const cell = cells[cellIdx++];
|
|
1526
|
+
grid[r][c] = cell;
|
|
1527
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1528
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
1529
|
+
if (dr === 0 && dc === 0) continue;
|
|
1530
|
+
if (r + dr < rows && c + dc < cols)
|
|
1531
|
+
grid[r + dr][c + dc] = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1275
1536
|
}
|
|
1276
1537
|
return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
|
|
1277
1538
|
}
|
|
@@ -1833,13 +2094,26 @@ import { getDocument, GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mj
|
|
|
1833
2094
|
GlobalWorkerOptions.workerSrc = "";
|
|
1834
2095
|
var MAX_PAGES = 5e3;
|
|
1835
2096
|
var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
|
|
1836
|
-
|
|
1837
|
-
|
|
2097
|
+
var PDF_LOAD_TIMEOUT_MS = 3e4;
|
|
2098
|
+
async function loadPdfWithTimeout(buffer) {
|
|
2099
|
+
const loadingTask = getDocument({
|
|
1838
2100
|
data: new Uint8Array(buffer),
|
|
1839
2101
|
useSystemFonts: true,
|
|
1840
2102
|
disableFontFace: true,
|
|
1841
2103
|
isEvalSupported: false
|
|
1842
|
-
})
|
|
2104
|
+
});
|
|
2105
|
+
return Promise.race([
|
|
2106
|
+
loadingTask.promise,
|
|
2107
|
+
new Promise(
|
|
2108
|
+
(_, reject) => setTimeout(() => {
|
|
2109
|
+
loadingTask.destroy();
|
|
2110
|
+
reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
|
|
2111
|
+
}, PDF_LOAD_TIMEOUT_MS)
|
|
2112
|
+
)
|
|
2113
|
+
]);
|
|
2114
|
+
}
|
|
2115
|
+
async function parsePdfDocument(buffer, options) {
|
|
2116
|
+
const doc = await loadPdfWithTimeout(buffer);
|
|
1843
2117
|
try {
|
|
1844
2118
|
const pageCount = doc.numPages;
|
|
1845
2119
|
if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
|
|
@@ -1851,32 +2125,43 @@ async function parsePdfDocument(buffer, options) {
|
|
|
1851
2125
|
let totalTextBytes = 0;
|
|
1852
2126
|
const effectivePageCount = Math.min(pageCount, MAX_PAGES);
|
|
1853
2127
|
const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
|
|
2128
|
+
const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
|
|
1854
2129
|
const allFontSizes = [];
|
|
2130
|
+
const pageHeights = /* @__PURE__ */ new Map();
|
|
2131
|
+
let parsedPages = 0;
|
|
1855
2132
|
for (let i = 1; i <= effectivePageCount; i++) {
|
|
1856
2133
|
if (pageFilter && !pageFilter.has(i)) continue;
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2134
|
+
try {
|
|
2135
|
+
const page = await doc.getPage(i);
|
|
2136
|
+
const tc = await page.getTextContent();
|
|
2137
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
2138
|
+
pageHeights.set(i, viewport.height);
|
|
2139
|
+
const rawItems = tc.items;
|
|
2140
|
+
const items = normalizeItems(rawItems);
|
|
2141
|
+
const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
|
|
2142
|
+
if (hiddenCount > 0) {
|
|
2143
|
+
warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2144
|
+
}
|
|
2145
|
+
for (const item of visible) {
|
|
2146
|
+
if (item.fontSize > 0) allFontSizes.push(item.fontSize);
|
|
2147
|
+
}
|
|
2148
|
+
const opList = await page.getOperatorList();
|
|
2149
|
+
const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
|
|
2150
|
+
for (const b of pageBlocks) blocks.push(b);
|
|
2151
|
+
for (const b of pageBlocks) {
|
|
2152
|
+
const t = b.text || "";
|
|
2153
|
+
totalChars += t.replace(/\s/g, "").length;
|
|
2154
|
+
totalTextBytes += t.length * 2;
|
|
2155
|
+
}
|
|
2156
|
+
if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
|
|
2157
|
+
parsedPages++;
|
|
2158
|
+
options?.onProgress?.(parsedPages, totalTarget);
|
|
2159
|
+
} catch (pageErr) {
|
|
2160
|
+
if (pageErr instanceof KordocError) throw pageErr;
|
|
2161
|
+
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" });
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
|
|
1880
2165
|
if (totalChars / Math.max(parsedPageCount, 1) < 10) {
|
|
1881
2166
|
if (options?.ocr) {
|
|
1882
2167
|
try {
|
|
@@ -1891,6 +2176,12 @@ async function parsePdfDocument(buffer, options) {
|
|
|
1891
2176
|
}
|
|
1892
2177
|
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" };
|
|
1893
2178
|
}
|
|
2179
|
+
if (options?.removeHeaderFooter && parsedPageCount >= 3) {
|
|
2180
|
+
const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
|
|
2181
|
+
for (let ri = removed.length - 1; ri >= 0; ri--) {
|
|
2182
|
+
blocks.splice(removed[ri], 1);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
1894
2185
|
const medianFontSize = computeMedianFontSize(allFontSizes);
|
|
1895
2186
|
if (medianFontSize > 0) {
|
|
1896
2187
|
detectHeadings(blocks, medianFontSize);
|
|
@@ -1927,12 +2218,7 @@ function parsePdfDate(dateStr) {
|
|
|
1927
2218
|
return `${year}-${month}-${day}T${hour}:${min}:${sec}`;
|
|
1928
2219
|
}
|
|
1929
2220
|
async function extractPdfMetadataOnly(buffer) {
|
|
1930
|
-
const doc = await
|
|
1931
|
-
data: new Uint8Array(buffer),
|
|
1932
|
-
useSystemFonts: true,
|
|
1933
|
-
disableFontFace: true,
|
|
1934
|
-
isEvalSupported: false
|
|
1935
|
-
}).promise;
|
|
2221
|
+
const doc = await loadPdfWithTimeout(buffer);
|
|
1936
2222
|
try {
|
|
1937
2223
|
const metadata = { pageCount: doc.numPages };
|
|
1938
2224
|
await extractPdfMetadata(doc, metadata);
|
|
@@ -2088,8 +2374,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
2088
2374
|
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
2089
2375
|
);
|
|
2090
2376
|
for (const cell of cells) {
|
|
2091
|
-
const
|
|
2092
|
-
const text = cellTextToString(
|
|
2377
|
+
const cellItems = cellTextMap.get(cell) || [];
|
|
2378
|
+
const text = cellTextToString(cellItems);
|
|
2093
2379
|
irGrid[cell.row][cell.col] = {
|
|
2094
2380
|
text,
|
|
2095
2381
|
colSpan: cell.colSpan,
|
|
@@ -2507,6 +2793,7 @@ function detectListBlocks(blocks) {
|
|
|
2507
2793
|
return result;
|
|
2508
2794
|
}
|
|
2509
2795
|
var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
|
|
2796
|
+
var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
|
|
2510
2797
|
function detectSpecialKoreanTables(blocks) {
|
|
2511
2798
|
const result = [];
|
|
2512
2799
|
let kvLines = [];
|
|
@@ -2572,16 +2859,18 @@ function detectSpecialKoreanTables(blocks) {
|
|
|
2572
2859
|
}
|
|
2573
2860
|
continue;
|
|
2574
2861
|
}
|
|
2575
|
-
if (kvLines.length > 0 && text.includes(":")
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2862
|
+
if (kvLines.length > 0 && text.includes(":")) {
|
|
2863
|
+
if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
|
|
2864
|
+
const colonIdx = text.indexOf(":");
|
|
2865
|
+
const key = text.slice(0, colonIdx).trim();
|
|
2866
|
+
if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
|
|
2867
|
+
kvLines.push({
|
|
2868
|
+
key,
|
|
2869
|
+
value: text.slice(colonIdx + 1).trim(),
|
|
2870
|
+
block
|
|
2871
|
+
});
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2585
2874
|
}
|
|
2586
2875
|
}
|
|
2587
2876
|
flushKvTable();
|
|
@@ -2590,6 +2879,62 @@ function detectSpecialKoreanTables(blocks) {
|
|
|
2590
2879
|
flushKvTable();
|
|
2591
2880
|
return result;
|
|
2592
2881
|
}
|
|
2882
|
+
function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
|
|
2883
|
+
const ZONE_RATIO = 0.1;
|
|
2884
|
+
const MIN_REPEAT = 3;
|
|
2885
|
+
const headerTexts = /* @__PURE__ */ new Map();
|
|
2886
|
+
const footerTexts = /* @__PURE__ */ new Map();
|
|
2887
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2888
|
+
const b = blocks[bi];
|
|
2889
|
+
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
2890
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2891
|
+
if (!ph) continue;
|
|
2892
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2893
|
+
const blockBottom = ph - b.bbox.y;
|
|
2894
|
+
if (blockBottom <= ph * ZONE_RATIO) {
|
|
2895
|
+
const arr = footerTexts.get(b.pageNumber) || [];
|
|
2896
|
+
arr.push(b.text.trim());
|
|
2897
|
+
footerTexts.set(b.pageNumber, arr);
|
|
2898
|
+
} else if (blockTop >= ph * (1 - ZONE_RATIO)) {
|
|
2899
|
+
const arr = headerTexts.get(b.pageNumber) || [];
|
|
2900
|
+
arr.push(b.text.trim());
|
|
2901
|
+
headerTexts.set(b.pageNumber, arr);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
const repeatedPatterns = /* @__PURE__ */ new Set();
|
|
2905
|
+
for (const textsMap of [headerTexts, footerTexts]) {
|
|
2906
|
+
const patternCount = /* @__PURE__ */ new Map();
|
|
2907
|
+
for (const [, texts] of textsMap) {
|
|
2908
|
+
for (const t of texts) {
|
|
2909
|
+
const normalized = t.replace(/\d+/g, "#");
|
|
2910
|
+
patternCount.set(normalized, (patternCount.get(normalized) || 0) + 1);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
for (const [pattern, count] of patternCount) {
|
|
2914
|
+
if (count >= MIN_REPEAT) repeatedPatterns.add(pattern);
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
if (repeatedPatterns.size === 0) return [];
|
|
2918
|
+
const removeIndices = [];
|
|
2919
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2920
|
+
const b = blocks[bi];
|
|
2921
|
+
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
2922
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2923
|
+
if (!ph) continue;
|
|
2924
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2925
|
+
const blockBottom = ph - b.bbox.y;
|
|
2926
|
+
const inZone = blockBottom <= ph * ZONE_RATIO || blockTop >= ph * (1 - ZONE_RATIO);
|
|
2927
|
+
if (!inZone) continue;
|
|
2928
|
+
const normalized = b.text.trim().replace(/\d+/g, "#");
|
|
2929
|
+
if (repeatedPatterns.has(normalized)) {
|
|
2930
|
+
removeIndices.push(bi);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
if (removeIndices.length > 0) {
|
|
2934
|
+
warnings.push({ message: `${removeIndices.length}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2935
|
+
}
|
|
2936
|
+
return removeIndices;
|
|
2937
|
+
}
|
|
2593
2938
|
function mergeKoreanLines(text) {
|
|
2594
2939
|
if (!text) return "";
|
|
2595
2940
|
const lines = text.split("\n");
|
|
@@ -2602,7 +2947,7 @@ function mergeKoreanLines(text) {
|
|
|
2602
2947
|
result.push(curr);
|
|
2603
2948
|
continue;
|
|
2604
2949
|
}
|
|
2605
|
-
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
|
|
2950
|
+
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
|
|
2606
2951
|
result[result.length - 1] = prev + " " + curr;
|
|
2607
2952
|
} else {
|
|
2608
2953
|
result.push(curr);
|
|
@@ -2611,6 +2956,9 @@ function mergeKoreanLines(text) {
|
|
|
2611
2956
|
return result.join("\n");
|
|
2612
2957
|
}
|
|
2613
2958
|
|
|
2959
|
+
// src/index.ts
|
|
2960
|
+
import { readFile } from "fs/promises";
|
|
2961
|
+
|
|
2614
2962
|
// src/form/recognize.ts
|
|
2615
2963
|
var LABEL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2616
2964
|
"\uC131\uBA85",
|
|
@@ -2741,7 +3089,21 @@ function extractInlineFields(text) {
|
|
|
2741
3089
|
import JSZip2 from "jszip";
|
|
2742
3090
|
|
|
2743
3091
|
// src/index.ts
|
|
2744
|
-
async function parse(
|
|
3092
|
+
async function parse(input, options) {
|
|
3093
|
+
let buffer;
|
|
3094
|
+
if (typeof input === "string") {
|
|
3095
|
+
try {
|
|
3096
|
+
const buf = await readFile(input);
|
|
3097
|
+
buffer = toArrayBuffer(buf);
|
|
3098
|
+
} catch (err) {
|
|
3099
|
+
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}`;
|
|
3100
|
+
return { success: false, fileType: "unknown", error: msg, code: "PARSE_ERROR" };
|
|
3101
|
+
}
|
|
3102
|
+
} else if (Buffer.isBuffer(input)) {
|
|
3103
|
+
buffer = toArrayBuffer(input);
|
|
3104
|
+
} else {
|
|
3105
|
+
buffer = input;
|
|
3106
|
+
}
|
|
2745
3107
|
if (!buffer || buffer.byteLength === 0) {
|
|
2746
3108
|
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" };
|
|
2747
3109
|
}
|
|
@@ -2759,16 +3121,16 @@ async function parse(buffer, options) {
|
|
|
2759
3121
|
}
|
|
2760
3122
|
async function parseHwpx(buffer, options) {
|
|
2761
3123
|
try {
|
|
2762
|
-
const { markdown, blocks, metadata, outline, warnings } = await parseHwpxDocument(buffer, options);
|
|
2763
|
-
return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings };
|
|
3124
|
+
const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
|
|
3125
|
+
return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
|
|
2764
3126
|
} catch (err) {
|
|
2765
3127
|
return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
|
|
2766
3128
|
}
|
|
2767
3129
|
}
|
|
2768
3130
|
async function parseHwp(buffer, options) {
|
|
2769
3131
|
try {
|
|
2770
|
-
const { markdown, blocks, metadata, outline, warnings } = parseHwp5Document(Buffer.from(buffer), options);
|
|
2771
|
-
return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings };
|
|
3132
|
+
const { markdown, blocks, metadata, outline, warnings, images } = parseHwp5Document(Buffer.from(buffer), options);
|
|
3133
|
+
return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
|
|
2772
3134
|
} catch (err) {
|
|
2773
3135
|
return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
|
|
2774
3136
|
}
|
|
@@ -2795,7 +3157,9 @@ function normalizedSimilarity(a, b) {
|
|
|
2795
3157
|
function normalize(s) {
|
|
2796
3158
|
return s.replace(/\s+/g, " ").trim();
|
|
2797
3159
|
}
|
|
3160
|
+
var MAX_LEVENSHTEIN_LEN = 1e4;
|
|
2798
3161
|
function levenshtein(a, b) {
|
|
3162
|
+
if (a.length + b.length > MAX_LEVENSHTEIN_LEN) return Math.abs(a.length - b.length);
|
|
2799
3163
|
if (a.length > b.length) [a, b] = [b, a];
|
|
2800
3164
|
const m = a.length;
|
|
2801
3165
|
const n = b.length;
|
|
@@ -2964,4 +3328,4 @@ export {
|
|
|
2964
3328
|
extractFormFields,
|
|
2965
3329
|
parse
|
|
2966
3330
|
};
|
|
2967
|
-
//# sourceMappingURL=chunk-
|
|
3331
|
+
//# sourceMappingURL=chunk-YDXK5T53.js.map
|