kordoc 4.0.7 → 4.0.8

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.
Files changed (111) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +823 -806
  3. package/dist/{-6HWEFXVV.js → -LE4RLXVA.js} +9 -9
  4. package/dist/{chunk-BKWHX3RC.js → chunk-2DZQF6YJ.js} +3 -3
  5. package/dist/chunk-2DZQF6YJ.js.map +1 -0
  6. package/dist/{chunk-6GBLFQMA.js → chunk-5RPYBC2Q.js} +1 -1
  7. package/dist/chunk-5RPYBC2Q.js.map +1 -0
  8. package/dist/{chunk-U25XGCNH.cjs → chunk-6XAAYAWL.cjs} +142 -3
  9. package/dist/chunk-6XAAYAWL.cjs.map +1 -0
  10. package/dist/{chunk-TJJTLM76.js → chunk-7S3M4N4E.js} +666 -742
  11. package/dist/chunk-7S3M4N4E.js.map +1 -0
  12. package/dist/{chunk-Q7EN4EUJ.js → chunk-BNU5QGIZ.js} +2 -2
  13. package/dist/chunk-BNU5QGIZ.js.map +1 -0
  14. package/dist/{chunk-NGSHDBQR.js → chunk-D35VBACN.js} +3 -3
  15. package/dist/chunk-D35VBACN.js.map +1 -0
  16. package/dist/{chunk-X34YWRL5.cjs → chunk-DCZVOIEO.cjs} +1 -1
  17. package/dist/chunk-DCZVOIEO.cjs.map +1 -0
  18. package/dist/{chunk-AIQ3ISQU.js → chunk-GE43BE46.js} +1 -1
  19. package/dist/chunk-GE43BE46.js.map +1 -0
  20. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  21. package/dist/{chunk-CNM75ZSX.js → chunk-HMUEU7D7.js} +2 -2
  22. package/dist/chunk-HMUEU7D7.js.map +1 -0
  23. package/dist/{chunk-CFQLK3LM.js → chunk-MEPHGCPQ.js} +1 -1
  24. package/dist/chunk-MEPHGCPQ.js.map +1 -0
  25. package/dist/{chunk-3TBUDJDE.js → chunk-MOL7MDBG.js} +1 -1
  26. package/dist/chunk-MOL7MDBG.js.map +1 -0
  27. package/dist/{chunk-5OKHAJ62.js → chunk-OQPILS7B.js} +142 -3
  28. package/dist/chunk-OQPILS7B.js.map +1 -0
  29. package/dist/{chunk-ONXVBURQ.js → chunk-RROH5WHO.js} +2 -2
  30. package/dist/chunk-RROH5WHO.js.map +1 -0
  31. package/dist/{chunk-2SN3CKME.js → chunk-UEIYETCQ.js} +142 -3
  32. package/dist/chunk-UEIYETCQ.js.map +1 -0
  33. package/dist/cli.js +18 -18
  34. package/dist/cli.js.map +1 -1
  35. package/dist/{detect-NTR2FUEG.js → detect-RI2MQ33K.js} +2 -2
  36. package/dist/{formula-ZYUDQHD3.cjs → formula-5NKVS2LR.cjs} +1 -1
  37. package/dist/formula-5NKVS2LR.cjs.map +1 -0
  38. package/dist/{formula-KOQV353G.js → formula-JCNF43NE.js} +1 -1
  39. package/dist/formula-JCNF43NE.js.map +1 -0
  40. package/dist/{formula-MEDGYQXS.js → formula-RXVSQPXI.js} +1 -1
  41. package/dist/formula-RXVSQPXI.js.map +1 -0
  42. package/dist/index.cjs +1018 -1094
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +1 -1
  45. package/dist/index.d.ts +1 -1
  46. package/dist/index.js +780 -856
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp.js +14 -14
  49. package/dist/mcp.js.map +1 -1
  50. package/dist/page-range-737B4EZW.js +8 -0
  51. package/dist/page-range-LNMXJU6W.js +7 -0
  52. package/dist/page-range-P7SDW6LR.cjs +8 -0
  53. package/dist/page-range-P7SDW6LR.cjs.map +1 -0
  54. package/dist/{parser-E6U5TNAU.cjs → parser-ITMU2WN5.cjs} +214 -38
  55. package/dist/parser-ITMU2WN5.cjs.map +1 -0
  56. package/dist/{parser-UGB3NIVH.js → parser-KMMUS73Q.js} +183 -7
  57. package/dist/parser-KMMUS73Q.js.map +1 -0
  58. package/dist/{parser-5EHWYJMC.js → parser-TCTCSBYZ.js} +185 -9
  59. package/dist/parser-TCTCSBYZ.js.map +1 -0
  60. package/dist/{profile-io-CAKRPQRD.js → profile-io-SLN4P76T.js} +3 -3
  61. package/dist/{provider-H4WWSPOV.js → provider-4ZJKV3DC.js} +1 -1
  62. package/dist/provider-4ZJKV3DC.js.map +1 -0
  63. package/dist/{provider-7H4CPZYS.js → provider-AKROB7WQ.js} +1 -1
  64. package/dist/provider-AKROB7WQ.js.map +1 -0
  65. package/dist/{provider-5K7O3Z2O.cjs → provider-G4C2V2PD.cjs} +1 -1
  66. package/dist/provider-G4C2V2PD.cjs.map +1 -0
  67. package/dist/render-25BT623I.js +10 -0
  68. package/dist/seal-R4TETA4C.js +10 -0
  69. package/dist/{setup-QSJ2INNS.js → setup-57FB3LSP.js} +1 -1
  70. package/dist/setup-57FB3LSP.js.map +1 -0
  71. package/dist/{watch-RMX427YR.js → watch-OHQWSVPE.js} +9 -9
  72. package/dist/watch-OHQWSVPE.js.map +1 -0
  73. package/package.json +97 -97
  74. package/dist/chunk-2SN3CKME.js.map +0 -1
  75. package/dist/chunk-3TBUDJDE.js.map +0 -1
  76. package/dist/chunk-5OKHAJ62.js.map +0 -1
  77. package/dist/chunk-6GBLFQMA.js.map +0 -1
  78. package/dist/chunk-AIQ3ISQU.js.map +0 -1
  79. package/dist/chunk-BKWHX3RC.js.map +0 -1
  80. package/dist/chunk-CFQLK3LM.js.map +0 -1
  81. package/dist/chunk-CNM75ZSX.js.map +0 -1
  82. package/dist/chunk-NGSHDBQR.js.map +0 -1
  83. package/dist/chunk-ONXVBURQ.js.map +0 -1
  84. package/dist/chunk-Q7EN4EUJ.js.map +0 -1
  85. package/dist/chunk-TJJTLM76.js.map +0 -1
  86. package/dist/chunk-U25XGCNH.cjs.map +0 -1
  87. package/dist/chunk-X34YWRL5.cjs.map +0 -1
  88. package/dist/formula-KOQV353G.js.map +0 -1
  89. package/dist/formula-MEDGYQXS.js.map +0 -1
  90. package/dist/formula-ZYUDQHD3.cjs.map +0 -1
  91. package/dist/page-range-CIRRJPXJ.js +0 -7
  92. package/dist/page-range-OF5I4PQY.js +0 -8
  93. package/dist/page-range-TPIRSA3J.cjs +0 -8
  94. package/dist/page-range-TPIRSA3J.cjs.map +0 -1
  95. package/dist/parser-5EHWYJMC.js.map +0 -1
  96. package/dist/parser-E6U5TNAU.cjs.map +0 -1
  97. package/dist/parser-UGB3NIVH.js.map +0 -1
  98. package/dist/provider-5K7O3Z2O.cjs.map +0 -1
  99. package/dist/provider-7H4CPZYS.js.map +0 -1
  100. package/dist/provider-H4WWSPOV.js.map +0 -1
  101. package/dist/render-P6663O4I.js +0 -10
  102. package/dist/seal-7PPTXKKV.js +0 -10
  103. package/dist/setup-QSJ2INNS.js.map +0 -1
  104. package/dist/watch-RMX427YR.js.map +0 -1
  105. /package/dist/{-6HWEFXVV.js.map → -LE4RLXVA.js.map} +0 -0
  106. /package/dist/{detect-NTR2FUEG.js.map → detect-RI2MQ33K.js.map} +0 -0
  107. /package/dist/{page-range-CIRRJPXJ.js.map → page-range-737B4EZW.js.map} +0 -0
  108. /package/dist/{page-range-OF5I4PQY.js.map → page-range-LNMXJU6W.js.map} +0 -0
  109. /package/dist/{profile-io-CAKRPQRD.js.map → profile-io-SLN4P76T.js.map} +0 -0
  110. /package/dist/{render-P6663O4I.js.map → render-25BT623I.js.map} +0 -0
  111. /package/dist/{seal-7PPTXKKV.js.map → seal-R4TETA4C.js.map} +0 -0
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  convertTableToText,
14
14
  dedupeRunningHeaders,
15
15
  flattenLayoutTables,
16
+ inlineImagesIntoMarkdown,
16
17
  isPathTraversal,
17
18
  mapPuaText,
18
19
  normalizeSectionHref,
@@ -20,10 +21,10 @@ import {
20
21
  sanitizeHref,
21
22
  stripDtd,
22
23
  toArrayBuffer
23
- } from "./chunk-5OKHAJ62.js";
24
+ } from "./chunk-OQPILS7B.js";
24
25
  import {
25
26
  parsePageRange
26
- } from "./chunk-AIQ3ISQU.js";
27
+ } from "./chunk-GE43BE46.js";
27
28
 
28
29
  // src/index.ts
29
30
  import { readFile } from "fs/promises";
@@ -2711,628 +2712,807 @@ function gongmunDepthFromIndent(para2, left, ctx) {
2711
2712
  return depth >= 1 && depth < 8 ? depth : null;
2712
2713
  }
2713
2714
 
2714
- // src/hwpx/images.ts
2715
- function imageExtToMime(ext) {
2716
- switch (ext.toLowerCase()) {
2717
- case "jpg":
2718
- case "jpeg":
2719
- return "image/jpeg";
2720
- case "png":
2721
- return "image/png";
2722
- case "gif":
2723
- return "image/gif";
2724
- case "bmp":
2725
- return "image/bmp";
2726
- case "tif":
2727
- case "tiff":
2728
- return "image/tiff";
2729
- case "wmf":
2730
- return "image/wmf";
2731
- case "emf":
2732
- return "image/emf";
2733
- case "svg":
2734
- return "image/svg+xml";
2735
- default:
2736
- return "application/octet-stream";
2715
+ // src/hwp5/record.ts
2716
+ import { inflateRawSync, inflateSync } from "zlib";
2717
+ var TAG_PARA_HEADER = 66;
2718
+ var TAG_PARA_TEXT = 67;
2719
+ var TAG_CHAR_SHAPE = 68;
2720
+ var TAG_CTRL_HEADER = 71;
2721
+ var TAG_LIST_HEADER = 72;
2722
+ var TAG_TABLE = 77;
2723
+ var TAG_SHAPE_COMPONENT = 76;
2724
+ var TAG_SHAPE_COMPONENT_PICTURE = 85;
2725
+ var TAG_SHAPE_COMPONENT_CONTAINER = 86;
2726
+ var TAG_EQEDIT = 88;
2727
+ var TAG_BIN_DATA = 18;
2728
+ var TAG_DOC_CHAR_SHAPE = 21;
2729
+ var TAG_NUMBERING = 23;
2730
+ var TAG_BULLET = 24;
2731
+ var TAG_DOC_PARA_SHAPE = 25;
2732
+ var TAG_DOC_STYLE = 26;
2733
+ var CHAR_LINE = 0;
2734
+ var CHAR_SECTION_BREAK = 10;
2735
+ var CHAR_PARA = 13;
2736
+ var CHAR_TAB = 9;
2737
+ var CHAR_HYPHEN = 30;
2738
+ var CHAR_NBSP = 31;
2739
+ var CHAR_FIXED_NBSP = 24;
2740
+ var CHAR_FIXED_WIDTH = 25;
2741
+ var FLAG_COMPRESSED = 1 << 0;
2742
+ var FLAG_ENCRYPTED = 1 << 1;
2743
+ var FLAG_DISTRIBUTION = 1 << 2;
2744
+ var FLAG_DRM = 1 << 4;
2745
+ var MAX_RECORDS = 5e5;
2746
+ function readRecords(data) {
2747
+ const records = [];
2748
+ let offset = 0;
2749
+ while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
2750
+ const header = data.readUInt32LE(offset);
2751
+ offset += 4;
2752
+ const tagId = header & 1023;
2753
+ const level = header >> 10 & 1023;
2754
+ let size = header >> 20 & 4095;
2755
+ if (size === 4095) {
2756
+ if (offset + 4 > data.length) break;
2757
+ size = data.readUInt32LE(offset);
2758
+ offset += 4;
2759
+ }
2760
+ if (offset + size > data.length) break;
2761
+ records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
2762
+ offset += size;
2737
2763
  }
2764
+ return records;
2738
2765
  }
2739
- function mimeToExt(mime) {
2740
- if (mime.includes("jpeg")) return "jpg";
2741
- if (mime.includes("png")) return "png";
2742
- if (mime.includes("gif")) return "gif";
2743
- if (mime.includes("bmp")) return "bmp";
2744
- if (mime.includes("tiff")) return "tif";
2745
- if (mime.includes("wmf")) return "wmf";
2746
- if (mime.includes("emf")) return "emf";
2747
- if (mime.includes("svg")) return "svg";
2748
- return "bin";
2749
- }
2750
- function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
2751
- if (depth > MAX_XML_DEPTH) return;
2752
- for (const block of blocks) {
2753
- if (block.type === "image") {
2754
- out.push({ block, ownerCell });
2755
- } else if (block.type === "table" && block.table) {
2756
- for (const row of block.table.cells) {
2757
- for (const cell2 of row) {
2758
- if (cell2.blocks?.length) collectImageBlocks(cell2.blocks, out, cell2, depth + 1);
2759
- }
2760
- }
2766
+ var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
2767
+ function decompressStream(data) {
2768
+ const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
2769
+ if (data.length >= 2 && data[0] === 120) {
2770
+ try {
2771
+ return inflateSync(data, opts);
2772
+ } catch {
2761
2773
  }
2762
2774
  }
2775
+ return inflateRawSync(data, opts);
2763
2776
  }
2764
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
2765
- const images = [];
2766
- let imageIndex = 0;
2767
- const imageBlocks = [];
2768
- collectImageBlocks(blocks, imageBlocks);
2769
- const resolved = /* @__PURE__ */ new Map();
2770
- for (const { block, ownerCell } of imageBlocks) {
2771
- if (block.type !== "image" || !block.text) continue;
2772
- const ref = block.text;
2773
- let img = resolved.get(ref);
2774
- if (img === void 0) {
2775
- img = null;
2776
- const candidates = [
2777
- `BinData/${ref}`,
2778
- `Contents/BinData/${ref}`,
2779
- ref
2780
- // 절대 경로일 수도 있음
2781
- ];
2782
- let resolvedPath = null;
2783
- if (!ref.includes(".")) {
2784
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
2785
- for (const prefix of prefixes) {
2786
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
2787
- if (match.length > 0) {
2788
- resolvedPath = match[0].name;
2789
- break;
2790
- }
2777
+ function parseFileHeader(data) {
2778
+ if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
2779
+ const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
2780
+ return {
2781
+ signature: sig,
2782
+ versionMajor: data[35],
2783
+ flags: data.readUInt32LE(36)
2784
+ };
2785
+ }
2786
+ function readHwpString(data, offset) {
2787
+ if (offset + 2 > data.length) return { value: "", next: data.length };
2788
+ const len = data.readUInt16LE(offset);
2789
+ const start = offset + 2;
2790
+ const end = start + len * 2;
2791
+ if (len === 0 || end > data.length) return { value: "", next: start };
2792
+ return { value: data.subarray(start, end).toString("utf16le"), next: end };
2793
+ }
2794
+ function parseDocInfo(records) {
2795
+ const charShapes = [];
2796
+ const paraShapes = [];
2797
+ const styles = [];
2798
+ const binData = [];
2799
+ const numberings = [];
2800
+ const bullets = [];
2801
+ for (const rec of records) {
2802
+ if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
2803
+ const attr1 = rec.data.readUInt32LE(0);
2804
+ const headType = attr1 >>> 23 & 3;
2805
+ const paraLevel = attr1 >>> 25 & 7;
2806
+ const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
2807
+ paraShapes.push({ headType, paraLevel, numberingId });
2808
+ }
2809
+ if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
2810
+ const attr = rec.data.readUInt16LE(0);
2811
+ const typeBits = attr & 15;
2812
+ if (typeBits === 0) {
2813
+ binData.push({ kind: "link", storageId: 0, extension: "" });
2814
+ } else {
2815
+ const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
2816
+ const { value: extension } = readHwpString(rec.data, 4);
2817
+ binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
2818
+ }
2819
+ }
2820
+ if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
2821
+ const levelFormats = [];
2822
+ const numberFormats = [];
2823
+ const startNumbers = [1, 1, 1, 1, 1, 1, 1];
2824
+ let offset = 0;
2825
+ for (let level = 0; level < 7; level++) {
2826
+ if (offset + 12 > rec.data.length) {
2827
+ levelFormats.push("");
2828
+ numberFormats.push(0);
2829
+ continue;
2791
2830
  }
2831
+ const attr = rec.data.readUInt32LE(offset);
2832
+ numberFormats.push(attr >>> 5 & 15);
2833
+ offset += 12;
2834
+ const { value, next } = readHwpString(rec.data, offset);
2835
+ levelFormats.push(value);
2836
+ offset = next;
2792
2837
  }
2793
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
2794
- for (const path of allCandidates) {
2795
- if (isPathTraversal(path)) continue;
2796
- const file = zip.file(path);
2797
- if (!file) continue;
2798
- try {
2799
- const data = await file.async("uint8array");
2800
- decompressed.total += data.length;
2801
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2802
- const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
2803
- const mimeType = imageExtToMime(ext);
2804
- imageIndex++;
2805
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2806
- img = { filename, data, mimeType };
2807
- images.push(img);
2808
- break;
2809
- } catch (err) {
2810
- if (err instanceof KordocError) throw err;
2838
+ let baseStart = 1;
2839
+ if (offset + 2 <= rec.data.length) {
2840
+ baseStart = rec.data.readUInt16LE(offset) || 1;
2841
+ offset += 2;
2842
+ }
2843
+ for (let level = 0; level < 7; level++) {
2844
+ if (offset + 4 <= rec.data.length) {
2845
+ startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
2846
+ offset += 4;
2847
+ } else {
2848
+ startNumbers[level] = baseStart;
2811
2849
  }
2812
2850
  }
2813
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
2814
- resolved.set(ref, img);
2851
+ numberings.push({ levelFormats, numberFormats, startNumbers });
2815
2852
  }
2816
- if (!img) {
2817
- block.type = "paragraph";
2818
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
2819
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
2820
- continue;
2853
+ if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
2854
+ const code = rec.data.readUInt16LE(12);
2855
+ bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
2821
2856
  }
2822
- block.text = img.filename;
2823
- block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
2824
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
2825
- }
2826
- return images;
2827
- }
2828
-
2829
- // src/hwpx/metadata.ts
2830
- import JSZip2 from "jszip";
2831
-
2832
- // src/hwpx/zip-sections.ts
2833
- import { inflateRawSync } from "zlib";
2834
- function extractFromBrokenZip(buffer) {
2835
- const data = new Uint8Array(buffer);
2836
- const view = new DataView(buffer);
2837
- let pos = 0;
2838
- const blocks = [];
2839
- const warnings = [
2840
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
2841
- ];
2842
- let totalDecompressed = 0;
2843
- let entryCount = 0;
2844
- let sectionNum = 0;
2845
- const shared = createSectionShared();
2846
- while (pos < data.length - 30) {
2847
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
2848
- pos++;
2849
- while (pos < data.length - 30) {
2850
- if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
2851
- pos++;
2857
+ if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
2858
+ if (rec.data.length >= 50) {
2859
+ const fontSize = rec.data.readUInt32LE(42);
2860
+ const attrFlags = rec.data.readUInt32LE(46);
2861
+ charShapes.push({ fontSize, attrFlags });
2862
+ } else {
2863
+ charShapes.push({ fontSize: 0, attrFlags: 0 });
2852
2864
  }
2853
- continue;
2854
- }
2855
- if (++entryCount > MAX_ZIP_ENTRIES) break;
2856
- const method = view.getUint16(pos + 8, true);
2857
- const compSize = view.getUint32(pos + 18, true);
2858
- const nameLen = view.getUint16(pos + 26, true);
2859
- const extraLen = view.getUint16(pos + 28, true);
2860
- if (nameLen > 1024 || extraLen > 65535) {
2861
- pos += 30 + nameLen + extraLen;
2862
- continue;
2863
2865
  }
2864
- const fileStart = pos + 30 + nameLen + extraLen;
2865
- if (fileStart + compSize > data.length) break;
2866
- if (compSize === 0 && method !== 0) {
2867
- pos = fileStart;
2868
- continue;
2869
- }
2870
- const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
2871
- const name = new TextDecoder().decode(nameBytes);
2872
- if (isPathTraversal(name)) {
2873
- pos = fileStart + compSize;
2874
- continue;
2866
+ if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
2867
+ try {
2868
+ let offset = 0;
2869
+ const nameLen = rec.data.readUInt16LE(offset);
2870
+ offset += 2;
2871
+ const nameBytes = nameLen * 2;
2872
+ const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
2873
+ offset += nameBytes;
2874
+ let nameKo = "";
2875
+ if (offset + 2 <= rec.data.length) {
2876
+ const nameKoLen = rec.data.readUInt16LE(offset);
2877
+ offset += 2;
2878
+ const nameKoBytes = nameKoLen * 2;
2879
+ if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
2880
+ nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
2881
+ }
2882
+ offset += nameKoBytes;
2883
+ }
2884
+ const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
2885
+ offset += 1;
2886
+ offset += 1;
2887
+ offset += 2;
2888
+ const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2889
+ offset += 2;
2890
+ const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2891
+ styles.push({ name, nameKo, charShapeId, paraShapeId, type });
2892
+ } catch {
2893
+ }
2875
2894
  }
2876
- const fileData = data.slice(fileStart, fileStart + compSize);
2877
- pos = fileStart + compSize;
2878
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
2879
- try {
2880
- let content;
2881
- if (method === 0) {
2882
- content = new TextDecoder().decode(fileData);
2883
- } else if (method === 8) {
2884
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
2885
- content = new TextDecoder().decode(decompressed);
2886
- } else {
2887
- continue;
2895
+ }
2896
+ return { charShapes, paraShapes, styles, binData, numberings, bullets };
2897
+ }
2898
+ function createParaTextState() {
2899
+ return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2900
+ }
2901
+ function isExtendedOnlyCtrlChar(ch) {
2902
+ return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2903
+ }
2904
+ function appendParaText(state, data, resolveControl) {
2905
+ let result = "";
2906
+ let i = 0;
2907
+ const base = state.text.length;
2908
+ const resolveAt = (byteOffset, extended) => {
2909
+ const ctrlId = data.readUInt32LE(byteOffset);
2910
+ const idx = extended ? state.ctrlIdx : -1;
2911
+ const replacement = resolveControl?.(idx, ctrlId);
2912
+ if (replacement) result += replacement;
2913
+ if (extended) state.ctrlIdx++;
2914
+ };
2915
+ while (i + 1 < data.length) {
2916
+ const ch = data.readUInt16LE(i);
2917
+ i += 2;
2918
+ switch (ch) {
2919
+ // ── char 타입 (2바이트만, 확장 데이터 없음) ──
2920
+ case CHAR_LINE:
2921
+ result += "\n";
2922
+ break;
2923
+ case CHAR_SECTION_BREAK: {
2924
+ if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
2925
+ resolveAt(i + 2, true);
2926
+ i += 16;
2927
+ break;
2928
+ }
2929
+ result += "\n";
2930
+ break;
2888
2931
  }
2889
- totalDecompressed += content.length * 2;
2890
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
2891
- sectionNum++;
2892
- blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
2893
- } catch {
2894
- continue;
2932
+ case CHAR_PARA:
2933
+ break;
2934
+ // 문단 끝
2935
+ case CHAR_HYPHEN:
2936
+ result += "-";
2937
+ break;
2938
+ case CHAR_NBSP:
2939
+ result += " ";
2940
+ break;
2941
+ case CHAR_FIXED_NBSP:
2942
+ result += "\xA0";
2943
+ break;
2944
+ // 진짜 NBSP
2945
+ case CHAR_FIXED_WIDTH:
2946
+ result += " ";
2947
+ break;
2948
+ // 고정폭 공백
2949
+ // ── inline 타입 (2바이트 + 14바이트 확장) ──
2950
+ case CHAR_TAB:
2951
+ result += " ";
2952
+ if (i + 14 <= data.length) i += 14;
2953
+ break;
2954
+ default:
2955
+ if (ch >= 1 && ch <= 31) {
2956
+ const isExtended = isExtendedOnlyCtrlChar(ch);
2957
+ const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
2958
+ if ((isExtended || isInline) && i + 14 <= data.length) {
2959
+ if (ch === 3) {
2960
+ state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
2961
+ } else if (ch === 4) {
2962
+ const open = state.fieldStack.pop();
2963
+ if (open) {
2964
+ state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
2965
+ }
2966
+ }
2967
+ resolveAt(i, isExtended);
2968
+ i += 14;
2969
+ }
2970
+ } else if (ch >= 32) {
2971
+ if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
2972
+ const lo = data.readUInt16LE(i);
2973
+ if (lo >= 56320 && lo <= 57343) {
2974
+ i += 2;
2975
+ const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
2976
+ result += String.fromCodePoint(codePoint);
2977
+ break;
2978
+ }
2979
+ }
2980
+ result += String.fromCharCode(ch);
2981
+ }
2982
+ break;
2895
2983
  }
2896
2984
  }
2897
- if (blocks.length === 0) throw new KordocError("\uC190\uC0C1\uB41C HWPX\uC5D0\uC11C \uC139\uC158 \uB370\uC774\uD130\uB97C \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2898
- applyPageText(blocks, shared);
2899
- const markdown = blocksToMarkdown(blocks);
2900
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
2985
+ state.text += result;
2901
2986
  }
2902
- async function readKordocLayout(zip) {
2903
- const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2904
- if (!file) return null;
2905
- try {
2906
- const xml = await file.async("text");
2907
- if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2908
- return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2909
- } catch {
2910
- return null;
2911
- }
2987
+ function extractEquationText(data) {
2988
+ if (data.length < 6) return null;
2989
+ const scriptLength = data.readUInt16LE(4);
2990
+ const scriptStart = 6;
2991
+ const scriptEnd = scriptStart + scriptLength * 2;
2992
+ if (scriptLength <= 0 || scriptEnd > data.length) return null;
2993
+ const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
2994
+ return equation || null;
2912
2995
  }
2913
- async function resolveSectionPaths(zip) {
2914
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2915
- for (const mp of manifestPaths) {
2916
- const mpLower = mp.toLowerCase();
2917
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
2918
- if (!file) continue;
2919
- const xml = await file.async("text");
2920
- const paths = parseSectionPathsFromManifest(xml);
2921
- if (paths.length > 0) return paths;
2922
- }
2923
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2924
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2996
+
2997
+ // src/hwp5/images.ts
2998
+ function detectImageMime(data) {
2999
+ if (data.length < 4) return null;
3000
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
3001
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3002
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
3003
+ if (data[0] === 66 && data[1] === 77) return "image/bmp";
3004
+ if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
3005
+ if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
3006
+ return null;
2925
3007
  }
2926
- function parseSectionPathsFromManifest(xml) {
2927
- const parser = createXmlParser();
2928
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2929
- const items = doc.getElementsByTagName("opf:item");
2930
- const spine = doc.getElementsByTagName("opf:itemref");
2931
- const idToHref = /* @__PURE__ */ new Map();
2932
- for (let i = 0; i < items.length; i++) {
2933
- const item = items[i];
2934
- const id = item.getAttribute("id") || "";
2935
- const href = normalizeSectionHref(item.getAttribute("href") || "");
2936
- if (id && href) idToHref.set(id, href);
2937
- }
2938
- if (spine.length > 0) {
2939
- const ordered = [];
2940
- for (let i = 0; i < spine.length; i++) {
2941
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
2942
- if (href) ordered.push(href);
2943
- }
2944
- if (ordered.length > 0) return ordered;
3008
+ function normalizeBinPayload(data) {
3009
+ if (detectImageMime(data)) return data;
3010
+ try {
3011
+ const inflated = decompressStream(data);
3012
+ if (inflated.length > 0) return inflated;
3013
+ } catch {
2945
3014
  }
2946
- return Array.from(idToHref.values()).sort(compareSectionPaths);
3015
+ return data;
2947
3016
  }
2948
-
2949
- // src/hwpx/metadata.ts
2950
- async function extractHwpxMetadata(zip, metadata, decompressed) {
2951
- try {
2952
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
2953
- for (const mp of metaPaths) {
2954
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
2955
- if (!file) continue;
2956
- const xml = await file.async("text");
2957
- if (decompressed) {
2958
- decompressed.total += xml.length * 2;
2959
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3017
+ var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
3018
+ function collectImageBlocks(blocks, out) {
3019
+ for (const b of blocks) {
3020
+ if (b.type === "image") out.push(b);
3021
+ if (b.table) {
3022
+ for (const row of b.table.cells) {
3023
+ for (const cell2 of row) {
3024
+ if (cell2.blocks) collectImageBlocks(cell2.blocks, out);
3025
+ }
2960
3026
  }
2961
- parseDublinCoreMetadata(xml, metadata);
2962
- if (metadata.title || metadata.author) return;
2963
3027
  }
2964
- } catch {
3028
+ if (b.children) collectImageBlocks(b.children, out);
2965
3029
  }
2966
3030
  }
2967
- function parseDublinCoreMetadata(xml, metadata) {
2968
- const parser = createXmlParser();
2969
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2970
- if (!doc.documentElement) return;
2971
- const getText = (tagNames) => {
2972
- for (const tag of tagNames) {
2973
- const els = doc.getElementsByTagName(tag);
2974
- if (els.length > 0) {
2975
- const text = els[0].textContent?.trim();
2976
- if (text) return text;
3031
+ function forEachTableCell(blocks, fn) {
3032
+ for (const b of blocks) {
3033
+ if (b.table) {
3034
+ for (const row of b.table.cells) {
3035
+ for (const cell2 of row) {
3036
+ fn(cell2);
3037
+ if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
3038
+ }
2977
3039
  }
2978
3040
  }
2979
- return void 0;
2980
- };
2981
- metadata.title = metadata.title || getText(["dc:title", "title"]);
2982
- metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
2983
- metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
2984
- metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
2985
- metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
2986
- const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
2987
- if (keywords && !metadata.keywords) {
2988
- metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
3041
+ if (b.children) forEachTableCell(b.children, fn);
2989
3042
  }
2990
3043
  }
2991
-
2992
- // src/hwpx/parser.ts
2993
- async function parseHwpxDocument(buffer, options) {
2994
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
2995
- let zip;
2996
- try {
2997
- zip = await JSZip3.loadAsync(buffer);
2998
- } catch {
2999
- return extractFromBrokenZip(buffer);
3000
- }
3001
- const actualEntryCount = Object.keys(zip.files).length;
3002
- if (actualEntryCount > MAX_ZIP_ENTRIES) {
3003
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3004
- }
3005
- const manifestFile = zip.file("META-INF/manifest.xml");
3006
- if (manifestFile) {
3007
- const manifestXml = await manifestFile.async("text");
3008
- if (isEncryptedHwpx(manifestXml)) {
3009
- if (isComFallbackAvailable() && options?.filePath) {
3010
- const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
3011
- if (pages.some((p) => p && p.trim().length > 0)) {
3012
- return comResultToParseResult(pages, pageCount, warnings2);
3044
+ var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
3045
+ function resolveCellImageSentinels(blocks, renamed) {
3046
+ forEachTableCell(blocks, (cell2) => {
3047
+ if (!cell2.text.includes("hwp5bin:")) return;
3048
+ cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
3049
+ const filename = renamed.get(Number(idStr));
3050
+ return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
3051
+ });
3052
+ });
3053
+ }
3054
+ function resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced) {
3055
+ const imageBlocks = [];
3056
+ collectImageBlocks(blocks, imageBlocks);
3057
+ const images = [];
3058
+ const renamed = /* @__PURE__ */ new Map();
3059
+ const resolved = /* @__PURE__ */ new Map();
3060
+ let imageIndex = 0;
3061
+ for (const block of imageBlocks) {
3062
+ if (!block.text) continue;
3063
+ const storageId = parseInt(block.text, 10);
3064
+ if (isNaN(storageId)) continue;
3065
+ let img = resolved.get(storageId);
3066
+ if (img === void 0) {
3067
+ const bin = binDataMap.get(storageId);
3068
+ if (!bin) {
3069
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
3070
+ resolved.set(storageId, null);
3071
+ } else {
3072
+ const mime = detectImageMime(bin.data);
3073
+ if (!mime) {
3074
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
3075
+ resolved.set(storageId, null);
3076
+ } else {
3077
+ imageIndex++;
3078
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3079
+ img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
3080
+ resolved.set(storageId, img);
3081
+ images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
3082
+ renamed.set(storageId, img.filename);
3013
3083
  }
3014
3084
  }
3015
- throw new KordocError("DRM \uC554\uD638\uD654\uB41C HWPX \uD30C\uC77C\uC785\uB2C8\uB2E4. Windows + \uD55C\uCEF4 \uC624\uD53C\uC2A4 \uC124\uCE58 \uC2DC \uC790\uB3D9 \uCD94\uCD9C\uB429\uB2C8\uB2E4.");
3085
+ img = resolved.get(storageId);
3016
3086
  }
3017
- }
3018
- const decompressed = { total: 0 };
3019
- const metadata = {};
3020
- await extractHwpxMetadata(zip, metadata, decompressed);
3021
- const styleMap = await extractHwpxStyles(zip, decompressed);
3022
- const warnings = [];
3023
- const sectionPaths = await resolveSectionPaths(zip);
3024
- if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3025
- metadata.pageCount = sectionPaths.length;
3026
- const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
3027
- const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
3028
- const blocks = [];
3029
- const shared = createSectionShared();
3030
- shared.kordocLayout = await readKordocLayout(zip);
3031
- let parsedSections = 0;
3032
- for (let si = 0; si < sectionPaths.length; si++) {
3033
- if (pageFilter && !pageFilter.has(si + 1)) continue;
3034
- const file = zip.file(sectionPaths[si]);
3035
- if (!file) continue;
3036
- try {
3037
- const xml = await file.async("text");
3038
- decompressed.total += xml.length * 2;
3039
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3040
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
3041
- parsedSections++;
3042
- options?.onProgress?.(parsedSections, totalTarget);
3043
- } catch (secErr) {
3044
- if (secErr instanceof KordocError) throw secErr;
3045
- 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" });
3087
+ if (!img) {
3088
+ const bin = binDataMap.get(storageId);
3089
+ block.type = "paragraph";
3090
+ block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
3091
+ continue;
3046
3092
  }
3093
+ block.text = img.filename;
3094
+ block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3047
3095
  }
3048
- applyPageText(blocks, shared);
3049
- const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
3050
- detectHwpxHeadings(blocks, styleMap);
3051
- const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3052
- const markdown = blocksToMarkdown(blocks);
3053
- 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 };
3054
- }
3055
-
3056
- // src/hwp5/record.ts
3057
- import { inflateRawSync as inflateRawSync2, inflateSync } from "zlib";
3058
- var TAG_PARA_HEADER = 66;
3059
- var TAG_PARA_TEXT = 67;
3060
- var TAG_CHAR_SHAPE = 68;
3061
- var TAG_CTRL_HEADER = 71;
3062
- var TAG_LIST_HEADER = 72;
3063
- var TAG_TABLE = 77;
3064
- var TAG_SHAPE_COMPONENT = 76;
3065
- var TAG_SHAPE_COMPONENT_PICTURE = 85;
3066
- var TAG_SHAPE_COMPONENT_CONTAINER = 86;
3067
- var TAG_EQEDIT = 88;
3068
- var TAG_BIN_DATA = 18;
3069
- var TAG_DOC_CHAR_SHAPE = 21;
3070
- var TAG_NUMBERING = 23;
3071
- var TAG_BULLET = 24;
3072
- var TAG_DOC_PARA_SHAPE = 25;
3073
- var TAG_DOC_STYLE = 26;
3074
- var CHAR_LINE = 0;
3075
- var CHAR_SECTION_BREAK = 10;
3076
- var CHAR_PARA = 13;
3077
- var CHAR_TAB = 9;
3078
- var CHAR_HYPHEN = 30;
3079
- var CHAR_NBSP = 31;
3080
- var CHAR_FIXED_NBSP = 24;
3081
- var CHAR_FIXED_WIDTH = 25;
3082
- var FLAG_COMPRESSED = 1 << 0;
3083
- var FLAG_ENCRYPTED = 1 << 1;
3084
- var FLAG_DISTRIBUTION = 1 << 2;
3085
- var FLAG_DRM = 1 << 4;
3086
- var MAX_RECORDS = 5e5;
3087
- function readRecords(data) {
3088
- const records = [];
3089
- let offset = 0;
3090
- while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
3091
- const header = data.readUInt32LE(offset);
3092
- offset += 4;
3093
- const tagId = header & 1023;
3094
- const level = header >> 10 & 1023;
3095
- let size = header >> 20 & 4095;
3096
- if (size === 4095) {
3097
- if (offset + 4 > data.length) break;
3098
- size = data.readUInt32LE(offset);
3099
- offset += 4;
3096
+ if (sweepUnreferenced) {
3097
+ for (const [storageId, bin] of [...binDataMap.entries()].sort((a, b) => a[0] - b[0])) {
3098
+ if (resolved.has(storageId)) continue;
3099
+ const mime = detectImageMime(bin.data);
3100
+ if (!mime) continue;
3101
+ imageIndex++;
3102
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3103
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
3104
+ const data = new Uint8Array(bin.data);
3105
+ images.push({ filename, data, mimeType: mime });
3106
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType: mime, filename: bin.name } });
3100
3107
  }
3101
- if (offset + size > data.length) break;
3102
- records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
3103
- offset += size;
3104
3108
  }
3105
- return records;
3109
+ resolveCellImageSentinels(blocks, renamed);
3110
+ return images;
3106
3111
  }
3107
- var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
3108
- function decompressStream(data) {
3109
- const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
3110
- if (data.length >= 2 && data[0] === 120) {
3111
- try {
3112
- return inflateSync(data, opts);
3113
- } catch {
3112
+ function extractHwp5Images(fileIndex, blocks, warnings, sweepUnreferenced) {
3113
+ const binDataMap = /* @__PURE__ */ new Map();
3114
+ if (fileIndex) {
3115
+ for (const entry of fileIndex) {
3116
+ if (!entry?.name || !entry.content) continue;
3117
+ const match = entry.name.match(BIN_ENTRY_RE);
3118
+ if (!match) continue;
3119
+ const idx = parseInt(match[1], 16);
3120
+ const data = normalizeBinPayload(Buffer.from(entry.content));
3121
+ binDataMap.set(idx, { data, name: entry.name });
3114
3122
  }
3115
3123
  }
3116
- return inflateRawSync2(data, opts);
3124
+ if (binDataMap.size === 0) {
3125
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3126
+ return [];
3127
+ }
3128
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
3117
3129
  }
3118
- function parseFileHeader(data) {
3119
- if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
3120
- const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
3121
- return {
3122
- signature: sig,
3123
- versionMajor: data[35],
3124
- flags: data.readUInt32LE(36)
3125
- };
3130
+ function extractHwp5ImagesLenient(lcfb, blocks, warnings, sweepUnreferenced) {
3131
+ const binDataMap = /* @__PURE__ */ new Map();
3132
+ const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
3133
+ for (const e of lcfb.entries()) {
3134
+ const match = e.name.match(binRe);
3135
+ if (!match) continue;
3136
+ const idx = parseInt(match[1], 16);
3137
+ const raw = lcfb.findStream(e.name);
3138
+ if (!raw) continue;
3139
+ binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
3140
+ }
3141
+ if (binDataMap.size === 0) {
3142
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3143
+ return [];
3144
+ }
3145
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
3126
3146
  }
3127
- function readHwpString(data, offset) {
3128
- if (offset + 2 > data.length) return { value: "", next: data.length };
3129
- const len = data.readUInt16LE(offset);
3130
- const start = offset + 2;
3131
- const end = start + len * 2;
3132
- if (len === 0 || end > data.length) return { value: "", next: start };
3133
- return { value: data.subarray(start, end).toString("utf16le"), next: end };
3147
+
3148
+ // src/hwpx/images.ts
3149
+ function imageExtToMime(ext) {
3150
+ switch (ext.toLowerCase()) {
3151
+ case "jpg":
3152
+ case "jpeg":
3153
+ return "image/jpeg";
3154
+ case "png":
3155
+ return "image/png";
3156
+ case "gif":
3157
+ return "image/gif";
3158
+ case "bmp":
3159
+ return "image/bmp";
3160
+ case "tif":
3161
+ case "tiff":
3162
+ return "image/tiff";
3163
+ case "wmf":
3164
+ return "image/wmf";
3165
+ case "emf":
3166
+ return "image/emf";
3167
+ case "svg":
3168
+ return "image/svg+xml";
3169
+ default:
3170
+ return "application/octet-stream";
3171
+ }
3134
3172
  }
3135
- function parseDocInfo(records) {
3136
- const charShapes = [];
3137
- const paraShapes = [];
3138
- const styles = [];
3139
- const binData = [];
3140
- const numberings = [];
3141
- const bullets = [];
3142
- for (const rec of records) {
3143
- if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
3144
- const attr1 = rec.data.readUInt32LE(0);
3145
- const headType = attr1 >>> 23 & 3;
3146
- const paraLevel = attr1 >>> 25 & 7;
3147
- const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
3148
- paraShapes.push({ headType, paraLevel, numberingId });
3149
- }
3150
- if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
3151
- const attr = rec.data.readUInt16LE(0);
3152
- const typeBits = attr & 15;
3153
- if (typeBits === 0) {
3154
- binData.push({ kind: "link", storageId: 0, extension: "" });
3155
- } else {
3156
- const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
3157
- const { value: extension } = readHwpString(rec.data, 4);
3158
- binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
3173
+ function mimeToExt(mime) {
3174
+ if (mime.includes("jpeg")) return "jpg";
3175
+ if (mime.includes("png")) return "png";
3176
+ if (mime.includes("gif")) return "gif";
3177
+ if (mime.includes("bmp")) return "bmp";
3178
+ if (mime.includes("tiff")) return "tif";
3179
+ if (mime.includes("wmf")) return "wmf";
3180
+ if (mime.includes("emf")) return "emf";
3181
+ if (mime.includes("svg")) return "svg";
3182
+ return "bin";
3183
+ }
3184
+ function collectImageBlocks2(blocks, out, ownerCell, depth = 0) {
3185
+ if (depth > MAX_XML_DEPTH) return;
3186
+ for (const block of blocks) {
3187
+ if (block.type === "image") {
3188
+ out.push({ block, ownerCell });
3189
+ } else if (block.type === "table" && block.table) {
3190
+ for (const row of block.table.cells) {
3191
+ for (const cell2 of row) {
3192
+ if (cell2.blocks?.length) collectImageBlocks2(cell2.blocks, out, cell2, depth + 1);
3193
+ }
3159
3194
  }
3160
3195
  }
3161
- if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
3162
- const levelFormats = [];
3163
- const numberFormats = [];
3164
- const startNumbers = [1, 1, 1, 1, 1, 1, 1];
3165
- let offset = 0;
3166
- for (let level = 0; level < 7; level++) {
3167
- if (offset + 12 > rec.data.length) {
3168
- levelFormats.push("");
3169
- numberFormats.push(0);
3170
- continue;
3196
+ }
3197
+ }
3198
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings, sweepUnreferenced) {
3199
+ const images = [];
3200
+ let imageIndex = 0;
3201
+ const usedPaths = /* @__PURE__ */ new Set();
3202
+ const imageBlocks = [];
3203
+ collectImageBlocks2(blocks, imageBlocks);
3204
+ const resolved = /* @__PURE__ */ new Map();
3205
+ for (const { block, ownerCell } of imageBlocks) {
3206
+ if (block.type !== "image" || !block.text) continue;
3207
+ const ref = block.text;
3208
+ let img = resolved.get(ref);
3209
+ if (img === void 0) {
3210
+ img = null;
3211
+ const candidates = [
3212
+ `BinData/${ref}`,
3213
+ `Contents/BinData/${ref}`,
3214
+ ref
3215
+ // 절대 경로일 수도 있음
3216
+ ];
3217
+ let resolvedPath = null;
3218
+ if (!ref.includes(".")) {
3219
+ const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
3220
+ for (const prefix of prefixes) {
3221
+ const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
3222
+ if (match.length > 0) {
3223
+ resolvedPath = match[0].name;
3224
+ break;
3225
+ }
3171
3226
  }
3172
- const attr = rec.data.readUInt32LE(offset);
3173
- numberFormats.push(attr >>> 5 & 15);
3174
- offset += 12;
3175
- const { value, next } = readHwpString(rec.data, offset);
3176
- levelFormats.push(value);
3177
- offset = next;
3178
3227
  }
3179
- let baseStart = 1;
3180
- if (offset + 2 <= rec.data.length) {
3181
- baseStart = rec.data.readUInt16LE(offset) || 1;
3182
- offset += 2;
3228
+ const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
3229
+ for (const path of allCandidates) {
3230
+ if (isPathTraversal(path)) continue;
3231
+ const file = zip.file(path);
3232
+ if (!file) continue;
3233
+ try {
3234
+ const data = await file.async("uint8array");
3235
+ decompressed.total += data.length;
3236
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3237
+ const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
3238
+ const mimeType = imageExtToMime(ext);
3239
+ imageIndex++;
3240
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
3241
+ img = { filename, data, mimeType };
3242
+ images.push(img);
3243
+ usedPaths.add(path);
3244
+ break;
3245
+ } catch (err) {
3246
+ if (err instanceof KordocError) throw err;
3247
+ }
3183
3248
  }
3184
- for (let level = 0; level < 7; level++) {
3185
- if (offset + 4 <= rec.data.length) {
3186
- startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
3187
- offset += 4;
3188
- } else {
3189
- startNumbers[level] = baseStart;
3249
+ if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
3250
+ resolved.set(ref, img);
3251
+ }
3252
+ if (!img) {
3253
+ block.type = "paragraph";
3254
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
3255
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
3256
+ continue;
3257
+ }
3258
+ block.text = img.filename;
3259
+ block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
3260
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
3261
+ }
3262
+ if (sweepUnreferenced) {
3263
+ const binEntries = zip.file(/(?:^|\/)BinData\//i);
3264
+ for (const file of binEntries) {
3265
+ if (file.dir || usedPaths.has(file.name) || isPathTraversal(file.name)) continue;
3266
+ try {
3267
+ const data = await file.async("uint8array");
3268
+ decompressed.total += data.length;
3269
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3270
+ const ext = file.name.includes(".") ? file.name.split(".").pop() || "" : "";
3271
+ let mimeType = imageExtToMime(ext);
3272
+ if (mimeType === "application/octet-stream") {
3273
+ const sniffed = detectImageMime(data);
3274
+ if (!sniffed) continue;
3275
+ mimeType = sniffed;
3190
3276
  }
3277
+ imageIndex++;
3278
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
3279
+ const img = { filename, data, mimeType };
3280
+ images.push(img);
3281
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType, filename: file.name } });
3282
+ } catch (err) {
3283
+ if (err instanceof KordocError) throw err;
3284
+ warnings?.push({ message: `\uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328: ${file.name}`, code: "SKIPPED_IMAGE" });
3191
3285
  }
3192
- numberings.push({ levelFormats, numberFormats, startNumbers });
3193
- }
3194
- if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
3195
- const code = rec.data.readUInt16LE(12);
3196
- bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
3197
3286
  }
3198
- if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
3199
- if (rec.data.length >= 50) {
3200
- const fontSize = rec.data.readUInt32LE(42);
3201
- const attrFlags = rec.data.readUInt32LE(46);
3202
- charShapes.push({ fontSize, attrFlags });
3203
- } else {
3204
- charShapes.push({ fontSize: 0, attrFlags: 0 });
3287
+ }
3288
+ return images;
3289
+ }
3290
+
3291
+ // src/hwpx/metadata.ts
3292
+ import JSZip2 from "jszip";
3293
+
3294
+ // src/hwpx/zip-sections.ts
3295
+ import { inflateRawSync as inflateRawSync2 } from "zlib";
3296
+ function extractFromBrokenZip(buffer) {
3297
+ const data = new Uint8Array(buffer);
3298
+ const view = new DataView(buffer);
3299
+ let pos = 0;
3300
+ const blocks = [];
3301
+ const warnings = [
3302
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
3303
+ ];
3304
+ let totalDecompressed = 0;
3305
+ let entryCount = 0;
3306
+ let sectionNum = 0;
3307
+ const shared = createSectionShared();
3308
+ while (pos < data.length - 30) {
3309
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
3310
+ pos++;
3311
+ while (pos < data.length - 30) {
3312
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
3313
+ pos++;
3205
3314
  }
3315
+ continue;
3206
3316
  }
3207
- if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
3208
- try {
3209
- let offset = 0;
3210
- const nameLen = rec.data.readUInt16LE(offset);
3211
- offset += 2;
3212
- const nameBytes = nameLen * 2;
3213
- const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
3214
- offset += nameBytes;
3215
- let nameKo = "";
3216
- if (offset + 2 <= rec.data.length) {
3217
- const nameKoLen = rec.data.readUInt16LE(offset);
3218
- offset += 2;
3219
- const nameKoBytes = nameKoLen * 2;
3220
- if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
3221
- nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
3222
- }
3223
- offset += nameKoBytes;
3224
- }
3225
- const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
3226
- offset += 1;
3227
- offset += 1;
3228
- offset += 2;
3229
- const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
3230
- offset += 2;
3231
- const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
3232
- styles.push({ name, nameKo, charShapeId, paraShapeId, type });
3233
- } catch {
3317
+ if (++entryCount > MAX_ZIP_ENTRIES) break;
3318
+ const method = view.getUint16(pos + 8, true);
3319
+ const compSize = view.getUint32(pos + 18, true);
3320
+ const nameLen = view.getUint16(pos + 26, true);
3321
+ const extraLen = view.getUint16(pos + 28, true);
3322
+ if (nameLen > 1024 || extraLen > 65535) {
3323
+ pos += 30 + nameLen + extraLen;
3324
+ continue;
3325
+ }
3326
+ const fileStart = pos + 30 + nameLen + extraLen;
3327
+ if (fileStart + compSize > data.length) break;
3328
+ if (compSize === 0 && method !== 0) {
3329
+ pos = fileStart;
3330
+ continue;
3331
+ }
3332
+ const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
3333
+ const name = new TextDecoder().decode(nameBytes);
3334
+ if (isPathTraversal(name)) {
3335
+ pos = fileStart + compSize;
3336
+ continue;
3337
+ }
3338
+ const fileData = data.slice(fileStart, fileStart + compSize);
3339
+ pos = fileStart + compSize;
3340
+ if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
3341
+ try {
3342
+ let content;
3343
+ if (method === 0) {
3344
+ content = new TextDecoder().decode(fileData);
3345
+ } else if (method === 8) {
3346
+ const decompressed = inflateRawSync2(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
3347
+ content = new TextDecoder().decode(decompressed);
3348
+ } else {
3349
+ continue;
3234
3350
  }
3351
+ totalDecompressed += content.length * 2;
3352
+ if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
3353
+ sectionNum++;
3354
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
3355
+ } catch {
3356
+ continue;
3235
3357
  }
3236
3358
  }
3237
- return { charShapes, paraShapes, styles, binData, numberings, bullets };
3359
+ if (blocks.length === 0) throw new KordocError("\uC190\uC0C1\uB41C HWPX\uC5D0\uC11C \uC139\uC158 \uB370\uC774\uD130\uB97C \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3360
+ applyPageText(blocks, shared);
3361
+ const markdown = blocksToMarkdown(blocks);
3362
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
3238
3363
  }
3239
- function createParaTextState() {
3240
- return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
3364
+ async function readKordocLayout(zip) {
3365
+ const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
3366
+ if (!file) return null;
3367
+ try {
3368
+ const xml = await file.async("text");
3369
+ if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
3370
+ return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
3371
+ } catch {
3372
+ return null;
3373
+ }
3241
3374
  }
3242
- function isExtendedOnlyCtrlChar(ch) {
3243
- return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
3375
+ async function resolveSectionPaths(zip) {
3376
+ const manifestPaths = ["Contents/content.hpf", "content.hpf"];
3377
+ for (const mp of manifestPaths) {
3378
+ const mpLower = mp.toLowerCase();
3379
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
3380
+ if (!file) continue;
3381
+ const xml = await file.async("text");
3382
+ const paths = parseSectionPathsFromManifest(xml);
3383
+ if (paths.length > 0) return paths;
3384
+ }
3385
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
3386
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
3244
3387
  }
3245
- function appendParaText(state, data, resolveControl) {
3246
- let result = "";
3247
- let i = 0;
3248
- const base = state.text.length;
3249
- const resolveAt = (byteOffset, extended) => {
3250
- const ctrlId = data.readUInt32LE(byteOffset);
3251
- const idx = extended ? state.ctrlIdx : -1;
3252
- const replacement = resolveControl?.(idx, ctrlId);
3253
- if (replacement) result += replacement;
3254
- if (extended) state.ctrlIdx++;
3255
- };
3256
- while (i + 1 < data.length) {
3257
- const ch = data.readUInt16LE(i);
3258
- i += 2;
3259
- switch (ch) {
3260
- // ── char 타입 (2바이트만, 확장 데이터 없음) ──
3261
- case CHAR_LINE:
3262
- result += "\n";
3263
- break;
3264
- case CHAR_SECTION_BREAK: {
3265
- if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
3266
- resolveAt(i + 2, true);
3267
- i += 16;
3268
- break;
3269
- }
3270
- result += "\n";
3271
- break;
3388
+ function parseSectionPathsFromManifest(xml) {
3389
+ const parser = createXmlParser();
3390
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
3391
+ const items = doc.getElementsByTagName("opf:item");
3392
+ const spine = doc.getElementsByTagName("opf:itemref");
3393
+ const idToHref = /* @__PURE__ */ new Map();
3394
+ for (let i = 0; i < items.length; i++) {
3395
+ const item = items[i];
3396
+ const id = item.getAttribute("id") || "";
3397
+ const href = normalizeSectionHref(item.getAttribute("href") || "");
3398
+ if (id && href) idToHref.set(id, href);
3399
+ }
3400
+ if (spine.length > 0) {
3401
+ const ordered = [];
3402
+ for (let i = 0; i < spine.length; i++) {
3403
+ const href = idToHref.get(spine[i].getAttribute("idref") || "");
3404
+ if (href) ordered.push(href);
3405
+ }
3406
+ if (ordered.length > 0) return ordered;
3407
+ }
3408
+ return Array.from(idToHref.values()).sort(compareSectionPaths);
3409
+ }
3410
+
3411
+ // src/hwpx/metadata.ts
3412
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
3413
+ try {
3414
+ const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
3415
+ for (const mp of metaPaths) {
3416
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
3417
+ if (!file) continue;
3418
+ const xml = await file.async("text");
3419
+ if (decompressed) {
3420
+ decompressed.total += xml.length * 2;
3421
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3272
3422
  }
3273
- case CHAR_PARA:
3274
- break;
3275
- // 문단 끝
3276
- case CHAR_HYPHEN:
3277
- result += "-";
3278
- break;
3279
- case CHAR_NBSP:
3280
- result += " ";
3281
- break;
3282
- case CHAR_FIXED_NBSP:
3283
- result += "\xA0";
3284
- break;
3285
- // 진짜 NBSP
3286
- case CHAR_FIXED_WIDTH:
3287
- result += " ";
3288
- break;
3289
- // 고정폭 공백
3290
- // ── inline 타입 (2바이트 + 14바이트 확장) ──
3291
- case CHAR_TAB:
3292
- result += " ";
3293
- if (i + 14 <= data.length) i += 14;
3294
- break;
3295
- default:
3296
- if (ch >= 1 && ch <= 31) {
3297
- const isExtended = isExtendedOnlyCtrlChar(ch);
3298
- const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
3299
- if ((isExtended || isInline) && i + 14 <= data.length) {
3300
- if (ch === 3) {
3301
- state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
3302
- } else if (ch === 4) {
3303
- const open = state.fieldStack.pop();
3304
- if (open) {
3305
- state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
3306
- }
3307
- }
3308
- resolveAt(i, isExtended);
3309
- i += 14;
3310
- }
3311
- } else if (ch >= 32) {
3312
- if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
3313
- const lo = data.readUInt16LE(i);
3314
- if (lo >= 56320 && lo <= 57343) {
3315
- i += 2;
3316
- const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
3317
- result += String.fromCodePoint(codePoint);
3318
- break;
3319
- }
3320
- }
3321
- result += String.fromCharCode(ch);
3322
- }
3323
- break;
3423
+ parseDublinCoreMetadata(xml, metadata);
3424
+ if (metadata.title || metadata.author) return;
3324
3425
  }
3426
+ } catch {
3325
3427
  }
3326
- state.text += result;
3327
3428
  }
3328
- function extractEquationText(data) {
3329
- if (data.length < 6) return null;
3330
- const scriptLength = data.readUInt16LE(4);
3331
- const scriptStart = 6;
3332
- const scriptEnd = scriptStart + scriptLength * 2;
3333
- if (scriptLength <= 0 || scriptEnd > data.length) return null;
3334
- const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
3335
- return equation || null;
3429
+ function parseDublinCoreMetadata(xml, metadata) {
3430
+ const parser = createXmlParser();
3431
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
3432
+ if (!doc.documentElement) return;
3433
+ const getText = (tagNames) => {
3434
+ for (const tag of tagNames) {
3435
+ const els = doc.getElementsByTagName(tag);
3436
+ if (els.length > 0) {
3437
+ const text = els[0].textContent?.trim();
3438
+ if (text) return text;
3439
+ }
3440
+ }
3441
+ return void 0;
3442
+ };
3443
+ metadata.title = metadata.title || getText(["dc:title", "title"]);
3444
+ metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
3445
+ metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
3446
+ metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
3447
+ metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
3448
+ const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
3449
+ if (keywords && !metadata.keywords) {
3450
+ metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
3451
+ }
3452
+ }
3453
+
3454
+ // src/hwpx/parser.ts
3455
+ async function parseHwpxDocument(buffer, options) {
3456
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
3457
+ let zip;
3458
+ try {
3459
+ zip = await JSZip3.loadAsync(buffer);
3460
+ } catch {
3461
+ return extractFromBrokenZip(buffer);
3462
+ }
3463
+ const actualEntryCount = Object.keys(zip.files).length;
3464
+ if (actualEntryCount > MAX_ZIP_ENTRIES) {
3465
+ throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3466
+ }
3467
+ const manifestFile = zip.file("META-INF/manifest.xml");
3468
+ if (manifestFile) {
3469
+ const manifestXml = await manifestFile.async("text");
3470
+ if (isEncryptedHwpx(manifestXml)) {
3471
+ if (isComFallbackAvailable() && options?.filePath) {
3472
+ const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
3473
+ if (pages.some((p) => p && p.trim().length > 0)) {
3474
+ return comResultToParseResult(pages, pageCount, warnings2);
3475
+ }
3476
+ }
3477
+ throw new KordocError("DRM \uC554\uD638\uD654\uB41C HWPX \uD30C\uC77C\uC785\uB2C8\uB2E4. Windows + \uD55C\uCEF4 \uC624\uD53C\uC2A4 \uC124\uCE58 \uC2DC \uC790\uB3D9 \uCD94\uCD9C\uB429\uB2C8\uB2E4.");
3478
+ }
3479
+ }
3480
+ const decompressed = { total: 0 };
3481
+ const metadata = {};
3482
+ await extractHwpxMetadata(zip, metadata, decompressed);
3483
+ const styleMap = await extractHwpxStyles(zip, decompressed);
3484
+ const warnings = [];
3485
+ const sectionPaths = await resolveSectionPaths(zip);
3486
+ if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3487
+ metadata.pageCount = sectionPaths.length;
3488
+ const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
3489
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
3490
+ const blocks = [];
3491
+ const shared = createSectionShared();
3492
+ shared.kordocLayout = await readKordocLayout(zip);
3493
+ let parsedSections = 0;
3494
+ for (let si = 0; si < sectionPaths.length; si++) {
3495
+ if (pageFilter && !pageFilter.has(si + 1)) continue;
3496
+ const file = zip.file(sectionPaths[si]);
3497
+ if (!file) continue;
3498
+ try {
3499
+ const xml = await file.async("text");
3500
+ decompressed.total += xml.length * 2;
3501
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3502
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
3503
+ parsedSections++;
3504
+ options?.onProgress?.(parsedSections, totalTarget);
3505
+ } catch (secErr) {
3506
+ if (secErr instanceof KordocError) throw secErr;
3507
+ 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" });
3508
+ }
3509
+ }
3510
+ applyPageText(blocks, shared);
3511
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings, !pageFilter);
3512
+ detectHwpxHeadings(blocks, styleMap);
3513
+ const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3514
+ const markdown = blocksToMarkdown(blocks);
3515
+ 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 };
3336
3516
  }
3337
3517
 
3338
3518
  // src/hwp5/numbering.ts
@@ -3513,282 +3693,6 @@ function expandNumberingFormat(formatStr, counters, numbering) {
3513
3693
  return result;
3514
3694
  }
3515
3695
 
3516
- // src/hwp5/images.ts
3517
- function detectImageMime(data) {
3518
- if (data.length < 4) return null;
3519
- if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
3520
- if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3521
- if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
3522
- if (data[0] === 66 && data[1] === 77) return "image/bmp";
3523
- if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
3524
- if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
3525
- return null;
3526
- }
3527
- function normalizeBinPayload(data) {
3528
- if (detectImageMime(data)) return data;
3529
- try {
3530
- const inflated = decompressStream(data);
3531
- if (inflated.length > 0) return inflated;
3532
- } catch {
3533
- }
3534
- return data;
3535
- }
3536
- var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
3537
- function collectImageBlocks2(blocks, out) {
3538
- for (const b of blocks) {
3539
- if (b.type === "image") out.push(b);
3540
- if (b.table) {
3541
- for (const row of b.table.cells) {
3542
- for (const cell2 of row) {
3543
- if (cell2.blocks) collectImageBlocks2(cell2.blocks, out);
3544
- }
3545
- }
3546
- }
3547
- if (b.children) collectImageBlocks2(b.children, out);
3548
- }
3549
- }
3550
- function forEachTableCell(blocks, fn) {
3551
- for (const b of blocks) {
3552
- if (b.table) {
3553
- for (const row of b.table.cells) {
3554
- for (const cell2 of row) {
3555
- fn(cell2);
3556
- if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
3557
- }
3558
- }
3559
- }
3560
- if (b.children) forEachTableCell(b.children, fn);
3561
- }
3562
- }
3563
- var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
3564
- function resolveCellImageSentinels(blocks, renamed) {
3565
- forEachTableCell(blocks, (cell2) => {
3566
- if (!cell2.text.includes("hwp5bin:")) return;
3567
- cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
3568
- const filename = renamed.get(Number(idStr));
3569
- return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
3570
- });
3571
- });
3572
- }
3573
- function resolveImageBlocks(binDataMap, blocks, warnings) {
3574
- const imageBlocks = [];
3575
- collectImageBlocks2(blocks, imageBlocks);
3576
- if (imageBlocks.length === 0) return [];
3577
- const images = [];
3578
- const renamed = /* @__PURE__ */ new Map();
3579
- const resolved = /* @__PURE__ */ new Map();
3580
- let imageIndex = 0;
3581
- for (const block of imageBlocks) {
3582
- if (!block.text) continue;
3583
- const storageId = parseInt(block.text, 10);
3584
- if (isNaN(storageId)) continue;
3585
- let img = resolved.get(storageId);
3586
- if (img === void 0) {
3587
- const bin = binDataMap.get(storageId);
3588
- if (!bin) {
3589
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
3590
- resolved.set(storageId, null);
3591
- } else {
3592
- const mime = detectImageMime(bin.data);
3593
- if (!mime) {
3594
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
3595
- resolved.set(storageId, null);
3596
- } else {
3597
- imageIndex++;
3598
- const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3599
- img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
3600
- resolved.set(storageId, img);
3601
- images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
3602
- renamed.set(storageId, img.filename);
3603
- }
3604
- }
3605
- img = resolved.get(storageId);
3606
- }
3607
- if (!img) {
3608
- const bin = binDataMap.get(storageId);
3609
- block.type = "paragraph";
3610
- block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
3611
- continue;
3612
- }
3613
- block.text = img.filename;
3614
- block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3615
- }
3616
- resolveCellImageSentinels(blocks, renamed);
3617
- return images;
3618
- }
3619
- function extractHwp5Images(fileIndex, blocks, warnings) {
3620
- const binDataMap = /* @__PURE__ */ new Map();
3621
- if (fileIndex) {
3622
- for (const entry of fileIndex) {
3623
- if (!entry?.name || !entry.content) continue;
3624
- const match = entry.name.match(BIN_ENTRY_RE);
3625
- if (!match) continue;
3626
- const idx = parseInt(match[1], 16);
3627
- const data = normalizeBinPayload(Buffer.from(entry.content));
3628
- binDataMap.set(idx, { data, name: entry.name });
3629
- }
3630
- }
3631
- if (binDataMap.size === 0) {
3632
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3633
- return [];
3634
- }
3635
- return resolveImageBlocks(binDataMap, blocks, warnings);
3636
- }
3637
- function extractHwp5ImagesLenient(lcfb, blocks, warnings) {
3638
- const binDataMap = /* @__PURE__ */ new Map();
3639
- const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
3640
- for (const e of lcfb.entries()) {
3641
- const match = e.name.match(binRe);
3642
- if (!match) continue;
3643
- const idx = parseInt(match[1], 16);
3644
- const raw = lcfb.findStream(e.name);
3645
- if (!raw) continue;
3646
- binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
3647
- }
3648
- if (binDataMap.size === 0) {
3649
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3650
- return [];
3651
- }
3652
- return resolveImageBlocks(binDataMap, blocks, warnings);
3653
- }
3654
-
3655
- // src/image/transcode.ts
3656
- import { deflateSync } from "zlib";
3657
- var CRC_TABLE = (() => {
3658
- const table2 = new Uint32Array(256);
3659
- for (let n = 0; n < 256; n++) {
3660
- let c = n;
3661
- for (let k = 0; k < 8; k++) {
3662
- c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3663
- }
3664
- table2[n] = c >>> 0;
3665
- }
3666
- return table2;
3667
- })();
3668
- function crc32(bytes) {
3669
- let c = 4294967295;
3670
- for (let i = 0; i < bytes.length; i++) {
3671
- c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3672
- }
3673
- return (c ^ 4294967295) >>> 0;
3674
- }
3675
- var BI_RGB = 0;
3676
- var MAX_DIM = 32767;
3677
- var MAX_PIXELS = 36e6;
3678
- function bmpToPng(bmp) {
3679
- if (bmp.length < 54) return null;
3680
- if (bmp[0] !== 66 || bmp[1] !== 77) return null;
3681
- const dv = new DataView(bmp.buffer, bmp.byteOffset, bmp.byteLength);
3682
- const dataOffset = dv.getUint32(10, true);
3683
- const headerSize = dv.getUint32(14, true);
3684
- if (headerSize < 40) return null;
3685
- const width = dv.getInt32(18, true);
3686
- const rawHeight = dv.getInt32(22, true);
3687
- const bitCount = dv.getUint16(28, true);
3688
- const compression = dv.getUint32(30, true);
3689
- if (compression !== BI_RGB) return null;
3690
- if (bitCount !== 24 && bitCount !== 32) return null;
3691
- if (width <= 0 || rawHeight === 0) return null;
3692
- if (width > MAX_DIM || Math.abs(rawHeight) > MAX_DIM) return null;
3693
- const topDown = rawHeight < 0;
3694
- const height = Math.abs(rawHeight);
3695
- if (width * height > MAX_PIXELS) return null;
3696
- const bytesPerPixel = bitCount >> 3;
3697
- const rowStride = width * bytesPerPixel + 3 & ~3;
3698
- if (dataOffset + rowStride * height > bmp.length) return null;
3699
- const rgba = new Uint8Array(width * height * 4);
3700
- let anyAlpha = 0;
3701
- for (let y = 0; y < height; y++) {
3702
- const srcRow = topDown ? y : height - 1 - y;
3703
- let src = dataOffset + srcRow * rowStride;
3704
- let dst = y * width * 4;
3705
- for (let x = 0; x < width; x++) {
3706
- rgba[dst] = bmp[src + 2];
3707
- rgba[dst + 1] = bmp[src + 1];
3708
- rgba[dst + 2] = bmp[src];
3709
- const a = bitCount === 32 ? bmp[src + 3] : 255;
3710
- rgba[dst + 3] = a;
3711
- anyAlpha |= a;
3712
- src += bytesPerPixel;
3713
- dst += 4;
3714
- }
3715
- }
3716
- if (bitCount === 32 && anyAlpha === 0) {
3717
- for (let i = 3; i < rgba.length; i += 4) rgba[i] = 255;
3718
- }
3719
- return encodePng(width, height, rgba);
3720
- }
3721
- var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
3722
- function chunk(type, data) {
3723
- const body = new Uint8Array(4 + data.length);
3724
- body[0] = type.charCodeAt(0);
3725
- body[1] = type.charCodeAt(1);
3726
- body[2] = type.charCodeAt(2);
3727
- body[3] = type.charCodeAt(3);
3728
- body.set(data, 4);
3729
- const out = new Uint8Array(8 + data.length + 4);
3730
- const dv = new DataView(out.buffer);
3731
- dv.setUint32(0, data.length, false);
3732
- out.set(body, 4);
3733
- dv.setUint32(8 + data.length, crc32(body), false);
3734
- return out;
3735
- }
3736
- function encodePng(width, height, rgba) {
3737
- const ihdr = new Uint8Array(13);
3738
- const iv = new DataView(ihdr.buffer);
3739
- iv.setUint32(0, width, false);
3740
- iv.setUint32(4, height, false);
3741
- ihdr[8] = 8;
3742
- ihdr[9] = 6;
3743
- const stride = width * 4;
3744
- const raw = new Uint8Array((stride + 1) * height);
3745
- for (let y = 0; y < height; y++) {
3746
- const rowStart = y * (stride + 1);
3747
- raw[rowStart] = 0;
3748
- raw.set(rgba.subarray(y * stride, y * stride + stride), rowStart + 1);
3749
- }
3750
- const idat = deflateSync(raw);
3751
- const ihdrChunk = chunk("IHDR", ihdr);
3752
- const idatChunk = chunk("IDAT", idat);
3753
- const iendChunk = chunk("IEND", new Uint8Array(0));
3754
- const out = new Uint8Array(PNG_SIGNATURE.length + ihdrChunk.length + idatChunk.length + iendChunk.length);
3755
- let o = 0;
3756
- out.set(PNG_SIGNATURE, o);
3757
- o += PNG_SIGNATURE.length;
3758
- out.set(ihdrChunk, o);
3759
- o += ihdrChunk.length;
3760
- out.set(idatChunk, o);
3761
- o += idatChunk.length;
3762
- out.set(iendChunk, o);
3763
- return out;
3764
- }
3765
- function escapeRegExp(s) {
3766
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3767
- }
3768
- function inlineImagesIntoMarkdown(markdown, images, opts) {
3769
- const compress = opts?.compress !== false;
3770
- let out = markdown;
3771
- for (const img of images) {
3772
- let bytes = img.data;
3773
- let mime = img.mimeType;
3774
- if (compress && mime === "image/bmp") {
3775
- const png = bmpToPng(img.data);
3776
- if (png) {
3777
- bytes = png;
3778
- mime = "image/png";
3779
- }
3780
- }
3781
- const base64 = Buffer.from(bytes).toString("base64");
3782
- const dataUri = `data:${mime};base64,${base64}`;
3783
- const name = escapeRegExp(img.filename);
3784
- const mdRe = new RegExp(`!\\[image\\]\\((?:images/)?${name}\\)`, "g");
3785
- out = out.replace(mdRe, () => `![image](${dataUri})`);
3786
- const imgTagRe = new RegExp(`(<img\\b[^>]*\\bsrc=")(?:images/)?${name}(")`, "g");
3787
- out = out.replace(imgTagRe, (_m, pre, post) => `${pre}${dataUri}${post}`);
3788
- }
3789
- return out;
3790
- }
3791
-
3792
3696
  // src/hwp5/aes.ts
3793
3697
  var S_BOX = new Uint8Array([
3794
3698
  99,
@@ -4836,7 +4740,7 @@ function parseHwp5Document(buffer, options) {
4836
4740
  }
4837
4741
  }
4838
4742
  const blocks = [...doc.headerBlocks, ...bodyBlocks, ...doc.footerBlocks];
4839
- const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings);
4743
+ const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings, !pageFilter) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings, !pageFilter);
4840
4744
  let flatBlocks = flattenLayoutTables(blocks);
4841
4745
  if (options?.dedupeRunningHeaders) {
4842
4746
  const deduped = dedupeRunningHeaders(flatBlocks);
@@ -18057,7 +17961,7 @@ async function parseXlsxDocument(buffer, options) {
18057
17961
  }
18058
17962
  let pageFilter = null;
18059
17963
  if (options?.pages) {
18060
- const { parsePageRange: parsePageRange2 } = await import("./page-range-CIRRJPXJ.js");
17964
+ const { parsePageRange: parsePageRange2 } = await import("./page-range-LNMXJU6W.js");
18061
17965
  pageFilter = parsePageRange2(options.pages, sheets.length);
18062
17966
  }
18063
17967
  const blocks = [];
@@ -18695,7 +18599,7 @@ async function parseXlsDocument(buffer, options) {
18695
18599
  const totalSheets = Math.min(globals.sheets.length, MAX_SHEETS2);
18696
18600
  let pageFilter = null;
18697
18601
  if (options?.pages) {
18698
- const { parsePageRange: parsePageRange2 } = await import("./page-range-CIRRJPXJ.js");
18602
+ const { parsePageRange: parsePageRange2 } = await import("./page-range-LNMXJU6W.js");
18699
18603
  pageFilter = parsePageRange2(options.pages, totalSheets);
18700
18604
  }
18701
18605
  const allBlocks = [];
@@ -19533,9 +19437,17 @@ async function buildImageMap(zip, rels, doc, warnings) {
19533
19437
  const imageMap = /* @__PURE__ */ new Map();
19534
19438
  const images = [];
19535
19439
  let imgIdx = 0;
19440
+ const imageRefs = [];
19536
19441
  for (const blip of findElements(doc.documentElement, "blip")) {
19537
19442
  const embedId = getAttr(blip, "embed");
19538
- if (!embedId || imageMap.has(embedId)) continue;
19443
+ if (embedId) imageRefs.push(embedId);
19444
+ }
19445
+ for (const imagedata of collectNonFallbackImagedata(doc.documentElement)) {
19446
+ const embedId = getAttr(imagedata, "id");
19447
+ if (embedId) imageRefs.push(embedId);
19448
+ }
19449
+ for (const embedId of imageRefs) {
19450
+ if (imageMap.has(embedId)) continue;
19539
19451
  const target = rels.get(embedId);
19540
19452
  if (!target) continue;
19541
19453
  const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
@@ -19570,14 +19482,26 @@ function collectParagraphBlips(node, out = [], depth = 0) {
19570
19482
  if (depth > 40) return out;
19571
19483
  for (const el of effectiveChildElements(node)) {
19572
19484
  if (matchesLocal(el, "txbxContent") || matchesLocal(el, "Fallback")) continue;
19573
- if (matchesLocal(el, "blip")) out.push(el);
19485
+ if (matchesLocal(el, "blip") || matchesLocal(el, "imagedata")) out.push(el);
19574
19486
  else collectParagraphBlips(el, out, depth + 1);
19575
19487
  }
19576
19488
  return out;
19577
19489
  }
19490
+ function collectNonFallbackImagedata(node, out = [], depth = 0) {
19491
+ if (depth > 60) return out;
19492
+ const children = node.childNodes;
19493
+ for (let i = 0; i < children.length; i++) {
19494
+ if (children[i].nodeType !== 1) continue;
19495
+ const el = children[i];
19496
+ if (matchesLocal(el, "Fallback")) continue;
19497
+ if (matchesLocal(el, "imagedata")) out.push(el);
19498
+ else collectNonFallbackImagedata(el, out, depth + 1);
19499
+ }
19500
+ return out;
19501
+ }
19578
19502
  function emitParagraphImages(p, imageMap, linked, out) {
19579
19503
  for (const blip of collectParagraphBlips(p)) {
19580
- const embedId = getAttr(blip, "embed");
19504
+ const embedId = getAttr(blip, "embed") ?? getAttr(blip, "id");
19581
19505
  if (!embedId) continue;
19582
19506
  const filename = imageMap.get(embedId);
19583
19507
  if (!filename) continue;
@@ -21270,7 +21194,7 @@ function parseCentralDirectory(buf) {
21270
21194
  }
21271
21195
  return { entries, cdOffset, cdSize, eocdOffset };
21272
21196
  }
21273
- var CRC_TABLE2 = (() => {
21197
+ var CRC_TABLE = (() => {
21274
21198
  const table2 = new Uint32Array(256);
21275
21199
  for (let n = 0; n < 256; n++) {
21276
21200
  let c = n;
@@ -21279,10 +21203,10 @@ var CRC_TABLE2 = (() => {
21279
21203
  }
21280
21204
  return table2;
21281
21205
  })();
21282
- function crc322(data) {
21206
+ function crc32(data) {
21283
21207
  let crc = 4294967295;
21284
21208
  for (let i = 0; i < data.length; i++) {
21285
- crc = CRC_TABLE2[(crc ^ data[i]) & 255] ^ crc >>> 8;
21209
+ crc = CRC_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
21286
21210
  }
21287
21211
  return (crc ^ 4294967295) >>> 0;
21288
21212
  }
@@ -21321,7 +21245,7 @@ function patchZipEntries(original, replacements, additions) {
21321
21245
  const hview = new DataView(header.buffer, header.byteOffset, header.byteLength);
21322
21246
  const method = e.method;
21323
21247
  const compData = method === 0 ? newData : new Uint8Array(deflateRawSync(newData));
21324
- const crc = crc322(newData);
21248
+ const crc = crc32(newData);
21325
21249
  const flags = e.flags & ~8;
21326
21250
  hview.setUint16(6, flags, true);
21327
21251
  hview.setUint32(14, crc, true);
@@ -21339,7 +21263,7 @@ function patchZipEntries(original, replacements, additions) {
21339
21263
  const deflated = new Uint8Array(deflateRawSync(data));
21340
21264
  const method = deflated.length < data.length ? 8 : 0;
21341
21265
  const compData = method === 8 ? deflated : data;
21342
- const crc = crc322(data);
21266
+ const crc = crc32(data);
21343
21267
  const header = new Uint8Array(30 + nameBytes.length);
21344
21268
  const hv = new DataView(header.buffer);
21345
21269
  hv.setUint32(0, LOCAL_SIG, true);
@@ -26881,16 +26805,16 @@ function buildOrigUnits(blocks) {
26881
26805
  for (let i = 0; i < blocks.length; i++) {
26882
26806
  const block = blocks[i];
26883
26807
  let consume = 1;
26884
- let chunk2;
26808
+ let chunk;
26885
26809
  if (block.type === "paragraph" && block.text && /^\[별표\s*\d+/.test(sanitizeText(block.text))) {
26886
26810
  const next = blocks[i + 1];
26887
26811
  if (next?.type === "paragraph" && next.text && /관련\)?$/.test(next.text)) consume = 2;
26888
- chunk2 = blocksToMarkdown(blocks.slice(i, i + consume));
26812
+ chunk = blocksToMarkdown(blocks.slice(i, i + consume));
26889
26813
  } else {
26890
- chunk2 = blocksToMarkdown([block]);
26814
+ chunk = blocksToMarkdown([block]);
26891
26815
  }
26892
- if (chunk2) {
26893
- const subUnits = splitMarkdownUnits(chunk2);
26816
+ if (chunk) {
26817
+ const subUnits = splitMarkdownUnits(chunk);
26894
26818
  const isFragment = consume === 2 || (block.type === "paragraph" || block.type === "heading") && subUnits.length > 1;
26895
26819
  for (let s = 0; s < subUnits.length; s++) {
26896
26820
  const u = { ...subUnits[s], blockIdx: i, fragment: isFragment || void 0 };
@@ -29883,7 +29807,7 @@ async function parseHwp(buffer, options) {
29883
29807
  async function parsePdf(buffer, options) {
29884
29808
  let parsePdfDocument;
29885
29809
  try {
29886
- const mod = await import("./parser-UGB3NIVH.js");
29810
+ const mod = await import("./parser-KMMUS73Q.js");
29887
29811
  parsePdfDocument = mod.parsePdfDocument;
29888
29812
  } catch {
29889
29813
  return {
@@ -29894,8 +29818,8 @@ async function parsePdf(buffer, options) {
29894
29818
  };
29895
29819
  }
29896
29820
  try {
29897
- const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary } = await parsePdfDocument(buffer, options);
29898
- return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary };
29821
+ const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images } = await parsePdfDocument(buffer, options);
29822
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images };
29899
29823
  } catch (err) {
29900
29824
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
29901
29825
  return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };