kordoc 4.0.6 → 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 (86) hide show
  1. package/README.md +823 -806
  2. package/dist/{-NDU7YGSS.js → -LE4RLXVA.js} +8 -8
  3. package/dist/{chunk-5SOZXPQY.js → chunk-2DZQF6YJ.js} +3 -3
  4. package/dist/chunk-2DZQF6YJ.js.map +1 -0
  5. package/dist/{chunk-6GBLFQMA.js → chunk-5RPYBC2Q.js} +1 -1
  6. package/dist/chunk-5RPYBC2Q.js.map +1 -0
  7. package/dist/{chunk-DQJTNXVS.cjs → chunk-6XAAYAWL.cjs} +142 -3
  8. package/dist/chunk-6XAAYAWL.cjs.map +1 -0
  9. package/dist/{chunk-HC2SEH6I.js → chunk-7S3M4N4E.js} +939 -946
  10. package/dist/chunk-7S3M4N4E.js.map +1 -0
  11. package/dist/{chunk-4C7SBZNW.js → chunk-BNU5QGIZ.js} +2 -2
  12. package/dist/chunk-BNU5QGIZ.js.map +1 -0
  13. package/dist/{chunk-63ASYC4N.js → chunk-D35VBACN.js} +3 -3
  14. package/dist/chunk-D35VBACN.js.map +1 -0
  15. package/dist/chunk-DCZVOIEO.cjs.map +1 -1
  16. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  17. package/dist/{chunk-L3I4TIRP.js → chunk-HMUEU7D7.js} +2 -2
  18. package/dist/chunk-HMUEU7D7.js.map +1 -0
  19. package/dist/{chunk-5CJGKKMZ.js → chunk-MEPHGCPQ.js} +1 -1
  20. package/dist/chunk-MEPHGCPQ.js.map +1 -0
  21. package/dist/chunk-MOL7MDBG.js +0 -0
  22. package/dist/{chunk-HOGJ5ALY.js → chunk-OQPILS7B.js} +142 -3
  23. package/dist/chunk-OQPILS7B.js.map +1 -0
  24. package/dist/{chunk-KA4FEBS2.js → chunk-RROH5WHO.js} +2 -2
  25. package/dist/chunk-RROH5WHO.js.map +1 -0
  26. package/dist/{chunk-UVSOSFER.js → chunk-UEIYETCQ.js} +142 -3
  27. package/dist/chunk-UEIYETCQ.js.map +1 -0
  28. package/dist/cli.js +15 -15
  29. package/dist/cli.js.map +1 -1
  30. package/dist/{detect-PJZMUL2Z.js → detect-RI2MQ33K.js} +2 -2
  31. package/dist/formula-5NKVS2LR.cjs.map +1 -1
  32. package/dist/formula-JCNF43NE.js +0 -0
  33. package/dist/index.cjs +1248 -1255
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +925 -932
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp.js +13 -13
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/page-range-737B4EZW.js +0 -0
  42. package/dist/page-range-P7SDW6LR.cjs.map +1 -1
  43. package/dist/{parser-VEY4B5CJ.cjs → parser-ITMU2WN5.cjs} +231 -37
  44. package/dist/parser-ITMU2WN5.cjs.map +1 -0
  45. package/dist/{parser-DMKHIV4E.js → parser-KMMUS73Q.js} +200 -6
  46. package/dist/parser-KMMUS73Q.js.map +1 -0
  47. package/dist/{parser-C636QIOB.js → parser-TCTCSBYZ.js} +202 -8
  48. package/dist/parser-TCTCSBYZ.js.map +1 -0
  49. package/dist/{profile-io-JYGCC6XQ.js → profile-io-SLN4P76T.js} +3 -3
  50. package/dist/{provider-H4WWSPOV.js → provider-4ZJKV3DC.js} +1 -1
  51. package/dist/provider-4ZJKV3DC.js.map +1 -0
  52. package/dist/{provider-7H4CPZYS.js → provider-AKROB7WQ.js} +1 -1
  53. package/dist/provider-AKROB7WQ.js.map +1 -0
  54. package/dist/{provider-5K7O3Z2O.cjs → provider-G4C2V2PD.cjs} +1 -1
  55. package/dist/provider-G4C2V2PD.cjs.map +1 -0
  56. package/dist/render-25BT623I.js +10 -0
  57. package/dist/seal-R4TETA4C.js +10 -0
  58. package/dist/setup-57FB3LSP.js +0 -0
  59. package/dist/{watch-ZQUFJBFR.js → watch-OHQWSVPE.js} +8 -8
  60. package/dist/watch-OHQWSVPE.js.map +1 -0
  61. package/package.json +97 -97
  62. package/dist/chunk-4C7SBZNW.js.map +0 -1
  63. package/dist/chunk-5CJGKKMZ.js.map +0 -1
  64. package/dist/chunk-5SOZXPQY.js.map +0 -1
  65. package/dist/chunk-63ASYC4N.js.map +0 -1
  66. package/dist/chunk-6GBLFQMA.js.map +0 -1
  67. package/dist/chunk-DQJTNXVS.cjs.map +0 -1
  68. package/dist/chunk-HC2SEH6I.js.map +0 -1
  69. package/dist/chunk-HOGJ5ALY.js.map +0 -1
  70. package/dist/chunk-KA4FEBS2.js.map +0 -1
  71. package/dist/chunk-L3I4TIRP.js.map +0 -1
  72. package/dist/chunk-UVSOSFER.js.map +0 -1
  73. package/dist/parser-C636QIOB.js.map +0 -1
  74. package/dist/parser-DMKHIV4E.js.map +0 -1
  75. package/dist/parser-VEY4B5CJ.cjs.map +0 -1
  76. package/dist/provider-5K7O3Z2O.cjs.map +0 -1
  77. package/dist/provider-7H4CPZYS.js.map +0 -1
  78. package/dist/provider-H4WWSPOV.js.map +0 -1
  79. package/dist/render-ISOEOMTM.js +0 -10
  80. package/dist/seal-676ZIAZZ.js +0 -10
  81. package/dist/watch-ZQUFJBFR.js.map +0 -1
  82. /package/dist/{-NDU7YGSS.js.map → -LE4RLXVA.js.map} +0 -0
  83. /package/dist/{detect-PJZMUL2Z.js.map → detect-RI2MQ33K.js.map} +0 -0
  84. /package/dist/{profile-io-JYGCC6XQ.js.map → profile-io-SLN4P76T.js.map} +0 -0
  85. /package/dist/{render-ISOEOMTM.js.map → render-25BT623I.js.map} +0 -0
  86. /package/dist/{seal-676ZIAZZ.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,7 +21,7 @@ import {
20
21
  sanitizeHref,
21
22
  stripDtd,
22
23
  toArrayBuffer
23
- } from "./chunk-HOGJ5ALY.js";
24
+ } from "./chunk-OQPILS7B.js";
24
25
  import {
25
26
  parsePageRange
26
27
  } from "./chunk-GE43BE46.js";
@@ -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
2865
  }
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
- }
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;
2875
- }
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;
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 {
2888
2893
  }
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;
2895
2894
  }
2896
2895
  }
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 };
2896
+ return { charShapes, paraShapes, styles, binData, numberings, bullets };
2901
2897
  }
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
- }
2898
+ function createParaTextState() {
2899
+ return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2912
2900
  }
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);
2901
+ function isExtendedOnlyCtrlChar(ch) {
2902
+ return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2925
2903
  }
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);
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;
2931
+ }
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;
2943
2983
  }
2944
- if (ordered.length > 0) return ordered;
2945
2984
  }
2946
- return Array.from(idToHref.values()).sort(compareSectionPaths);
2985
+ state.text += result;
2986
+ }
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;
2947
2995
  }
2948
2996
 
2949
- // src/hwpx/metadata.ts
2950
- async function extractHwpxMetadata(zip, metadata, decompressed) {
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;
3007
+ }
3008
+ function normalizeBinPayload(data) {
3009
+ if (detectImageMime(data)) return data;
2951
3010
  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)");
2960
- }
2961
- parseDublinCoreMetadata(xml, metadata);
2962
- if (metadata.title || metadata.author) return;
2963
- }
3011
+ const inflated = decompressStream(data);
3012
+ if (inflated.length > 0) return inflated;
2964
3013
  } catch {
2965
3014
  }
3015
+ return data;
2966
3016
  }
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;
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
+ }
2977
3026
  }
2978
3027
  }
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);
3028
+ if (b.children) collectImageBlocks(b.children, out);
2989
3029
  }
2990
3030
  }
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)");
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
+ }
3039
+ }
3040
+ }
3041
+ if (b.children) forEachTableCell(b.children, fn);
3004
3042
  }
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);
3043
+ }
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
  }
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;
3092
+ }
3093
+ block.text = img.filename;
3094
+ block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3017
3095
  }
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" });
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 } });
3046
3107
  }
3047
3108
  }
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 };
3109
+ resolveCellImageSentinels(blocks, renamed);
3110
+ return images;
3054
3111
  }
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;
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 });
3100
3122
  }
3101
- if (offset + size > data.length) break;
3102
- records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
3103
- offset += size;
3104
3123
  }
3105
- return records;
3124
+ if (binDataMap.size === 0) {
3125
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3126
+ return [];
3127
+ }
3128
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
3106
3129
  }
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 {
3114
- }
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 [];
3115
3144
  }
3116
- return inflateRawSync2(data, opts);
3145
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
3117
3146
  }
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
- };
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
+ }
3126
3172
  }
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 };
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";
3134
3183
  }
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 });
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
3286
  }
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" });
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++;
3314
+ }
3315
+ continue;
3316
+ }
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;
3197
3337
  }
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 });
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);
3203
3348
  } else {
3204
- charShapes.push({ fontSize: 0, attrFlags: 0 });
3205
- }
3206
- }
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 {
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++;
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)");
3422
+ }
3423
+ parseDublinCoreMetadata(xml, metadata);
3424
+ if (metadata.title || metadata.author) return;
3425
+ }
3426
+ } catch {
3427
+ }
3428
+ }
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;
3255
3442
  };
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;
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);
3269
3475
  }
3270
- result += "\n";
3271
- break;
3272
3476
  }
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;
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" });
3324
3508
  }
3325
3509
  }
3326
- state.text += result;
3327
- }
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;
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);
@@ -19297,6 +19201,102 @@ function extractRun(r) {
19297
19201
  }
19298
19202
  return { text, bold, italic };
19299
19203
  }
19204
+ function fieldUrlFromInstr(instr) {
19205
+ if (!/HYPERLINK\b/i.test(instr)) return null;
19206
+ const urlM = instr.match(/HYPERLINK\s+"([^"]*)"/i);
19207
+ const anchorM = instr.match(/\\l\s+"([^"]*)"/i);
19208
+ if (urlM && urlM[1]) {
19209
+ return anchorM && anchorM[1] ? `${urlM[1]}#${anchorM[1]}` : urlM[1];
19210
+ }
19211
+ if (anchorM && anchorM[1]) return `#${anchorM[1]}`;
19212
+ return null;
19213
+ }
19214
+ function makeLink(text, rawHref) {
19215
+ if (!text) return "";
19216
+ if (!rawHref) return text;
19217
+ const href = sanitizeHref(rawHref);
19218
+ return href ? `[${text}](${href})` : text;
19219
+ }
19220
+ function collectInline(p, footnotes, rels) {
19221
+ const parts = [];
19222
+ let bold = false;
19223
+ let italic = false;
19224
+ let footnoteText;
19225
+ let fieldActive = false;
19226
+ let fieldStage = "instr";
19227
+ let fieldInstr = "";
19228
+ let fieldDisplay = "";
19229
+ const flushField = () => {
19230
+ if (!fieldActive) return;
19231
+ if (fieldDisplay) {
19232
+ const url = fieldUrlFromInstr(fieldInstr);
19233
+ parts.push(url ? makeLink(fieldDisplay, url) : fieldDisplay);
19234
+ }
19235
+ fieldActive = false;
19236
+ fieldStage = "instr";
19237
+ fieldInstr = "";
19238
+ fieldDisplay = "";
19239
+ };
19240
+ for (const el of effectiveChildElements(p)) {
19241
+ if (matchesLocal(el, "hyperlink")) {
19242
+ const rId = getAttr(el, "id");
19243
+ const anchor = getAttr(el, "anchor");
19244
+ const t = findElements(el, "r").map((r) => extractRun(r).text).join("");
19245
+ if (!t) continue;
19246
+ let raw = null;
19247
+ if (rId && rels.has(rId)) raw = rels.get(rId);
19248
+ else if (anchor) raw = `#${anchor}`;
19249
+ if (fieldActive && fieldStage === "display") fieldDisplay += t;
19250
+ else parts.push(makeLink(t, raw));
19251
+ continue;
19252
+ }
19253
+ if (matchesLocal(el, "fldSimple")) {
19254
+ const t = findElements(el, "r").map((r) => extractRun(r).text).join("");
19255
+ if (!t) continue;
19256
+ const url = fieldUrlFromInstr(getAttr(el, "instr") ?? "");
19257
+ parts.push(url ? makeLink(t, url) : t);
19258
+ continue;
19259
+ }
19260
+ if (matchesLocal(el, "r")) {
19261
+ for (const fc of getChildElements(el, "fldChar")) {
19262
+ const ty = getAttr(fc, "fldCharType");
19263
+ if (ty === "begin") {
19264
+ flushField();
19265
+ fieldActive = true;
19266
+ fieldStage = "instr";
19267
+ } else if (ty === "separate") {
19268
+ if (fieldActive) fieldStage = "display";
19269
+ } else if (ty === "end") flushField();
19270
+ }
19271
+ if (fieldActive && fieldStage === "instr") {
19272
+ for (const it of getChildElements(el, "instrText")) fieldInstr += it.textContent ?? "";
19273
+ }
19274
+ const rr = extractRun(el);
19275
+ if (rr.bold) bold = true;
19276
+ if (rr.italic) italic = true;
19277
+ const fnRefEls = getChildElements(el, "footnoteReference");
19278
+ if (fnRefEls.length > 0) {
19279
+ const fnId = getAttr(fnRefEls[0], "id");
19280
+ if (fnId && footnotes.has(fnId)) footnoteText = footnotes.get(fnId);
19281
+ }
19282
+ if (rr.text) {
19283
+ if (fieldActive && fieldStage === "display") fieldDisplay += rr.text;
19284
+ else if (fieldActive && fieldStage === "instr") {
19285
+ } else parts.push(rr.text);
19286
+ }
19287
+ continue;
19288
+ }
19289
+ }
19290
+ flushField();
19291
+ for (const om of collectOmmlRoots(p)) {
19292
+ const latex = ommlElementToLatex(om);
19293
+ if (!latex) continue;
19294
+ if (isDisplayMath(om)) parts.push(" $$" + latex + "$$ ");
19295
+ else parts.push(" $" + latex + "$ ");
19296
+ }
19297
+ const text = parts.join("").replace(/[ \t]{2,}/g, " ").trim();
19298
+ return { text, bold, italic, footnoteText };
19299
+ }
19300
19300
  function parseParagraph2(p, styles, numbering, footnotes, rels) {
19301
19301
  const pPrEls = getChildElements(p, "pPr");
19302
19302
  let styleId = "";
@@ -19313,54 +19313,7 @@ function parseParagraph2(p, styles, numbering, footnotes, rels) {
19313
19313
  ilvl = ilvlEls.length > 0 ? parseInt(getAttr(ilvlEls[0], "val") ?? "0", 10) : 0;
19314
19314
  }
19315
19315
  }
19316
- const parts = [];
19317
- let hasBold = false;
19318
- let hasItalic = false;
19319
- let href;
19320
- let footnoteText;
19321
- const hyperlinks = getChildElements(p, "hyperlink");
19322
- const hyperlinkTexts = /* @__PURE__ */ new Set();
19323
- for (const hl of hyperlinks) {
19324
- const rId = getAttr(hl, "id");
19325
- const hlText = [];
19326
- const runs2 = findElements(hl, "r");
19327
- for (const r of runs2) {
19328
- const result = extractRun(r);
19329
- hlText.push(result.text);
19330
- }
19331
- const text2 = hlText.join("");
19332
- if (text2) {
19333
- hyperlinkTexts.add(text2);
19334
- if (rId && rels.has(rId)) {
19335
- href = rels.get(rId);
19336
- parts.push(text2);
19337
- } else {
19338
- parts.push(text2);
19339
- }
19340
- }
19341
- }
19342
- const runs = getChildElements(p, "r");
19343
- for (const r of runs) {
19344
- if (r.parentNode && r.parentNode.localName === "hyperlink") continue;
19345
- const result = extractRun(r);
19346
- if (result.bold) hasBold = true;
19347
- if (result.italic) hasItalic = true;
19348
- const fnRefEls = getChildElements(r, "footnoteReference");
19349
- if (fnRefEls.length > 0) {
19350
- const fnId = getAttr(fnRefEls[0], "id");
19351
- if (fnId && footnotes.has(fnId)) {
19352
- footnoteText = footnotes.get(fnId);
19353
- }
19354
- }
19355
- if (result.text) parts.push(result.text);
19356
- }
19357
- for (const om of collectOmmlRoots(p)) {
19358
- const latex = ommlElementToLatex(om);
19359
- if (!latex) continue;
19360
- if (isDisplayMath(om)) parts.push(" $$" + latex + "$$ ");
19361
- else parts.push(" $" + latex + "$ ");
19362
- }
19363
- const text = parts.join("").replace(/[ \t]{2,}/g, " ").trim();
19316
+ const { text, bold: hasBold, italic: hasItalic, footnoteText } = collectInline(p, footnotes, rels);
19364
19317
  if (!text) return null;
19365
19318
  const style = styles.get(styleId);
19366
19319
  if (style?.outlineLevel !== void 0 && style.outlineLevel >= 0 && style.outlineLevel <= 5) {
@@ -19380,7 +19333,6 @@ function parseParagraph2(p, styles, numbering, footnotes, rels) {
19380
19333
  if (hasBold || hasItalic) {
19381
19334
  block.style = { bold: hasBold || void 0, italic: hasItalic || void 0 };
19382
19335
  }
19383
- if (href) block.href = href;
19384
19336
  if (footnoteText) block.footnoteText = footnoteText;
19385
19337
  return block;
19386
19338
  }
@@ -19481,46 +19433,81 @@ function collectCellText(tc2, styles, numbering, footnotes, rels, depth) {
19481
19433
  }
19482
19434
  return parts;
19483
19435
  }
19484
- async function extractImages(zip, rels, doc, warnings) {
19485
- const blocks = [];
19436
+ async function buildImageMap(zip, rels, doc, warnings) {
19437
+ const imageMap = /* @__PURE__ */ new Map();
19486
19438
  const images = [];
19487
- const drawingElements = findElements(doc.documentElement, "drawing");
19488
19439
  let imgIdx = 0;
19489
- for (const drawing of drawingElements) {
19490
- const blips = findElements(drawing, "blip");
19491
- for (const blip of blips) {
19492
- const embedId = getAttr(blip, "embed");
19493
- if (!embedId) continue;
19494
- const target = rels.get(embedId);
19495
- if (!target) continue;
19496
- const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
19497
- const imgFile = zip.file(imgPath);
19498
- if (!imgFile) continue;
19499
- try {
19500
- const data = await imgFile.async("uint8array");
19501
- imgIdx++;
19502
- const ext = imgPath.split(".").pop()?.toLowerCase() ?? "png";
19503
- const mimeMap = {
19504
- png: "image/png",
19505
- jpg: "image/jpeg",
19506
- jpeg: "image/jpeg",
19507
- gif: "image/gif",
19508
- bmp: "image/bmp",
19509
- wmf: "image/wmf",
19510
- emf: "image/emf"
19511
- };
19512
- const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
19513
- images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
19514
- blocks.push({ type: "image", text: filename });
19515
- } catch (err) {
19516
- warnings.push({
19517
- code: "SKIPPED_IMAGE",
19518
- message: `DOCX \uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328 (${imgPath}): ${err instanceof Error ? err.message : String(err)}`
19519
- });
19520
- }
19440
+ const imageRefs = [];
19441
+ for (const blip of findElements(doc.documentElement, "blip")) {
19442
+ const embedId = getAttr(blip, "embed");
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;
19451
+ const target = rels.get(embedId);
19452
+ if (!target) continue;
19453
+ const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
19454
+ const imgFile = zip.file(imgPath);
19455
+ if (!imgFile) continue;
19456
+ try {
19457
+ const data = await imgFile.async("uint8array");
19458
+ imgIdx++;
19459
+ const ext = imgPath.split(".").pop()?.toLowerCase() ?? "png";
19460
+ const mimeMap = {
19461
+ png: "image/png",
19462
+ jpg: "image/jpeg",
19463
+ jpeg: "image/jpeg",
19464
+ gif: "image/gif",
19465
+ bmp: "image/bmp",
19466
+ wmf: "image/wmf",
19467
+ emf: "image/emf"
19468
+ };
19469
+ const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
19470
+ images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
19471
+ imageMap.set(embedId, filename);
19472
+ } catch (err) {
19473
+ warnings.push({
19474
+ code: "SKIPPED_IMAGE",
19475
+ message: `DOCX \uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328 (${imgPath}): ${err instanceof Error ? err.message : String(err)}`
19476
+ });
19521
19477
  }
19522
19478
  }
19523
- return { blocks, images };
19479
+ return { imageMap, images };
19480
+ }
19481
+ function collectParagraphBlips(node, out = [], depth = 0) {
19482
+ if (depth > 40) return out;
19483
+ for (const el of effectiveChildElements(node)) {
19484
+ if (matchesLocal(el, "txbxContent") || matchesLocal(el, "Fallback")) continue;
19485
+ if (matchesLocal(el, "blip") || matchesLocal(el, "imagedata")) out.push(el);
19486
+ else collectParagraphBlips(el, out, depth + 1);
19487
+ }
19488
+ return out;
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
+ }
19502
+ function emitParagraphImages(p, imageMap, linked, out) {
19503
+ for (const blip of collectParagraphBlips(p)) {
19504
+ const embedId = getAttr(blip, "embed") ?? getAttr(blip, "id");
19505
+ if (!embedId) continue;
19506
+ const filename = imageMap.get(embedId);
19507
+ if (!filename) continue;
19508
+ linked.add(embedId);
19509
+ out.push({ type: "image", text: filename });
19510
+ }
19524
19511
  }
19525
19512
  async function parseDocxDocument(buffer, options) {
19526
19513
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
@@ -19577,6 +19564,8 @@ async function parseDocxDocument(buffer, options) {
19577
19564
  if (body.length === 0) {
19578
19565
  throw new KordocError("DOCX \uBCF8\uBB38(w:body)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
19579
19566
  }
19567
+ const { imageMap, images } = await buildImageMap(zip, rels, doc, warnings);
19568
+ const linkedImages = /* @__PURE__ */ new Set();
19580
19569
  const blocks = [];
19581
19570
  const bodyEl = body[0];
19582
19571
  const topLevel = effectiveChildElements(bodyEl);
@@ -19585,16 +19574,20 @@ async function parseDocxDocument(buffer, options) {
19585
19574
  if (localName2 === "p") {
19586
19575
  const block = parseParagraph2(el, styles, numbering, footnotes, rels);
19587
19576
  if (block) blocks.push(block);
19577
+ if (imageMap.size > 0) emitParagraphImages(el, imageMap, linkedImages, blocks);
19588
19578
  for (const tp of collectTextboxParagraphs(el)) {
19589
19579
  const tb = parseParagraph2(tp, styles, numbering, footnotes, rels);
19590
19580
  if (tb) blocks.push(tb);
19581
+ if (imageMap.size > 0) emitParagraphImages(tp, imageMap, linkedImages, blocks);
19591
19582
  }
19592
19583
  } else if (localName2 === "tbl") {
19593
19584
  const block = parseTable(el, styles, numbering, footnotes, rels);
19594
19585
  if (block) blocks.push(block);
19595
19586
  }
19596
19587
  }
19597
- const { blocks: imgBlocks, images } = await extractImages(zip, rels, doc, warnings);
19588
+ for (const [embedId, filename] of imageMap) {
19589
+ if (!linkedImages.has(embedId)) blocks.push({ type: "image", text: filename });
19590
+ }
19598
19591
  const metadata = {};
19599
19592
  const coreFile = zip.file("docProps/core.xml");
19600
19593
  if (coreFile) {
@@ -21201,7 +21194,7 @@ function parseCentralDirectory(buf) {
21201
21194
  }
21202
21195
  return { entries, cdOffset, cdSize, eocdOffset };
21203
21196
  }
21204
- var CRC_TABLE2 = (() => {
21197
+ var CRC_TABLE = (() => {
21205
21198
  const table2 = new Uint32Array(256);
21206
21199
  for (let n = 0; n < 256; n++) {
21207
21200
  let c = n;
@@ -21210,10 +21203,10 @@ var CRC_TABLE2 = (() => {
21210
21203
  }
21211
21204
  return table2;
21212
21205
  })();
21213
- function crc322(data) {
21206
+ function crc32(data) {
21214
21207
  let crc = 4294967295;
21215
21208
  for (let i = 0; i < data.length; i++) {
21216
- crc = CRC_TABLE2[(crc ^ data[i]) & 255] ^ crc >>> 8;
21209
+ crc = CRC_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
21217
21210
  }
21218
21211
  return (crc ^ 4294967295) >>> 0;
21219
21212
  }
@@ -21252,7 +21245,7 @@ function patchZipEntries(original, replacements, additions) {
21252
21245
  const hview = new DataView(header.buffer, header.byteOffset, header.byteLength);
21253
21246
  const method = e.method;
21254
21247
  const compData = method === 0 ? newData : new Uint8Array(deflateRawSync(newData));
21255
- const crc = crc322(newData);
21248
+ const crc = crc32(newData);
21256
21249
  const flags = e.flags & ~8;
21257
21250
  hview.setUint16(6, flags, true);
21258
21251
  hview.setUint32(14, crc, true);
@@ -21270,7 +21263,7 @@ function patchZipEntries(original, replacements, additions) {
21270
21263
  const deflated = new Uint8Array(deflateRawSync(data));
21271
21264
  const method = deflated.length < data.length ? 8 : 0;
21272
21265
  const compData = method === 8 ? deflated : data;
21273
- const crc = crc322(data);
21266
+ const crc = crc32(data);
21274
21267
  const header = new Uint8Array(30 + nameBytes.length);
21275
21268
  const hv = new DataView(header.buffer);
21276
21269
  hv.setUint32(0, LOCAL_SIG, true);
@@ -26812,16 +26805,16 @@ function buildOrigUnits(blocks) {
26812
26805
  for (let i = 0; i < blocks.length; i++) {
26813
26806
  const block = blocks[i];
26814
26807
  let consume = 1;
26815
- let chunk2;
26808
+ let chunk;
26816
26809
  if (block.type === "paragraph" && block.text && /^\[별표\s*\d+/.test(sanitizeText(block.text))) {
26817
26810
  const next = blocks[i + 1];
26818
26811
  if (next?.type === "paragraph" && next.text && /관련\)?$/.test(next.text)) consume = 2;
26819
- chunk2 = blocksToMarkdown(blocks.slice(i, i + consume));
26812
+ chunk = blocksToMarkdown(blocks.slice(i, i + consume));
26820
26813
  } else {
26821
- chunk2 = blocksToMarkdown([block]);
26814
+ chunk = blocksToMarkdown([block]);
26822
26815
  }
26823
- if (chunk2) {
26824
- const subUnits = splitMarkdownUnits(chunk2);
26816
+ if (chunk) {
26817
+ const subUnits = splitMarkdownUnits(chunk);
26825
26818
  const isFragment = consume === 2 || (block.type === "paragraph" || block.type === "heading") && subUnits.length > 1;
26826
26819
  for (let s = 0; s < subUnits.length; s++) {
26827
26820
  const u = { ...subUnits[s], blockIdx: i, fragment: isFragment || void 0 };
@@ -29814,7 +29807,7 @@ async function parseHwp(buffer, options) {
29814
29807
  async function parsePdf(buffer, options) {
29815
29808
  let parsePdfDocument;
29816
29809
  try {
29817
- const mod = await import("./parser-DMKHIV4E.js");
29810
+ const mod = await import("./parser-KMMUS73Q.js");
29818
29811
  parsePdfDocument = mod.parsePdfDocument;
29819
29812
  } catch {
29820
29813
  return {
@@ -29825,8 +29818,8 @@ async function parsePdf(buffer, options) {
29825
29818
  };
29826
29819
  }
29827
29820
  try {
29828
- const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary } = await parsePdfDocument(buffer, options);
29829
- 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 };
29830
29823
  } catch (err) {
29831
29824
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
29832
29825
  return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };