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.
@@ -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("", `![image](${block.text})`, "");
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
- text = `[${text}](${block.href})`;
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.6.0" : "0.0.0-dev";
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
- async function extractHwpxStyles(zip) {
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
- const parser = new DOMParser();
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
- let totalDecompressed = 0;
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
- const xml = await file.async("text");
367
- totalDecompressed += xml.length * 2;
368
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
369
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
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 = new DOMParser();
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) break;
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 = new DOMParser();
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 = new DOMParser();
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 walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
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
- if (warnings && sectionNum) {
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
- if (warnings && sectionNum) {
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
- const sectionData = sections[si];
987
- const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
988
- totalDecompressed += data.length;
989
- if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
990
- const records = readRecords(data);
991
- const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
992
- blocks.push(...sectionBlocks);
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" || ctrlId === " elo" || ctrlId === "ole ") {
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
- if (rec.data.length >= 14) {
1241
- const cs = rec.data.readUInt16LE(10);
1242
- const rs = rec.data.readUInt16LE(12);
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
- let cellIdx = 0;
1262
- for (let r = 0; r < rows && cellIdx < cells.length; r++) {
1263
- for (let c = 0; c < cols && cellIdx < cells.length; c++) {
1264
- if (grid[r][c] !== null) continue;
1265
- const cell = cells[cellIdx++];
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
- async function parsePdfDocument(buffer, options) {
1837
- const doc = await getDocument({
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
- }).promise;
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
- const page = await doc.getPage(i);
1858
- const tc = await page.getTextContent();
1859
- const viewport = page.getViewport({ scale: 1 });
1860
- const rawItems = tc.items;
1861
- const items = normalizeItems(rawItems);
1862
- const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
1863
- if (hiddenCount > 0) {
1864
- warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
1865
- }
1866
- for (const item of visible) {
1867
- if (item.fontSize > 0) allFontSizes.push(item.fontSize);
1868
- }
1869
- const opList = await page.getOperatorList();
1870
- const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
1871
- for (const b of pageBlocks) blocks.push(b);
1872
- for (const b of pageBlocks) {
1873
- const t = b.text || "";
1874
- totalChars += t.replace(/\s/g, "").length;
1875
- totalTextBytes += t.length * 2;
1876
- }
1877
- if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
1878
- }
1879
- const parsedPageCount = pageFilter ? pageFilter.size : effectivePageCount;
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 getDocument({
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 textItems2 = cellTextMap.get(cell) || [];
2092
- const text = cellTextToString(textItems2);
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(":") && !text.includes("(") && !text.includes(")")) {
2576
- const colonIdx = text.indexOf(":");
2577
- const key = text.slice(0, colonIdx).trim();
2578
- if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
2579
- kvLines.push({
2580
- key,
2581
- value: text.slice(colonIdx + 1).trim(),
2582
- block
2583
- });
2584
- continue;
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(buffer, options) {
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-TFGOV2ML.js.map
3331
+ //# sourceMappingURL=chunk-YDXK5T53.js.map