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.cjs
CHANGED
|
@@ -91,6 +91,7 @@ __export(index_exports, {
|
|
|
91
91
|
parsePdf: () => parsePdf
|
|
92
92
|
});
|
|
93
93
|
module.exports = __toCommonJS(index_exports);
|
|
94
|
+
var import_promises = require("fs/promises");
|
|
94
95
|
|
|
95
96
|
// src/detect.ts
|
|
96
97
|
function magicBytes(buffer) {
|
|
@@ -122,6 +123,12 @@ var import_zlib = require("zlib");
|
|
|
122
123
|
var import_xmldom = require("@xmldom/xmldom");
|
|
123
124
|
|
|
124
125
|
// src/table/builder.ts
|
|
126
|
+
var SAFE_HREF_RE = /^(?:https?:|mailto:|tel:|#)/i;
|
|
127
|
+
function sanitizeHref(href) {
|
|
128
|
+
const trimmed = href.trim();
|
|
129
|
+
if (!trimmed || !SAFE_HREF_RE.test(trimmed)) return null;
|
|
130
|
+
return trimmed;
|
|
131
|
+
}
|
|
125
132
|
var MAX_COLS = 200;
|
|
126
133
|
var MAX_ROWS = 1e4;
|
|
127
134
|
function buildTable(rows) {
|
|
@@ -187,6 +194,10 @@ function blocksToMarkdown(blocks) {
|
|
|
187
194
|
lines.push("", `${prefix} ${block.text}`, "");
|
|
188
195
|
continue;
|
|
189
196
|
}
|
|
197
|
+
if (block.type === "image" && block.text) {
|
|
198
|
+
lines.push("", ``, "");
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
190
201
|
if (block.type === "separator") {
|
|
191
202
|
lines.push("", "---", "");
|
|
192
203
|
continue;
|
|
@@ -220,7 +231,8 @@ function blocksToMarkdown(blocks) {
|
|
|
220
231
|
continue;
|
|
221
232
|
}
|
|
222
233
|
if (block.href) {
|
|
223
|
-
|
|
234
|
+
const href = sanitizeHref(block.href);
|
|
235
|
+
if (href) text = `[${text}](${href})`;
|
|
224
236
|
}
|
|
225
237
|
if (block.footnoteText) {
|
|
226
238
|
text += ` (\uC8FC: ${block.footnoteText})`;
|
|
@@ -282,7 +294,13 @@ function tableToMarkdown(table) {
|
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
// src/utils.ts
|
|
285
|
-
var VERSION = true ? "1.
|
|
297
|
+
var VERSION = true ? "1.7.0" : "0.0.0-dev";
|
|
298
|
+
function toArrayBuffer(buf) {
|
|
299
|
+
if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
|
|
300
|
+
return buf.buffer;
|
|
301
|
+
}
|
|
302
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
303
|
+
}
|
|
286
304
|
var KordocError = class extends Error {
|
|
287
305
|
constructor(message) {
|
|
288
306
|
super(message);
|
|
@@ -290,6 +308,7 @@ var KordocError = class extends Error {
|
|
|
290
308
|
}
|
|
291
309
|
};
|
|
292
310
|
function isPathTraversal(name) {
|
|
311
|
+
if (name.includes("\0")) return true;
|
|
293
312
|
const normalized = name.replace(/\\/g, "/");
|
|
294
313
|
return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
|
|
295
314
|
}
|
|
@@ -341,7 +360,16 @@ var MAX_ZIP_ENTRIES = 500;
|
|
|
341
360
|
function clampSpan(val, max) {
|
|
342
361
|
return Math.max(1, Math.min(val, max));
|
|
343
362
|
}
|
|
344
|
-
|
|
363
|
+
var MAX_XML_DEPTH = 200;
|
|
364
|
+
function createXmlParser(warnings) {
|
|
365
|
+
return new import_xmldom.DOMParser({
|
|
366
|
+
onError(level, msg) {
|
|
367
|
+
if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg}`);
|
|
368
|
+
warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg}` });
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
async function extractHwpxStyles(zip, decompressed) {
|
|
345
373
|
const result = {
|
|
346
374
|
charProperties: /* @__PURE__ */ new Map(),
|
|
347
375
|
styles: /* @__PURE__ */ new Map()
|
|
@@ -353,7 +381,11 @@ async function extractHwpxStyles(zip) {
|
|
|
353
381
|
if (!file) continue;
|
|
354
382
|
try {
|
|
355
383
|
const xml = await file.async("text");
|
|
356
|
-
|
|
384
|
+
if (decompressed) {
|
|
385
|
+
decompressed.total += xml.length * 2;
|
|
386
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
387
|
+
}
|
|
388
|
+
const parser = createXmlParser();
|
|
357
389
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
358
390
|
if (!doc.documentElement) continue;
|
|
359
391
|
parseCharProperties(doc, result.charProperties);
|
|
@@ -431,37 +463,128 @@ async function parseHwpxDocument(buffer, options) {
|
|
|
431
463
|
if (actualEntryCount > MAX_ZIP_ENTRIES) {
|
|
432
464
|
throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
433
465
|
}
|
|
466
|
+
const decompressed = { total: 0 };
|
|
434
467
|
const metadata = {};
|
|
435
|
-
await extractHwpxMetadata(zip, metadata);
|
|
436
|
-
const styleMap = await extractHwpxStyles(zip);
|
|
468
|
+
await extractHwpxMetadata(zip, metadata, decompressed);
|
|
469
|
+
const styleMap = await extractHwpxStyles(zip, decompressed);
|
|
437
470
|
const warnings = [];
|
|
438
471
|
const sectionPaths = await resolveSectionPaths(zip);
|
|
439
472
|
if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
440
473
|
metadata.pageCount = sectionPaths.length;
|
|
441
474
|
const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
|
|
442
|
-
|
|
475
|
+
const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
|
|
443
476
|
const blocks = [];
|
|
477
|
+
let parsedSections = 0;
|
|
444
478
|
for (let si = 0; si < sectionPaths.length; si++) {
|
|
445
479
|
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
446
480
|
const file = zip.file(sectionPaths[si]);
|
|
447
481
|
if (!file) continue;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
482
|
+
try {
|
|
483
|
+
const xml = await file.async("text");
|
|
484
|
+
decompressed.total += xml.length * 2;
|
|
485
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
486
|
+
blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
|
|
487
|
+
parsedSections++;
|
|
488
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
489
|
+
} catch (secErr) {
|
|
490
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
491
|
+
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" });
|
|
492
|
+
}
|
|
452
493
|
}
|
|
494
|
+
const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
|
|
453
495
|
detectHwpxHeadings(blocks, styleMap);
|
|
454
496
|
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
455
497
|
const markdown = blocksToMarkdown(blocks);
|
|
456
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
498
|
+
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 };
|
|
499
|
+
}
|
|
500
|
+
function imageExtToMime(ext) {
|
|
501
|
+
switch (ext.toLowerCase()) {
|
|
502
|
+
case "jpg":
|
|
503
|
+
case "jpeg":
|
|
504
|
+
return "image/jpeg";
|
|
505
|
+
case "png":
|
|
506
|
+
return "image/png";
|
|
507
|
+
case "gif":
|
|
508
|
+
return "image/gif";
|
|
509
|
+
case "bmp":
|
|
510
|
+
return "image/bmp";
|
|
511
|
+
case "tif":
|
|
512
|
+
case "tiff":
|
|
513
|
+
return "image/tiff";
|
|
514
|
+
case "wmf":
|
|
515
|
+
return "image/wmf";
|
|
516
|
+
case "emf":
|
|
517
|
+
return "image/emf";
|
|
518
|
+
case "svg":
|
|
519
|
+
return "image/svg+xml";
|
|
520
|
+
default:
|
|
521
|
+
return "application/octet-stream";
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function mimeToExt(mime) {
|
|
525
|
+
if (mime.includes("jpeg")) return "jpg";
|
|
526
|
+
if (mime.includes("png")) return "png";
|
|
527
|
+
if (mime.includes("gif")) return "gif";
|
|
528
|
+
if (mime.includes("bmp")) return "bmp";
|
|
529
|
+
if (mime.includes("tiff")) return "tif";
|
|
530
|
+
if (mime.includes("wmf")) return "wmf";
|
|
531
|
+
if (mime.includes("emf")) return "emf";
|
|
532
|
+
if (mime.includes("svg")) return "svg";
|
|
533
|
+
return "bin";
|
|
534
|
+
}
|
|
535
|
+
async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
|
|
536
|
+
const images = [];
|
|
537
|
+
let imageIndex = 0;
|
|
538
|
+
for (const block of blocks) {
|
|
539
|
+
if (block.type !== "image" || !block.text) continue;
|
|
540
|
+
const ref = block.text;
|
|
541
|
+
const candidates = [
|
|
542
|
+
`BinData/${ref}`,
|
|
543
|
+
`Contents/BinData/${ref}`,
|
|
544
|
+
ref
|
|
545
|
+
// 절대 경로일 수도 있음
|
|
546
|
+
];
|
|
547
|
+
let found = false;
|
|
548
|
+
for (const path of candidates) {
|
|
549
|
+
if (isPathTraversal(path)) continue;
|
|
550
|
+
const file = zip.file(path);
|
|
551
|
+
if (!file) continue;
|
|
552
|
+
try {
|
|
553
|
+
const data = await file.async("uint8array");
|
|
554
|
+
decompressed.total += data.length;
|
|
555
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
556
|
+
const ext = ref.includes(".") ? ref.split(".").pop() : "png";
|
|
557
|
+
const mimeType = imageExtToMime(ext);
|
|
558
|
+
imageIndex++;
|
|
559
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
|
|
560
|
+
images.push({ filename, data, mimeType });
|
|
561
|
+
block.text = filename;
|
|
562
|
+
block.imageData = { data, mimeType, filename: ref };
|
|
563
|
+
found = true;
|
|
564
|
+
break;
|
|
565
|
+
} catch (err) {
|
|
566
|
+
if (err instanceof KordocError) throw err;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (!found) {
|
|
570
|
+
warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
|
|
571
|
+
block.type = "paragraph";
|
|
572
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return images;
|
|
457
576
|
}
|
|
458
|
-
async function extractHwpxMetadata(zip, metadata) {
|
|
577
|
+
async function extractHwpxMetadata(zip, metadata, decompressed) {
|
|
459
578
|
try {
|
|
460
579
|
const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
|
|
461
580
|
for (const mp of metaPaths) {
|
|
462
581
|
const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
|
|
463
582
|
if (!file) continue;
|
|
464
583
|
const xml = await file.async("text");
|
|
584
|
+
if (decompressed) {
|
|
585
|
+
decompressed.total += xml.length * 2;
|
|
586
|
+
if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
|
|
587
|
+
}
|
|
465
588
|
parseDublinCoreMetadata(xml, metadata);
|
|
466
589
|
if (metadata.title || metadata.author) return;
|
|
467
590
|
}
|
|
@@ -469,7 +592,7 @@ async function extractHwpxMetadata(zip, metadata) {
|
|
|
469
592
|
}
|
|
470
593
|
}
|
|
471
594
|
function parseDublinCoreMetadata(xml, metadata) {
|
|
472
|
-
const parser =
|
|
595
|
+
const parser = createXmlParser();
|
|
473
596
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
474
597
|
if (!doc.documentElement) return;
|
|
475
598
|
const getText = (tagNames) => {
|
|
@@ -533,7 +656,14 @@ function extractFromBrokenZip(buffer) {
|
|
|
533
656
|
let totalDecompressed = 0;
|
|
534
657
|
let entryCount = 0;
|
|
535
658
|
while (pos < data.length - 30) {
|
|
536
|
-
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4)
|
|
659
|
+
if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
|
|
660
|
+
pos++;
|
|
661
|
+
while (pos < data.length - 30) {
|
|
662
|
+
if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
|
|
663
|
+
pos++;
|
|
664
|
+
}
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
537
667
|
if (++entryCount > MAX_ZIP_ENTRIES) break;
|
|
538
668
|
const method = view.getUint16(pos + 8, true);
|
|
539
669
|
const compSize = view.getUint32(pos + 18, true);
|
|
@@ -593,7 +723,7 @@ async function resolveSectionPaths(zip) {
|
|
|
593
723
|
return sectionFiles.map((f) => f.name).sort();
|
|
594
724
|
}
|
|
595
725
|
function parseSectionPathsFromManifest(xml) {
|
|
596
|
-
const parser =
|
|
726
|
+
const parser = createXmlParser();
|
|
597
727
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
598
728
|
const items = doc.getElementsByTagName("opf:item");
|
|
599
729
|
const spine = doc.getElementsByTagName("opf:itemref");
|
|
@@ -655,14 +785,33 @@ function detectHwpxHeadings(blocks, styleMap) {
|
|
|
655
785
|
}
|
|
656
786
|
}
|
|
657
787
|
function parseSectionXml(xml, styleMap, warnings, sectionNum) {
|
|
658
|
-
const parser =
|
|
788
|
+
const parser = createXmlParser(warnings);
|
|
659
789
|
const doc = parser.parseFromString(stripDtd(xml), "text/xml");
|
|
660
790
|
if (!doc.documentElement) return [];
|
|
661
791
|
const blocks = [];
|
|
662
792
|
walkSection(doc.documentElement, blocks, null, [], styleMap, warnings, sectionNum);
|
|
663
793
|
return blocks;
|
|
664
794
|
}
|
|
665
|
-
function
|
|
795
|
+
function extractImageRef(el) {
|
|
796
|
+
const children = el.childNodes;
|
|
797
|
+
if (!children) return null;
|
|
798
|
+
for (let i = 0; i < children.length; i++) {
|
|
799
|
+
const child = children[i];
|
|
800
|
+
if (child.nodeType !== 1) continue;
|
|
801
|
+
const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
|
|
802
|
+
if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
|
|
803
|
+
const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
|
|
804
|
+
if (ref) return ref;
|
|
805
|
+
}
|
|
806
|
+
const nested = extractImageRef(child);
|
|
807
|
+
if (nested) return nested;
|
|
808
|
+
}
|
|
809
|
+
const directRef = el.getAttribute("binaryItemIDRef") || "";
|
|
810
|
+
if (directRef) return directRef;
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
|
|
814
|
+
if (depth > MAX_XML_DEPTH) return;
|
|
666
815
|
const children = node.childNodes;
|
|
667
816
|
if (!children) return;
|
|
668
817
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -674,7 +823,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
674
823
|
case "tbl": {
|
|
675
824
|
if (tableCtx) tableStack.push(tableCtx);
|
|
676
825
|
const newTable = { rows: [], currentRow: [], cell: null };
|
|
677
|
-
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
|
|
826
|
+
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
678
827
|
if (newTable.rows.length > 0) {
|
|
679
828
|
if (tableStack.length > 0) {
|
|
680
829
|
const parentTable = tableStack.pop();
|
|
@@ -695,7 +844,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
695
844
|
case "tr":
|
|
696
845
|
if (tableCtx) {
|
|
697
846
|
tableCtx.currentRow = [];
|
|
698
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
847
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
699
848
|
if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
|
|
700
849
|
tableCtx.currentRow = [];
|
|
701
850
|
}
|
|
@@ -703,7 +852,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
703
852
|
case "tc":
|
|
704
853
|
if (tableCtx) {
|
|
705
854
|
tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
|
|
706
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
855
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
707
856
|
if (tableCtx.cell) {
|
|
708
857
|
tableCtx.currentRow.push(tableCtx.cell);
|
|
709
858
|
tableCtx.cell = null;
|
|
@@ -731,24 +880,29 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
|
|
|
731
880
|
blocks.push(block);
|
|
732
881
|
}
|
|
733
882
|
}
|
|
734
|
-
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
883
|
+
tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
735
884
|
break;
|
|
736
885
|
}
|
|
737
|
-
// 이미지/그림 — 경고
|
|
886
|
+
// 이미지/그림 — 경로 추출 또는 경고
|
|
738
887
|
case "pic":
|
|
739
888
|
case "shape":
|
|
740
|
-
case "drawingObject":
|
|
741
|
-
|
|
889
|
+
case "drawingObject": {
|
|
890
|
+
const imgRef = extractImageRef(el);
|
|
891
|
+
if (imgRef) {
|
|
892
|
+
blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
|
|
893
|
+
} else if (warnings && sectionNum) {
|
|
742
894
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
743
895
|
}
|
|
744
896
|
break;
|
|
897
|
+
}
|
|
745
898
|
default:
|
|
746
|
-
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
|
|
899
|
+
walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
747
900
|
break;
|
|
748
901
|
}
|
|
749
902
|
}
|
|
750
903
|
}
|
|
751
|
-
function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
|
|
904
|
+
function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
|
|
905
|
+
if (depth > MAX_XML_DEPTH) return tableCtx;
|
|
752
906
|
const children = node.childNodes;
|
|
753
907
|
if (!children) return tableCtx;
|
|
754
908
|
for (let i = 0; i < children.length; i++) {
|
|
@@ -759,7 +913,7 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
|
|
|
759
913
|
if (localTag === "tbl") {
|
|
760
914
|
if (tableCtx) tableStack.push(tableCtx);
|
|
761
915
|
const newTable = { rows: [], currentRow: [], cell: null };
|
|
762
|
-
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
|
|
916
|
+
walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
|
|
763
917
|
if (newTable.rows.length > 0) {
|
|
764
918
|
if (tableStack.length > 0) {
|
|
765
919
|
const parentTable = tableStack.pop();
|
|
@@ -776,7 +930,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
|
|
|
776
930
|
tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
|
|
777
931
|
}
|
|
778
932
|
} else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
|
|
779
|
-
|
|
933
|
+
const imgRef = extractImageRef(el);
|
|
934
|
+
if (imgRef) {
|
|
935
|
+
blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
|
|
936
|
+
} else if (warnings && sectionNum) {
|
|
780
937
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
|
|
781
938
|
}
|
|
782
939
|
}
|
|
@@ -992,6 +1149,7 @@ function extractText(data) {
|
|
|
992
1149
|
break;
|
|
993
1150
|
case CHAR_TAB:
|
|
994
1151
|
result += " ";
|
|
1152
|
+
if (i + 14 <= data.length) i += 14;
|
|
995
1153
|
break;
|
|
996
1154
|
case CHAR_HYPHEN:
|
|
997
1155
|
result += "-";
|
|
@@ -1049,24 +1207,34 @@ function parseHwp5Document(buffer, options) {
|
|
|
1049
1207
|
if (sections.length === 0) throw new KordocError("\uC139\uC158 \uC2A4\uD2B8\uB9BC\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
|
|
1050
1208
|
metadata.pageCount = sections.length;
|
|
1051
1209
|
const pageFilter = options?.pages ? parsePageRange(options.pages, sections.length) : null;
|
|
1210
|
+
const totalTarget = pageFilter ? pageFilter.size : sections.length;
|
|
1052
1211
|
const blocks = [];
|
|
1053
1212
|
let totalDecompressed = 0;
|
|
1213
|
+
let parsedSections = 0;
|
|
1054
1214
|
for (let si = 0; si < sections.length; si++) {
|
|
1055
1215
|
if (pageFilter && !pageFilter.has(si + 1)) continue;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1216
|
+
try {
|
|
1217
|
+
const sectionData = sections[si];
|
|
1218
|
+
const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
|
|
1219
|
+
totalDecompressed += data.length;
|
|
1220
|
+
if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
|
|
1221
|
+
const records = readRecords(data);
|
|
1222
|
+
const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
|
|
1223
|
+
blocks.push(...sectionBlocks);
|
|
1224
|
+
parsedSections++;
|
|
1225
|
+
options?.onProgress?.(parsedSections, totalTarget);
|
|
1226
|
+
} catch (secErr) {
|
|
1227
|
+
if (secErr instanceof KordocError) throw secErr;
|
|
1228
|
+
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" });
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const images = extractHwp5Images(cfb, blocks, compressed, warnings);
|
|
1064
1232
|
if (docInfo) {
|
|
1065
1233
|
detectHwp5Headings(blocks, docInfo);
|
|
1066
1234
|
}
|
|
1067
1235
|
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
1068
1236
|
const markdown = blocksToMarkdown(blocks);
|
|
1069
|
-
return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1237
|
+
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 };
|
|
1070
1238
|
}
|
|
1071
1239
|
function parseDocInfoStream(cfb, compressed) {
|
|
1072
1240
|
try {
|
|
@@ -1176,6 +1344,78 @@ function findSections(cfb) {
|
|
|
1176
1344
|
}
|
|
1177
1345
|
return sections.sort((a, b) => a.idx - b.idx).map((s) => s.content);
|
|
1178
1346
|
}
|
|
1347
|
+
var TAG_SHAPE_COMPONENT = 74;
|
|
1348
|
+
function extractBinDataId(records, ctrlIdx) {
|
|
1349
|
+
const ctrlLevel = records[ctrlIdx].level;
|
|
1350
|
+
for (let j = ctrlIdx + 1; j < records.length && j < ctrlIdx + 50; j++) {
|
|
1351
|
+
const r = records[j];
|
|
1352
|
+
if (r.level <= ctrlLevel) break;
|
|
1353
|
+
if (r.data.length >= 2) {
|
|
1354
|
+
if (r.tagId > TAG_SHAPE_COMPONENT && r.level > ctrlLevel + 1 && r.data.length >= 4) {
|
|
1355
|
+
const possibleId = r.data.readUInt16LE(0);
|
|
1356
|
+
if (possibleId < 1e4) return possibleId;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return -1;
|
|
1361
|
+
}
|
|
1362
|
+
function detectImageMime(data) {
|
|
1363
|
+
if (data.length < 4) return null;
|
|
1364
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
|
|
1365
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
|
|
1366
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
|
|
1367
|
+
if (data[0] === 66 && data[1] === 77) return "image/bmp";
|
|
1368
|
+
if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
|
|
1369
|
+
if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
function extractHwp5Images(cfb, blocks, compressed, warnings) {
|
|
1373
|
+
const binDataMap = /* @__PURE__ */ new Map();
|
|
1374
|
+
for (let idx = 0; idx < 1e4; idx++) {
|
|
1375
|
+
const entry = CFB.find(cfb, `/BinData/BIN${String(idx).padStart(4, "0")}`) || CFB.find(cfb, `/BinData/Bin${String(idx).padStart(4, "0")}`);
|
|
1376
|
+
if (!entry?.content) {
|
|
1377
|
+
if (idx > 0) break;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
let data = Buffer.from(entry.content);
|
|
1381
|
+
if (compressed) {
|
|
1382
|
+
try {
|
|
1383
|
+
data = decompressStream(data);
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
|
|
1388
|
+
}
|
|
1389
|
+
if (binDataMap.size === 0) return [];
|
|
1390
|
+
const images = [];
|
|
1391
|
+
let imageIndex = 0;
|
|
1392
|
+
for (const block of blocks) {
|
|
1393
|
+
if (block.type !== "image" || !block.text) continue;
|
|
1394
|
+
const binId = parseInt(block.text, 10);
|
|
1395
|
+
if (isNaN(binId)) continue;
|
|
1396
|
+
const bin = binDataMap.get(binId);
|
|
1397
|
+
if (!bin) {
|
|
1398
|
+
warnings.push({ page: block.pageNumber, message: `BinData ${binId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
|
|
1399
|
+
block.type = "paragraph";
|
|
1400
|
+
block.text = `[\uC774\uBBF8\uC9C0: BinData ${binId}]`;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const mime = detectImageMime(bin.data);
|
|
1404
|
+
if (!mime) {
|
|
1405
|
+
warnings.push({ page: block.pageNumber, message: `BinData ${binId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
|
|
1406
|
+
block.type = "paragraph";
|
|
1407
|
+
block.text = `[\uC774\uBBF8\uC9C0: ${bin.name}]`;
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
imageIndex++;
|
|
1411
|
+
const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
|
|
1412
|
+
const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
|
|
1413
|
+
images.push({ filename, data: new Uint8Array(bin.data), mimeType: mime });
|
|
1414
|
+
block.text = filename;
|
|
1415
|
+
block.imageData = { data: new Uint8Array(bin.data), mimeType: mime, filename: bin.name };
|
|
1416
|
+
}
|
|
1417
|
+
return images;
|
|
1418
|
+
}
|
|
1179
1419
|
function parseSection(records, docInfo, warnings, sectionNum) {
|
|
1180
1420
|
const blocks = [];
|
|
1181
1421
|
let i = 0;
|
|
@@ -1203,7 +1443,14 @@ function parseSection(records, docInfo, warnings, sectionNum) {
|
|
|
1203
1443
|
i = nextIdx;
|
|
1204
1444
|
continue;
|
|
1205
1445
|
}
|
|
1206
|
-
if (ctrlId === "gso " || ctrlId === " osg"
|
|
1446
|
+
if (ctrlId === "gso " || ctrlId === " osg") {
|
|
1447
|
+
const binId = extractBinDataId(records, i);
|
|
1448
|
+
if (binId >= 0) {
|
|
1449
|
+
blocks.push({ type: "image", text: String(binId), pageNumber: sectionNum });
|
|
1450
|
+
} else {
|
|
1451
|
+
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
|
|
1452
|
+
}
|
|
1453
|
+
} else if (ctrlId === " elo" || ctrlId === "ole ") {
|
|
1207
1454
|
warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
|
|
1208
1455
|
}
|
|
1209
1456
|
}
|
|
@@ -1293,9 +1540,13 @@ function parseCellBlock(records, startIdx, tableLevel) {
|
|
|
1293
1540
|
const texts = [];
|
|
1294
1541
|
let colSpan = 1;
|
|
1295
1542
|
let rowSpan = 1;
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1543
|
+
let colAddr;
|
|
1544
|
+
let rowAddr;
|
|
1545
|
+
if (rec.data.length >= 16) {
|
|
1546
|
+
colAddr = rec.data.readUInt16LE(8);
|
|
1547
|
+
rowAddr = rec.data.readUInt16LE(10);
|
|
1548
|
+
const cs = rec.data.readUInt16LE(12);
|
|
1549
|
+
const rs = rec.data.readUInt16LE(14);
|
|
1299
1550
|
if (cs > 0) colSpan = Math.min(cs, MAX_COLS);
|
|
1300
1551
|
if (rs > 0) rowSpan = Math.min(rs, MAX_ROWS);
|
|
1301
1552
|
}
|
|
@@ -1310,15 +1561,16 @@ function parseCellBlock(records, startIdx, tableLevel) {
|
|
|
1310
1561
|
}
|
|
1311
1562
|
i++;
|
|
1312
1563
|
}
|
|
1313
|
-
return { cell: { text: texts.join("\n"), colSpan, rowSpan }, nextIdx: i };
|
|
1564
|
+
return { cell: { text: texts.join("\n"), colSpan, rowSpan, colAddr, rowAddr }, nextIdx: i };
|
|
1314
1565
|
}
|
|
1315
1566
|
function arrangeCells(rows, cols, cells) {
|
|
1316
1567
|
const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
for (
|
|
1320
|
-
|
|
1321
|
-
const
|
|
1568
|
+
const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
|
|
1569
|
+
if (hasAddr) {
|
|
1570
|
+
for (const cell of cells) {
|
|
1571
|
+
const r = cell.rowAddr ?? 0;
|
|
1572
|
+
const c = cell.colAddr ?? 0;
|
|
1573
|
+
if (r >= rows || c >= cols) continue;
|
|
1322
1574
|
grid[r][c] = cell;
|
|
1323
1575
|
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1324
1576
|
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
@@ -1328,6 +1580,22 @@ function arrangeCells(rows, cols, cells) {
|
|
|
1328
1580
|
}
|
|
1329
1581
|
}
|
|
1330
1582
|
}
|
|
1583
|
+
} else {
|
|
1584
|
+
let cellIdx = 0;
|
|
1585
|
+
for (let r = 0; r < rows && cellIdx < cells.length; r++) {
|
|
1586
|
+
for (let c = 0; c < cols && cellIdx < cells.length; c++) {
|
|
1587
|
+
if (grid[r][c] !== null) continue;
|
|
1588
|
+
const cell = cells[cellIdx++];
|
|
1589
|
+
grid[r][c] = cell;
|
|
1590
|
+
for (let dr = 0; dr < cell.rowSpan; dr++) {
|
|
1591
|
+
for (let dc = 0; dc < cell.colSpan; dc++) {
|
|
1592
|
+
if (dr === 0 && dc === 0) continue;
|
|
1593
|
+
if (r + dr < rows && c + dc < cols)
|
|
1594
|
+
grid[r + dr][c + dc] = { text: "", colSpan: 1, rowSpan: 1 };
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1331
1599
|
}
|
|
1332
1600
|
return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
|
|
1333
1601
|
}
|
|
@@ -1889,13 +2157,26 @@ var import_pdf2 = require("pdfjs-dist/legacy/build/pdf.mjs");
|
|
|
1889
2157
|
import_pdf2.GlobalWorkerOptions.workerSrc = "";
|
|
1890
2158
|
var MAX_PAGES = 5e3;
|
|
1891
2159
|
var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
|
|
1892
|
-
|
|
1893
|
-
|
|
2160
|
+
var PDF_LOAD_TIMEOUT_MS = 3e4;
|
|
2161
|
+
async function loadPdfWithTimeout(buffer) {
|
|
2162
|
+
const loadingTask = (0, import_pdf2.getDocument)({
|
|
1894
2163
|
data: new Uint8Array(buffer),
|
|
1895
2164
|
useSystemFonts: true,
|
|
1896
2165
|
disableFontFace: true,
|
|
1897
2166
|
isEvalSupported: false
|
|
1898
|
-
})
|
|
2167
|
+
});
|
|
2168
|
+
return Promise.race([
|
|
2169
|
+
loadingTask.promise,
|
|
2170
|
+
new Promise(
|
|
2171
|
+
(_, reject) => setTimeout(() => {
|
|
2172
|
+
loadingTask.destroy();
|
|
2173
|
+
reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
|
|
2174
|
+
}, PDF_LOAD_TIMEOUT_MS)
|
|
2175
|
+
)
|
|
2176
|
+
]);
|
|
2177
|
+
}
|
|
2178
|
+
async function parsePdfDocument(buffer, options) {
|
|
2179
|
+
const doc = await loadPdfWithTimeout(buffer);
|
|
1899
2180
|
try {
|
|
1900
2181
|
const pageCount = doc.numPages;
|
|
1901
2182
|
if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
|
|
@@ -1907,32 +2188,43 @@ async function parsePdfDocument(buffer, options) {
|
|
|
1907
2188
|
let totalTextBytes = 0;
|
|
1908
2189
|
const effectivePageCount = Math.min(pageCount, MAX_PAGES);
|
|
1909
2190
|
const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
|
|
2191
|
+
const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
|
|
1910
2192
|
const allFontSizes = [];
|
|
2193
|
+
const pageHeights = /* @__PURE__ */ new Map();
|
|
2194
|
+
let parsedPages = 0;
|
|
1911
2195
|
for (let i = 1; i <= effectivePageCount; i++) {
|
|
1912
2196
|
if (pageFilter && !pageFilter.has(i)) continue;
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
const
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2197
|
+
try {
|
|
2198
|
+
const page = await doc.getPage(i);
|
|
2199
|
+
const tc = await page.getTextContent();
|
|
2200
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
2201
|
+
pageHeights.set(i, viewport.height);
|
|
2202
|
+
const rawItems = tc.items;
|
|
2203
|
+
const items = normalizeItems(rawItems);
|
|
2204
|
+
const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
|
|
2205
|
+
if (hiddenCount > 0) {
|
|
2206
|
+
warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2207
|
+
}
|
|
2208
|
+
for (const item of visible) {
|
|
2209
|
+
if (item.fontSize > 0) allFontSizes.push(item.fontSize);
|
|
2210
|
+
}
|
|
2211
|
+
const opList = await page.getOperatorList();
|
|
2212
|
+
const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
|
|
2213
|
+
for (const b of pageBlocks) blocks.push(b);
|
|
2214
|
+
for (const b of pageBlocks) {
|
|
2215
|
+
const t = b.text || "";
|
|
2216
|
+
totalChars += t.replace(/\s/g, "").length;
|
|
2217
|
+
totalTextBytes += t.length * 2;
|
|
2218
|
+
}
|
|
2219
|
+
if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
|
|
2220
|
+
parsedPages++;
|
|
2221
|
+
options?.onProgress?.(parsedPages, totalTarget);
|
|
2222
|
+
} catch (pageErr) {
|
|
2223
|
+
if (pageErr instanceof KordocError) throw pageErr;
|
|
2224
|
+
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" });
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
|
|
1936
2228
|
if (totalChars / Math.max(parsedPageCount, 1) < 10) {
|
|
1937
2229
|
if (options?.ocr) {
|
|
1938
2230
|
try {
|
|
@@ -1947,6 +2239,12 @@ async function parsePdfDocument(buffer, options) {
|
|
|
1947
2239
|
}
|
|
1948
2240
|
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" };
|
|
1949
2241
|
}
|
|
2242
|
+
if (options?.removeHeaderFooter && parsedPageCount >= 3) {
|
|
2243
|
+
const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
|
|
2244
|
+
for (let ri = removed.length - 1; ri >= 0; ri--) {
|
|
2245
|
+
blocks.splice(removed[ri], 1);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
1950
2248
|
const medianFontSize = computeMedianFontSize(allFontSizes);
|
|
1951
2249
|
if (medianFontSize > 0) {
|
|
1952
2250
|
detectHeadings(blocks, medianFontSize);
|
|
@@ -2128,8 +2426,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
2128
2426
|
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
2129
2427
|
);
|
|
2130
2428
|
for (const cell of cells) {
|
|
2131
|
-
const
|
|
2132
|
-
const text = cellTextToString(
|
|
2429
|
+
const cellItems = cellTextMap.get(cell) || [];
|
|
2430
|
+
const text = cellTextToString(cellItems);
|
|
2133
2431
|
irGrid[cell.row][cell.col] = {
|
|
2134
2432
|
text,
|
|
2135
2433
|
colSpan: cell.colSpan,
|
|
@@ -2547,6 +2845,7 @@ function detectListBlocks(blocks) {
|
|
|
2547
2845
|
return result;
|
|
2548
2846
|
}
|
|
2549
2847
|
var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
|
|
2848
|
+
var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
|
|
2550
2849
|
function detectSpecialKoreanTables(blocks) {
|
|
2551
2850
|
const result = [];
|
|
2552
2851
|
let kvLines = [];
|
|
@@ -2612,16 +2911,18 @@ function detectSpecialKoreanTables(blocks) {
|
|
|
2612
2911
|
}
|
|
2613
2912
|
continue;
|
|
2614
2913
|
}
|
|
2615
|
-
if (kvLines.length > 0 && text.includes(":")
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2914
|
+
if (kvLines.length > 0 && text.includes(":")) {
|
|
2915
|
+
if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
|
|
2916
|
+
const colonIdx = text.indexOf(":");
|
|
2917
|
+
const key = text.slice(0, colonIdx).trim();
|
|
2918
|
+
if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
|
|
2919
|
+
kvLines.push({
|
|
2920
|
+
key,
|
|
2921
|
+
value: text.slice(colonIdx + 1).trim(),
|
|
2922
|
+
block
|
|
2923
|
+
});
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2625
2926
|
}
|
|
2626
2927
|
}
|
|
2627
2928
|
flushKvTable();
|
|
@@ -2630,6 +2931,62 @@ function detectSpecialKoreanTables(blocks) {
|
|
|
2630
2931
|
flushKvTable();
|
|
2631
2932
|
return result;
|
|
2632
2933
|
}
|
|
2934
|
+
function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
|
|
2935
|
+
const ZONE_RATIO = 0.1;
|
|
2936
|
+
const MIN_REPEAT = 3;
|
|
2937
|
+
const headerTexts = /* @__PURE__ */ new Map();
|
|
2938
|
+
const footerTexts = /* @__PURE__ */ new Map();
|
|
2939
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2940
|
+
const b = blocks[bi];
|
|
2941
|
+
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
2942
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2943
|
+
if (!ph) continue;
|
|
2944
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2945
|
+
const blockBottom = ph - b.bbox.y;
|
|
2946
|
+
if (blockBottom <= ph * ZONE_RATIO) {
|
|
2947
|
+
const arr = footerTexts.get(b.pageNumber) || [];
|
|
2948
|
+
arr.push(b.text.trim());
|
|
2949
|
+
footerTexts.set(b.pageNumber, arr);
|
|
2950
|
+
} else if (blockTop >= ph * (1 - ZONE_RATIO)) {
|
|
2951
|
+
const arr = headerTexts.get(b.pageNumber) || [];
|
|
2952
|
+
arr.push(b.text.trim());
|
|
2953
|
+
headerTexts.set(b.pageNumber, arr);
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
const repeatedPatterns = /* @__PURE__ */ new Set();
|
|
2957
|
+
for (const textsMap of [headerTexts, footerTexts]) {
|
|
2958
|
+
const patternCount = /* @__PURE__ */ new Map();
|
|
2959
|
+
for (const [, texts] of textsMap) {
|
|
2960
|
+
for (const t of texts) {
|
|
2961
|
+
const normalized = t.replace(/\d+/g, "#");
|
|
2962
|
+
patternCount.set(normalized, (patternCount.get(normalized) || 0) + 1);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
for (const [pattern, count] of patternCount) {
|
|
2966
|
+
if (count >= MIN_REPEAT) repeatedPatterns.add(pattern);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
if (repeatedPatterns.size === 0) return [];
|
|
2970
|
+
const removeIndices = [];
|
|
2971
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2972
|
+
const b = blocks[bi];
|
|
2973
|
+
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
2974
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2975
|
+
if (!ph) continue;
|
|
2976
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2977
|
+
const blockBottom = ph - b.bbox.y;
|
|
2978
|
+
const inZone = blockBottom <= ph * ZONE_RATIO || blockTop >= ph * (1 - ZONE_RATIO);
|
|
2979
|
+
if (!inZone) continue;
|
|
2980
|
+
const normalized = b.text.trim().replace(/\d+/g, "#");
|
|
2981
|
+
if (repeatedPatterns.has(normalized)) {
|
|
2982
|
+
removeIndices.push(bi);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
if (removeIndices.length > 0) {
|
|
2986
|
+
warnings.push({ message: `${removeIndices.length}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2987
|
+
}
|
|
2988
|
+
return removeIndices;
|
|
2989
|
+
}
|
|
2633
2990
|
function mergeKoreanLines(text) {
|
|
2634
2991
|
if (!text) return "";
|
|
2635
2992
|
const lines = text.split("\n");
|
|
@@ -2642,7 +2999,7 @@ function mergeKoreanLines(text) {
|
|
|
2642
2999
|
result.push(curr);
|
|
2643
3000
|
continue;
|
|
2644
3001
|
}
|
|
2645
|
-
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
|
|
3002
|
+
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
|
|
2646
3003
|
result[result.length - 1] = prev + " " + curr;
|
|
2647
3004
|
} else {
|
|
2648
3005
|
result.push(curr);
|
|
@@ -2665,7 +3022,9 @@ function normalizedSimilarity(a, b) {
|
|
|
2665
3022
|
function normalize(s) {
|
|
2666
3023
|
return s.replace(/\s+/g, " ").trim();
|
|
2667
3024
|
}
|
|
3025
|
+
var MAX_LEVENSHTEIN_LEN = 1e4;
|
|
2668
3026
|
function levenshtein(a, b) {
|
|
3027
|
+
if (a.length + b.length > MAX_LEVENSHTEIN_LEN) return Math.abs(a.length - b.length);
|
|
2669
3028
|
if (a.length > b.length) [a, b] = [b, a];
|
|
2670
3029
|
const m = a.length;
|
|
2671
3030
|
const n = b.length;
|
|
@@ -2978,7 +3337,7 @@ function parseMarkdownToBlocks(md) {
|
|
|
2978
3337
|
const tableRows = [];
|
|
2979
3338
|
while (i < lines.length && lines[i].trimStart().startsWith("|")) {
|
|
2980
3339
|
const row = lines[i];
|
|
2981
|
-
if (
|
|
3340
|
+
if (/^[\s|:\-]+$/.test(row)) {
|
|
2982
3341
|
i++;
|
|
2983
3342
|
continue;
|
|
2984
3343
|
}
|
|
@@ -3042,7 +3401,21 @@ function generateManifest() {
|
|
|
3042
3401
|
}
|
|
3043
3402
|
|
|
3044
3403
|
// src/index.ts
|
|
3045
|
-
async function parse(
|
|
3404
|
+
async function parse(input, options) {
|
|
3405
|
+
let buffer;
|
|
3406
|
+
if (typeof input === "string") {
|
|
3407
|
+
try {
|
|
3408
|
+
const buf = await (0, import_promises.readFile)(input);
|
|
3409
|
+
buffer = toArrayBuffer(buf);
|
|
3410
|
+
} catch (err) {
|
|
3411
|
+
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}`;
|
|
3412
|
+
return { success: false, fileType: "unknown", error: msg, code: "PARSE_ERROR" };
|
|
3413
|
+
}
|
|
3414
|
+
} else if (Buffer.isBuffer(input)) {
|
|
3415
|
+
buffer = toArrayBuffer(input);
|
|
3416
|
+
} else {
|
|
3417
|
+
buffer = input;
|
|
3418
|
+
}
|
|
3046
3419
|
if (!buffer || buffer.byteLength === 0) {
|
|
3047
3420
|
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" };
|
|
3048
3421
|
}
|
|
@@ -3060,16 +3433,16 @@ async function parse(buffer, options) {
|
|
|
3060
3433
|
}
|
|
3061
3434
|
async function parseHwpx(buffer, options) {
|
|
3062
3435
|
try {
|
|
3063
|
-
const { markdown, blocks, metadata, outline, warnings } = await parseHwpxDocument(buffer, options);
|
|
3064
|
-
return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings };
|
|
3436
|
+
const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
|
|
3437
|
+
return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
|
|
3065
3438
|
} catch (err) {
|
|
3066
3439
|
return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
|
|
3067
3440
|
}
|
|
3068
3441
|
}
|
|
3069
3442
|
async function parseHwp(buffer, options) {
|
|
3070
3443
|
try {
|
|
3071
|
-
const { markdown, blocks, metadata, outline, warnings } = parseHwp5Document(Buffer.from(buffer), options);
|
|
3072
|
-
return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings };
|
|
3444
|
+
const { markdown, blocks, metadata, outline, warnings, images } = parseHwp5Document(Buffer.from(buffer), options);
|
|
3445
|
+
return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
|
|
3073
3446
|
} catch (err) {
|
|
3074
3447
|
return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
|
|
3075
3448
|
}
|