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
@@ -10,14 +10,15 @@ import {
10
10
  convertTableToText,
11
11
  dedupeRunningHeaders,
12
12
  flattenLayoutTables,
13
+ inlineImagesIntoMarkdown,
13
14
  mapPuaText
14
- } from "./chunk-UVSOSFER.js";
15
+ } from "./chunk-UEIYETCQ.js";
15
16
  import {
16
17
  detectFormat,
17
18
  detectOle2Format,
18
19
  detectZipFormat,
19
20
  parseLenientCfb
20
- } from "./chunk-5CJGKKMZ.js";
21
+ } from "./chunk-MEPHGCPQ.js";
21
22
  import {
22
23
  parsePageRange
23
24
  } from "./chunk-MOL7MDBG.js";
@@ -32,14 +33,14 @@ import {
32
33
  paraTextPureT,
33
34
  patchZipEntries,
34
35
  scanSectionXml
35
- } from "./chunk-63ASYC4N.js";
36
+ } from "./chunk-D35VBACN.js";
36
37
  import {
37
38
  SPACE_EM_FIXED,
38
39
  charWidthEm1000,
39
40
  fitRatioForFewerLines,
40
41
  measureTextWidth,
41
42
  simulateWrap
42
- } from "./chunk-5SOZXPQY.js";
43
+ } from "./chunk-2DZQF6YJ.js";
43
44
  import {
44
45
  MAX_DECOMPRESS_SIZE,
45
46
  MAX_XML_DEPTH,
@@ -50,7 +51,7 @@ import {
50
51
  createXmlParser,
51
52
  extractTextFromNode,
52
53
  findChildByLocalName
53
- } from "./chunk-4C7SBZNW.js";
54
+ } from "./chunk-BNU5QGIZ.js";
54
55
  import {
55
56
  KordocError,
56
57
  classifyError,
@@ -61,7 +62,7 @@ import {
61
62
  sanitizeHref,
62
63
  stripDtd,
63
64
  toArrayBuffer
64
- } from "./chunk-L3I4TIRP.js";
65
+ } from "./chunk-HMUEU7D7.js";
65
66
 
66
67
  // src/index.ts
67
68
  import { readFile } from "fs/promises";
@@ -2199,641 +2200,820 @@ function gongmunDepthFromIndent(para2, left, ctx) {
2199
2200
  return depth >= 1 && depth < 8 ? depth : null;
2200
2201
  }
2201
2202
 
2202
- // src/hwpx/images.ts
2203
- function imageExtToMime(ext) {
2204
- switch (ext.toLowerCase()) {
2205
- case "jpg":
2206
- case "jpeg":
2207
- return "image/jpeg";
2208
- case "png":
2209
- return "image/png";
2210
- case "gif":
2211
- return "image/gif";
2212
- case "bmp":
2213
- return "image/bmp";
2214
- case "tif":
2215
- case "tiff":
2216
- return "image/tiff";
2217
- case "wmf":
2218
- return "image/wmf";
2219
- case "emf":
2220
- return "image/emf";
2221
- case "svg":
2222
- return "image/svg+xml";
2223
- default:
2224
- return "application/octet-stream";
2203
+ // src/hwp5/record.ts
2204
+ import { inflateRawSync, inflateSync } from "zlib";
2205
+ var TAG_PARA_HEADER = 66;
2206
+ var TAG_PARA_TEXT = 67;
2207
+ var TAG_CHAR_SHAPE = 68;
2208
+ var TAG_CTRL_HEADER = 71;
2209
+ var TAG_LIST_HEADER = 72;
2210
+ var TAG_TABLE = 77;
2211
+ var TAG_SHAPE_COMPONENT = 76;
2212
+ var TAG_SHAPE_COMPONENT_PICTURE = 85;
2213
+ var TAG_SHAPE_COMPONENT_CONTAINER = 86;
2214
+ var TAG_EQEDIT = 88;
2215
+ var TAG_BIN_DATA = 18;
2216
+ var TAG_DOC_CHAR_SHAPE = 21;
2217
+ var TAG_NUMBERING = 23;
2218
+ var TAG_BULLET = 24;
2219
+ var TAG_DOC_PARA_SHAPE = 25;
2220
+ var TAG_DOC_STYLE = 26;
2221
+ var CHAR_LINE = 0;
2222
+ var CHAR_SECTION_BREAK = 10;
2223
+ var CHAR_PARA = 13;
2224
+ var CHAR_TAB = 9;
2225
+ var CHAR_HYPHEN = 30;
2226
+ var CHAR_NBSP = 31;
2227
+ var CHAR_FIXED_NBSP = 24;
2228
+ var CHAR_FIXED_WIDTH = 25;
2229
+ var FLAG_COMPRESSED = 1 << 0;
2230
+ var FLAG_ENCRYPTED = 1 << 1;
2231
+ var FLAG_DISTRIBUTION = 1 << 2;
2232
+ var FLAG_DRM = 1 << 4;
2233
+ var MAX_RECORDS = 5e5;
2234
+ function readRecords(data) {
2235
+ const records = [];
2236
+ let offset = 0;
2237
+ while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
2238
+ const header = data.readUInt32LE(offset);
2239
+ offset += 4;
2240
+ const tagId = header & 1023;
2241
+ const level = header >> 10 & 1023;
2242
+ let size = header >> 20 & 4095;
2243
+ if (size === 4095) {
2244
+ if (offset + 4 > data.length) break;
2245
+ size = data.readUInt32LE(offset);
2246
+ offset += 4;
2247
+ }
2248
+ if (offset + size > data.length) break;
2249
+ records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
2250
+ offset += size;
2225
2251
  }
2252
+ return records;
2226
2253
  }
2227
- function mimeToExt(mime) {
2228
- if (mime.includes("jpeg")) return "jpg";
2229
- if (mime.includes("png")) return "png";
2230
- if (mime.includes("gif")) return "gif";
2231
- if (mime.includes("bmp")) return "bmp";
2232
- if (mime.includes("tiff")) return "tif";
2233
- if (mime.includes("wmf")) return "wmf";
2234
- if (mime.includes("emf")) return "emf";
2235
- if (mime.includes("svg")) return "svg";
2236
- return "bin";
2237
- }
2238
- function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
2239
- if (depth > MAX_XML_DEPTH) return;
2240
- for (const block of blocks) {
2241
- if (block.type === "image") {
2242
- out.push({ block, ownerCell });
2243
- } else if (block.type === "table" && block.table) {
2244
- for (const row of block.table.cells) {
2245
- for (const cell2 of row) {
2246
- if (cell2.blocks?.length) collectImageBlocks(cell2.blocks, out, cell2, depth + 1);
2247
- }
2248
- }
2254
+ var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
2255
+ function decompressStream(data) {
2256
+ const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
2257
+ if (data.length >= 2 && data[0] === 120) {
2258
+ try {
2259
+ return inflateSync(data, opts);
2260
+ } catch {
2249
2261
  }
2250
2262
  }
2263
+ return inflateRawSync(data, opts);
2251
2264
  }
2252
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
2253
- const images = [];
2254
- let imageIndex = 0;
2255
- const imageBlocks = [];
2256
- collectImageBlocks(blocks, imageBlocks);
2257
- const resolved = /* @__PURE__ */ new Map();
2258
- for (const { block, ownerCell } of imageBlocks) {
2259
- if (block.type !== "image" || !block.text) continue;
2260
- const ref = block.text;
2261
- let img = resolved.get(ref);
2262
- if (img === void 0) {
2263
- img = null;
2264
- const candidates = [
2265
- `BinData/${ref}`,
2266
- `Contents/BinData/${ref}`,
2267
- ref
2268
- // 절대 경로일 수도 있음
2269
- ];
2270
- let resolvedPath = null;
2271
- if (!ref.includes(".")) {
2272
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
2273
- for (const prefix of prefixes) {
2274
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
2275
- if (match.length > 0) {
2276
- resolvedPath = match[0].name;
2277
- break;
2278
- }
2265
+ function parseFileHeader(data) {
2266
+ if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
2267
+ const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
2268
+ return {
2269
+ signature: sig,
2270
+ versionMajor: data[35],
2271
+ flags: data.readUInt32LE(36)
2272
+ };
2273
+ }
2274
+ function readHwpString(data, offset) {
2275
+ if (offset + 2 > data.length) return { value: "", next: data.length };
2276
+ const len = data.readUInt16LE(offset);
2277
+ const start = offset + 2;
2278
+ const end = start + len * 2;
2279
+ if (len === 0 || end > data.length) return { value: "", next: start };
2280
+ return { value: data.subarray(start, end).toString("utf16le"), next: end };
2281
+ }
2282
+ function parseDocInfo(records) {
2283
+ const charShapes = [];
2284
+ const paraShapes = [];
2285
+ const styles = [];
2286
+ const binData = [];
2287
+ const numberings = [];
2288
+ const bullets = [];
2289
+ for (const rec of records) {
2290
+ if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
2291
+ const attr1 = rec.data.readUInt32LE(0);
2292
+ const headType = attr1 >>> 23 & 3;
2293
+ const paraLevel = attr1 >>> 25 & 7;
2294
+ const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
2295
+ paraShapes.push({ headType, paraLevel, numberingId });
2296
+ }
2297
+ if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
2298
+ const attr = rec.data.readUInt16LE(0);
2299
+ const typeBits = attr & 15;
2300
+ if (typeBits === 0) {
2301
+ binData.push({ kind: "link", storageId: 0, extension: "" });
2302
+ } else {
2303
+ const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
2304
+ const { value: extension } = readHwpString(rec.data, 4);
2305
+ binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
2306
+ }
2307
+ }
2308
+ if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
2309
+ const levelFormats = [];
2310
+ const numberFormats = [];
2311
+ const startNumbers = [1, 1, 1, 1, 1, 1, 1];
2312
+ let offset = 0;
2313
+ for (let level = 0; level < 7; level++) {
2314
+ if (offset + 12 > rec.data.length) {
2315
+ levelFormats.push("");
2316
+ numberFormats.push(0);
2317
+ continue;
2279
2318
  }
2319
+ const attr = rec.data.readUInt32LE(offset);
2320
+ numberFormats.push(attr >>> 5 & 15);
2321
+ offset += 12;
2322
+ const { value, next } = readHwpString(rec.data, offset);
2323
+ levelFormats.push(value);
2324
+ offset = next;
2280
2325
  }
2281
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
2282
- for (const path of allCandidates) {
2283
- if (isPathTraversal(path)) continue;
2284
- const file = zip.file(path);
2285
- if (!file) continue;
2286
- try {
2287
- const data = await file.async("uint8array");
2288
- decompressed.total += data.length;
2289
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2290
- const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
2291
- const mimeType = imageExtToMime(ext);
2292
- imageIndex++;
2293
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2294
- img = { filename, data, mimeType };
2295
- images.push(img);
2296
- break;
2297
- } catch (err) {
2298
- if (err instanceof KordocError) throw err;
2326
+ let baseStart = 1;
2327
+ if (offset + 2 <= rec.data.length) {
2328
+ baseStart = rec.data.readUInt16LE(offset) || 1;
2329
+ offset += 2;
2330
+ }
2331
+ for (let level = 0; level < 7; level++) {
2332
+ if (offset + 4 <= rec.data.length) {
2333
+ startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
2334
+ offset += 4;
2335
+ } else {
2336
+ startNumbers[level] = baseStart;
2299
2337
  }
2300
2338
  }
2301
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
2302
- resolved.set(ref, img);
2339
+ numberings.push({ levelFormats, numberFormats, startNumbers });
2303
2340
  }
2304
- if (!img) {
2305
- block.type = "paragraph";
2306
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
2307
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
2308
- continue;
2341
+ if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
2342
+ const code = rec.data.readUInt16LE(12);
2343
+ bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
2309
2344
  }
2310
- block.text = img.filename;
2311
- block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
2312
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
2313
- }
2314
- return images;
2315
- }
2316
-
2317
- // src/hwpx/metadata.ts
2318
- import JSZip from "jszip";
2319
-
2320
- // src/hwpx/zip-sections.ts
2321
- import { inflateRawSync } from "zlib";
2322
- function extractFromBrokenZip(buffer) {
2323
- const data = new Uint8Array(buffer);
2324
- const view = new DataView(buffer);
2325
- let pos = 0;
2326
- const blocks = [];
2327
- const warnings = [
2328
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
2329
- ];
2330
- let totalDecompressed = 0;
2331
- let entryCount = 0;
2332
- let sectionNum = 0;
2333
- const shared = createSectionShared();
2334
- while (pos < data.length - 30) {
2335
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
2336
- pos++;
2337
- while (pos < data.length - 30) {
2338
- if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
2339
- pos++;
2345
+ if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
2346
+ if (rec.data.length >= 50) {
2347
+ const fontSize = rec.data.readUInt32LE(42);
2348
+ const attrFlags = rec.data.readUInt32LE(46);
2349
+ charShapes.push({ fontSize, attrFlags });
2350
+ } else {
2351
+ charShapes.push({ fontSize: 0, attrFlags: 0 });
2340
2352
  }
2341
- continue;
2342
- }
2343
- if (++entryCount > MAX_ZIP_ENTRIES) break;
2344
- const method = view.getUint16(pos + 8, true);
2345
- const compSize = view.getUint32(pos + 18, true);
2346
- const nameLen = view.getUint16(pos + 26, true);
2347
- const extraLen = view.getUint16(pos + 28, true);
2348
- if (nameLen > 1024 || extraLen > 65535) {
2349
- pos += 30 + nameLen + extraLen;
2350
- continue;
2351
- }
2352
- const fileStart = pos + 30 + nameLen + extraLen;
2353
- if (fileStart + compSize > data.length) break;
2354
- if (compSize === 0 && method !== 0) {
2355
- pos = fileStart;
2356
- continue;
2357
- }
2358
- const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
2359
- const name = new TextDecoder().decode(nameBytes);
2360
- if (isPathTraversal(name)) {
2361
- pos = fileStart + compSize;
2362
- continue;
2363
2353
  }
2364
- const fileData = data.slice(fileStart, fileStart + compSize);
2365
- pos = fileStart + compSize;
2366
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
2367
- try {
2368
- let content;
2369
- if (method === 0) {
2370
- content = new TextDecoder().decode(fileData);
2371
- } else if (method === 8) {
2372
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
2373
- content = new TextDecoder().decode(decompressed);
2374
- } else {
2375
- continue;
2354
+ if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
2355
+ try {
2356
+ let offset = 0;
2357
+ const nameLen = rec.data.readUInt16LE(offset);
2358
+ offset += 2;
2359
+ const nameBytes = nameLen * 2;
2360
+ const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
2361
+ offset += nameBytes;
2362
+ let nameKo = "";
2363
+ if (offset + 2 <= rec.data.length) {
2364
+ const nameKoLen = rec.data.readUInt16LE(offset);
2365
+ offset += 2;
2366
+ const nameKoBytes = nameKoLen * 2;
2367
+ if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
2368
+ nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
2369
+ }
2370
+ offset += nameKoBytes;
2371
+ }
2372
+ const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
2373
+ offset += 1;
2374
+ offset += 1;
2375
+ offset += 2;
2376
+ const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2377
+ offset += 2;
2378
+ const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2379
+ styles.push({ name, nameKo, charShapeId, paraShapeId, type });
2380
+ } catch {
2376
2381
  }
2377
- totalDecompressed += content.length * 2;
2378
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
2379
- sectionNum++;
2380
- blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
2381
- } catch {
2382
- continue;
2383
2382
  }
2384
2383
  }
2385
- 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");
2386
- applyPageText(blocks, shared);
2387
- const markdown = blocksToMarkdown(blocks);
2388
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
2384
+ return { charShapes, paraShapes, styles, binData, numberings, bullets };
2389
2385
  }
2390
- async function readKordocLayout(zip) {
2391
- const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2392
- if (!file) return null;
2393
- try {
2394
- const xml = await file.async("text");
2395
- if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2396
- return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2397
- } catch {
2398
- return null;
2399
- }
2386
+ function createParaTextState() {
2387
+ return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2400
2388
  }
2401
- async function resolveSectionPaths(zip) {
2402
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2403
- for (const mp of manifestPaths) {
2404
- const mpLower = mp.toLowerCase();
2405
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
2406
- if (!file) continue;
2407
- const xml = await file.async("text");
2408
- const paths = parseSectionPathsFromManifest(xml);
2409
- if (paths.length > 0) return paths;
2410
- }
2411
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2412
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2389
+ function isExtendedOnlyCtrlChar(ch) {
2390
+ return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2413
2391
  }
2414
- function parseSectionPathsFromManifest(xml) {
2415
- const parser = createXmlParser();
2416
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2417
- const items = doc.getElementsByTagName("opf:item");
2418
- const spine = doc.getElementsByTagName("opf:itemref");
2419
- const idToHref = /* @__PURE__ */ new Map();
2420
- for (let i = 0; i < items.length; i++) {
2421
- const item = items[i];
2422
- const id = item.getAttribute("id") || "";
2423
- const href = normalizeSectionHref(item.getAttribute("href") || "");
2424
- if (id && href) idToHref.set(id, href);
2425
- }
2426
- if (spine.length > 0) {
2427
- const ordered = [];
2428
- for (let i = 0; i < spine.length; i++) {
2429
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
2430
- if (href) ordered.push(href);
2392
+ function appendParaText(state, data, resolveControl) {
2393
+ let result = "";
2394
+ let i = 0;
2395
+ const base = state.text.length;
2396
+ const resolveAt = (byteOffset, extended) => {
2397
+ const ctrlId = data.readUInt32LE(byteOffset);
2398
+ const idx = extended ? state.ctrlIdx : -1;
2399
+ const replacement = resolveControl?.(idx, ctrlId);
2400
+ if (replacement) result += replacement;
2401
+ if (extended) state.ctrlIdx++;
2402
+ };
2403
+ while (i + 1 < data.length) {
2404
+ const ch = data.readUInt16LE(i);
2405
+ i += 2;
2406
+ switch (ch) {
2407
+ // ── char 타입 (2바이트만, 확장 데이터 없음) ──
2408
+ case CHAR_LINE:
2409
+ result += "\n";
2410
+ break;
2411
+ case CHAR_SECTION_BREAK: {
2412
+ if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
2413
+ resolveAt(i + 2, true);
2414
+ i += 16;
2415
+ break;
2416
+ }
2417
+ result += "\n";
2418
+ break;
2419
+ }
2420
+ case CHAR_PARA:
2421
+ break;
2422
+ // 문단 끝
2423
+ case CHAR_HYPHEN:
2424
+ result += "-";
2425
+ break;
2426
+ case CHAR_NBSP:
2427
+ result += " ";
2428
+ break;
2429
+ case CHAR_FIXED_NBSP:
2430
+ result += "\xA0";
2431
+ break;
2432
+ // 진짜 NBSP
2433
+ case CHAR_FIXED_WIDTH:
2434
+ result += " ";
2435
+ break;
2436
+ // 고정폭 공백
2437
+ // ── inline 타입 (2바이트 + 14바이트 확장) ──
2438
+ case CHAR_TAB:
2439
+ result += " ";
2440
+ if (i + 14 <= data.length) i += 14;
2441
+ break;
2442
+ default:
2443
+ if (ch >= 1 && ch <= 31) {
2444
+ const isExtended = isExtendedOnlyCtrlChar(ch);
2445
+ const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
2446
+ if ((isExtended || isInline) && i + 14 <= data.length) {
2447
+ if (ch === 3) {
2448
+ state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
2449
+ } else if (ch === 4) {
2450
+ const open = state.fieldStack.pop();
2451
+ if (open) {
2452
+ state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
2453
+ }
2454
+ }
2455
+ resolveAt(i, isExtended);
2456
+ i += 14;
2457
+ }
2458
+ } else if (ch >= 32) {
2459
+ if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
2460
+ const lo = data.readUInt16LE(i);
2461
+ if (lo >= 56320 && lo <= 57343) {
2462
+ i += 2;
2463
+ const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
2464
+ result += String.fromCodePoint(codePoint);
2465
+ break;
2466
+ }
2467
+ }
2468
+ result += String.fromCharCode(ch);
2469
+ }
2470
+ break;
2431
2471
  }
2432
- if (ordered.length > 0) return ordered;
2433
2472
  }
2434
- return Array.from(idToHref.values()).sort(compareSectionPaths);
2473
+ state.text += result;
2474
+ }
2475
+ function extractEquationText(data) {
2476
+ if (data.length < 6) return null;
2477
+ const scriptLength = data.readUInt16LE(4);
2478
+ const scriptStart = 6;
2479
+ const scriptEnd = scriptStart + scriptLength * 2;
2480
+ if (scriptLength <= 0 || scriptEnd > data.length) return null;
2481
+ const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
2482
+ return equation || null;
2435
2483
  }
2436
2484
 
2437
- // src/hwpx/metadata.ts
2438
- async function extractHwpxMetadata(zip, metadata, decompressed) {
2485
+ // src/hwp5/images.ts
2486
+ function detectImageMime(data) {
2487
+ if (data.length < 4) return null;
2488
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
2489
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
2490
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
2491
+ if (data[0] === 66 && data[1] === 77) return "image/bmp";
2492
+ if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
2493
+ if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
2494
+ return null;
2495
+ }
2496
+ function normalizeBinPayload(data) {
2497
+ if (detectImageMime(data)) return data;
2439
2498
  try {
2440
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
2441
- for (const mp of metaPaths) {
2442
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
2443
- if (!file) continue;
2444
- const xml = await file.async("text");
2445
- if (decompressed) {
2446
- decompressed.total += xml.length * 2;
2447
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2448
- }
2449
- parseDublinCoreMetadata(xml, metadata);
2450
- if (metadata.title || metadata.author) return;
2451
- }
2499
+ const inflated = decompressStream(data);
2500
+ if (inflated.length > 0) return inflated;
2452
2501
  } catch {
2453
2502
  }
2503
+ return data;
2454
2504
  }
2455
- function parseDublinCoreMetadata(xml, metadata) {
2456
- const parser = createXmlParser();
2457
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2458
- if (!doc.documentElement) return;
2459
- const getText = (tagNames) => {
2460
- for (const tag of tagNames) {
2461
- const els = doc.getElementsByTagName(tag);
2462
- if (els.length > 0) {
2463
- const text = els[0].textContent?.trim();
2464
- if (text) return text;
2505
+ var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
2506
+ function collectImageBlocks(blocks, out) {
2507
+ for (const b of blocks) {
2508
+ if (b.type === "image") out.push(b);
2509
+ if (b.table) {
2510
+ for (const row of b.table.cells) {
2511
+ for (const cell2 of row) {
2512
+ if (cell2.blocks) collectImageBlocks(cell2.blocks, out);
2513
+ }
2465
2514
  }
2466
2515
  }
2467
- return void 0;
2468
- };
2469
- metadata.title = metadata.title || getText(["dc:title", "title"]);
2470
- metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
2471
- metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
2472
- metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
2473
- metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
2474
- const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
2475
- if (keywords && !metadata.keywords) {
2476
- metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
2516
+ if (b.children) collectImageBlocks(b.children, out);
2477
2517
  }
2478
2518
  }
2479
- async function extractHwpxMetadataOnly(buffer) {
2480
- let zip;
2481
- try {
2482
- zip = await JSZip.loadAsync(buffer);
2483
- } catch {
2484
- throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2519
+ function forEachTableCell(blocks, fn) {
2520
+ for (const b of blocks) {
2521
+ if (b.table) {
2522
+ for (const row of b.table.cells) {
2523
+ for (const cell2 of row) {
2524
+ fn(cell2);
2525
+ if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
2526
+ }
2527
+ }
2528
+ }
2529
+ if (b.children) forEachTableCell(b.children, fn);
2485
2530
  }
2486
- const metadata = {};
2487
- await extractHwpxMetadata(zip, metadata);
2488
- const sectionPaths = await resolveSectionPaths(zip);
2489
- metadata.pageCount = sectionPaths.length;
2490
- return metadata;
2491
2531
  }
2492
-
2493
- // src/hwpx/parser.ts
2494
- async function parseHwpxDocument(buffer, options) {
2495
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
2496
- let zip;
2497
- try {
2498
- zip = await JSZip2.loadAsync(buffer);
2499
- } catch {
2500
- return extractFromBrokenZip(buffer);
2501
- }
2502
- const actualEntryCount = Object.keys(zip.files).length;
2503
- if (actualEntryCount > MAX_ZIP_ENTRIES) {
2504
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2505
- }
2506
- const manifestFile = zip.file("META-INF/manifest.xml");
2507
- if (manifestFile) {
2508
- const manifestXml = await manifestFile.async("text");
2509
- if (isEncryptedHwpx(manifestXml)) {
2510
- if (isComFallbackAvailable() && options?.filePath) {
2511
- const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
2512
- if (pages.some((p) => p && p.trim().length > 0)) {
2513
- return comResultToParseResult(pages, pageCount, warnings2);
2532
+ var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
2533
+ function resolveCellImageSentinels(blocks, renamed) {
2534
+ forEachTableCell(blocks, (cell2) => {
2535
+ if (!cell2.text.includes("hwp5bin:")) return;
2536
+ cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
2537
+ const filename = renamed.get(Number(idStr));
2538
+ return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
2539
+ });
2540
+ });
2541
+ }
2542
+ function resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced) {
2543
+ const imageBlocks = [];
2544
+ collectImageBlocks(blocks, imageBlocks);
2545
+ const images = [];
2546
+ const renamed = /* @__PURE__ */ new Map();
2547
+ const resolved = /* @__PURE__ */ new Map();
2548
+ let imageIndex = 0;
2549
+ for (const block of imageBlocks) {
2550
+ if (!block.text) continue;
2551
+ const storageId = parseInt(block.text, 10);
2552
+ if (isNaN(storageId)) continue;
2553
+ let img = resolved.get(storageId);
2554
+ if (img === void 0) {
2555
+ const bin = binDataMap.get(storageId);
2556
+ if (!bin) {
2557
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
2558
+ resolved.set(storageId, null);
2559
+ } else {
2560
+ const mime = detectImageMime(bin.data);
2561
+ if (!mime) {
2562
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
2563
+ resolved.set(storageId, null);
2564
+ } else {
2565
+ imageIndex++;
2566
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
2567
+ img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
2568
+ resolved.set(storageId, img);
2569
+ images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
2570
+ renamed.set(storageId, img.filename);
2514
2571
  }
2515
2572
  }
2516
- 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.");
2573
+ img = resolved.get(storageId);
2574
+ }
2575
+ if (!img) {
2576
+ const bin = binDataMap.get(storageId);
2577
+ block.type = "paragraph";
2578
+ block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
2579
+ continue;
2517
2580
  }
2581
+ block.text = img.filename;
2582
+ block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
2518
2583
  }
2519
- const decompressed = { total: 0 };
2520
- const metadata = {};
2521
- await extractHwpxMetadata(zip, metadata, decompressed);
2522
- const styleMap = await extractHwpxStyles(zip, decompressed);
2523
- const warnings = [];
2524
- const sectionPaths = await resolveSectionPaths(zip);
2525
- if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2526
- metadata.pageCount = sectionPaths.length;
2527
- const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
2528
- const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
2529
- const blocks = [];
2530
- const shared = createSectionShared();
2531
- shared.kordocLayout = await readKordocLayout(zip);
2532
- let parsedSections = 0;
2533
- for (let si = 0; si < sectionPaths.length; si++) {
2534
- if (pageFilter && !pageFilter.has(si + 1)) continue;
2535
- const file = zip.file(sectionPaths[si]);
2536
- if (!file) continue;
2537
- try {
2538
- const xml = await file.async("text");
2539
- decompressed.total += xml.length * 2;
2540
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2541
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
2542
- parsedSections++;
2543
- options?.onProgress?.(parsedSections, totalTarget);
2544
- } catch (secErr) {
2545
- if (secErr instanceof KordocError) throw secErr;
2546
- 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" });
2584
+ if (sweepUnreferenced) {
2585
+ for (const [storageId, bin] of [...binDataMap.entries()].sort((a, b) => a[0] - b[0])) {
2586
+ if (resolved.has(storageId)) continue;
2587
+ const mime = detectImageMime(bin.data);
2588
+ if (!mime) continue;
2589
+ imageIndex++;
2590
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
2591
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
2592
+ const data = new Uint8Array(bin.data);
2593
+ images.push({ filename, data, mimeType: mime });
2594
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType: mime, filename: bin.name } });
2547
2595
  }
2548
2596
  }
2549
- applyPageText(blocks, shared);
2550
- const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
2551
- detectHwpxHeadings(blocks, styleMap);
2552
- const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
2553
- const markdown = blocksToMarkdown(blocks);
2554
- 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 };
2597
+ resolveCellImageSentinels(blocks, renamed);
2598
+ return images;
2555
2599
  }
2556
-
2557
- // src/hwp5/record.ts
2558
- import { inflateRawSync as inflateRawSync2, inflateSync } from "zlib";
2559
- var TAG_PARA_HEADER = 66;
2560
- var TAG_PARA_TEXT = 67;
2561
- var TAG_CHAR_SHAPE = 68;
2562
- var TAG_CTRL_HEADER = 71;
2563
- var TAG_LIST_HEADER = 72;
2564
- var TAG_TABLE = 77;
2565
- var TAG_SHAPE_COMPONENT = 76;
2566
- var TAG_SHAPE_COMPONENT_PICTURE = 85;
2567
- var TAG_SHAPE_COMPONENT_CONTAINER = 86;
2568
- var TAG_EQEDIT = 88;
2569
- var TAG_BIN_DATA = 18;
2570
- var TAG_DOC_CHAR_SHAPE = 21;
2571
- var TAG_NUMBERING = 23;
2572
- var TAG_BULLET = 24;
2573
- var TAG_DOC_PARA_SHAPE = 25;
2574
- var TAG_DOC_STYLE = 26;
2575
- var CHAR_LINE = 0;
2576
- var CHAR_SECTION_BREAK = 10;
2577
- var CHAR_PARA = 13;
2578
- var CHAR_TAB = 9;
2579
- var CHAR_HYPHEN = 30;
2580
- var CHAR_NBSP = 31;
2581
- var CHAR_FIXED_NBSP = 24;
2582
- var CHAR_FIXED_WIDTH = 25;
2583
- var FLAG_COMPRESSED = 1 << 0;
2584
- var FLAG_ENCRYPTED = 1 << 1;
2585
- var FLAG_DISTRIBUTION = 1 << 2;
2586
- var FLAG_DRM = 1 << 4;
2587
- var MAX_RECORDS = 5e5;
2588
- function readRecords(data) {
2589
- const records = [];
2590
- let offset = 0;
2591
- while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
2592
- const header = data.readUInt32LE(offset);
2593
- offset += 4;
2594
- const tagId = header & 1023;
2595
- const level = header >> 10 & 1023;
2596
- let size = header >> 20 & 4095;
2597
- if (size === 4095) {
2598
- if (offset + 4 > data.length) break;
2599
- size = data.readUInt32LE(offset);
2600
- offset += 4;
2600
+ function extractHwp5Images(fileIndex, blocks, warnings, sweepUnreferenced) {
2601
+ const binDataMap = /* @__PURE__ */ new Map();
2602
+ if (fileIndex) {
2603
+ for (const entry of fileIndex) {
2604
+ if (!entry?.name || !entry.content) continue;
2605
+ const match = entry.name.match(BIN_ENTRY_RE);
2606
+ if (!match) continue;
2607
+ const idx = parseInt(match[1], 16);
2608
+ const data = normalizeBinPayload(Buffer.from(entry.content));
2609
+ binDataMap.set(idx, { data, name: entry.name });
2601
2610
  }
2602
- if (offset + size > data.length) break;
2603
- records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
2604
- offset += size;
2605
2611
  }
2606
- return records;
2612
+ if (binDataMap.size === 0) {
2613
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
2614
+ return [];
2615
+ }
2616
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
2607
2617
  }
2608
- var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
2609
- function decompressStream(data) {
2610
- const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
2611
- if (data.length >= 2 && data[0] === 120) {
2612
- try {
2613
- return inflateSync(data, opts);
2614
- } catch {
2615
- }
2618
+ function extractHwp5ImagesLenient(lcfb, blocks, warnings, sweepUnreferenced) {
2619
+ const binDataMap = /* @__PURE__ */ new Map();
2620
+ const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
2621
+ for (const e of lcfb.entries()) {
2622
+ const match = e.name.match(binRe);
2623
+ if (!match) continue;
2624
+ const idx = parseInt(match[1], 16);
2625
+ const raw = lcfb.findStream(e.name);
2626
+ if (!raw) continue;
2627
+ binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
2628
+ }
2629
+ if (binDataMap.size === 0) {
2630
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
2631
+ return [];
2616
2632
  }
2617
- return inflateRawSync2(data, opts);
2633
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
2618
2634
  }
2619
- function parseFileHeader(data) {
2620
- if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
2621
- const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
2622
- return {
2623
- signature: sig,
2624
- versionMajor: data[35],
2625
- flags: data.readUInt32LE(36)
2626
- };
2635
+
2636
+ // src/hwpx/images.ts
2637
+ function imageExtToMime(ext) {
2638
+ switch (ext.toLowerCase()) {
2639
+ case "jpg":
2640
+ case "jpeg":
2641
+ return "image/jpeg";
2642
+ case "png":
2643
+ return "image/png";
2644
+ case "gif":
2645
+ return "image/gif";
2646
+ case "bmp":
2647
+ return "image/bmp";
2648
+ case "tif":
2649
+ case "tiff":
2650
+ return "image/tiff";
2651
+ case "wmf":
2652
+ return "image/wmf";
2653
+ case "emf":
2654
+ return "image/emf";
2655
+ case "svg":
2656
+ return "image/svg+xml";
2657
+ default:
2658
+ return "application/octet-stream";
2659
+ }
2627
2660
  }
2628
- function readHwpString(data, offset) {
2629
- if (offset + 2 > data.length) return { value: "", next: data.length };
2630
- const len = data.readUInt16LE(offset);
2631
- const start = offset + 2;
2632
- const end = start + len * 2;
2633
- if (len === 0 || end > data.length) return { value: "", next: start };
2634
- return { value: data.subarray(start, end).toString("utf16le"), next: end };
2661
+ function mimeToExt(mime) {
2662
+ if (mime.includes("jpeg")) return "jpg";
2663
+ if (mime.includes("png")) return "png";
2664
+ if (mime.includes("gif")) return "gif";
2665
+ if (mime.includes("bmp")) return "bmp";
2666
+ if (mime.includes("tiff")) return "tif";
2667
+ if (mime.includes("wmf")) return "wmf";
2668
+ if (mime.includes("emf")) return "emf";
2669
+ if (mime.includes("svg")) return "svg";
2670
+ return "bin";
2635
2671
  }
2636
- function parseDocInfo(records) {
2637
- const charShapes = [];
2638
- const paraShapes = [];
2639
- const styles = [];
2640
- const binData = [];
2641
- const numberings = [];
2642
- const bullets = [];
2643
- for (const rec of records) {
2644
- if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
2645
- const attr1 = rec.data.readUInt32LE(0);
2646
- const headType = attr1 >>> 23 & 3;
2647
- const paraLevel = attr1 >>> 25 & 7;
2648
- const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
2649
- paraShapes.push({ headType, paraLevel, numberingId });
2650
- }
2651
- if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
2652
- const attr = rec.data.readUInt16LE(0);
2653
- const typeBits = attr & 15;
2654
- if (typeBits === 0) {
2655
- binData.push({ kind: "link", storageId: 0, extension: "" });
2656
- } else {
2657
- const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
2658
- const { value: extension } = readHwpString(rec.data, 4);
2659
- binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
2672
+ function collectImageBlocks2(blocks, out, ownerCell, depth = 0) {
2673
+ if (depth > MAX_XML_DEPTH) return;
2674
+ for (const block of blocks) {
2675
+ if (block.type === "image") {
2676
+ out.push({ block, ownerCell });
2677
+ } else if (block.type === "table" && block.table) {
2678
+ for (const row of block.table.cells) {
2679
+ for (const cell2 of row) {
2680
+ if (cell2.blocks?.length) collectImageBlocks2(cell2.blocks, out, cell2, depth + 1);
2681
+ }
2660
2682
  }
2661
2683
  }
2662
- if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
2663
- const levelFormats = [];
2664
- const numberFormats = [];
2665
- const startNumbers = [1, 1, 1, 1, 1, 1, 1];
2666
- let offset = 0;
2667
- for (let level = 0; level < 7; level++) {
2668
- if (offset + 12 > rec.data.length) {
2669
- levelFormats.push("");
2670
- numberFormats.push(0);
2671
- continue;
2684
+ }
2685
+ }
2686
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings, sweepUnreferenced) {
2687
+ const images = [];
2688
+ let imageIndex = 0;
2689
+ const usedPaths = /* @__PURE__ */ new Set();
2690
+ const imageBlocks = [];
2691
+ collectImageBlocks2(blocks, imageBlocks);
2692
+ const resolved = /* @__PURE__ */ new Map();
2693
+ for (const { block, ownerCell } of imageBlocks) {
2694
+ if (block.type !== "image" || !block.text) continue;
2695
+ const ref = block.text;
2696
+ let img = resolved.get(ref);
2697
+ if (img === void 0) {
2698
+ img = null;
2699
+ const candidates = [
2700
+ `BinData/${ref}`,
2701
+ `Contents/BinData/${ref}`,
2702
+ ref
2703
+ // 절대 경로일 수도 있음
2704
+ ];
2705
+ let resolvedPath = null;
2706
+ if (!ref.includes(".")) {
2707
+ const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
2708
+ for (const prefix of prefixes) {
2709
+ const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
2710
+ if (match.length > 0) {
2711
+ resolvedPath = match[0].name;
2712
+ break;
2713
+ }
2672
2714
  }
2673
- const attr = rec.data.readUInt32LE(offset);
2674
- numberFormats.push(attr >>> 5 & 15);
2675
- offset += 12;
2676
- const { value, next } = readHwpString(rec.data, offset);
2677
- levelFormats.push(value);
2678
- offset = next;
2679
2715
  }
2680
- let baseStart = 1;
2681
- if (offset + 2 <= rec.data.length) {
2682
- baseStart = rec.data.readUInt16LE(offset) || 1;
2683
- offset += 2;
2716
+ const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
2717
+ for (const path of allCandidates) {
2718
+ if (isPathTraversal(path)) continue;
2719
+ const file = zip.file(path);
2720
+ if (!file) continue;
2721
+ try {
2722
+ const data = await file.async("uint8array");
2723
+ decompressed.total += data.length;
2724
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2725
+ const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
2726
+ const mimeType = imageExtToMime(ext);
2727
+ imageIndex++;
2728
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2729
+ img = { filename, data, mimeType };
2730
+ images.push(img);
2731
+ usedPaths.add(path);
2732
+ break;
2733
+ } catch (err) {
2734
+ if (err instanceof KordocError) throw err;
2735
+ }
2684
2736
  }
2685
- for (let level = 0; level < 7; level++) {
2686
- if (offset + 4 <= rec.data.length) {
2687
- startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
2688
- offset += 4;
2689
- } else {
2690
- startNumbers[level] = baseStart;
2737
+ if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
2738
+ resolved.set(ref, img);
2739
+ }
2740
+ if (!img) {
2741
+ block.type = "paragraph";
2742
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
2743
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
2744
+ continue;
2745
+ }
2746
+ block.text = img.filename;
2747
+ block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
2748
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
2749
+ }
2750
+ if (sweepUnreferenced) {
2751
+ const binEntries = zip.file(/(?:^|\/)BinData\//i);
2752
+ for (const file of binEntries) {
2753
+ if (file.dir || usedPaths.has(file.name) || isPathTraversal(file.name)) continue;
2754
+ try {
2755
+ const data = await file.async("uint8array");
2756
+ decompressed.total += data.length;
2757
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2758
+ const ext = file.name.includes(".") ? file.name.split(".").pop() || "" : "";
2759
+ let mimeType = imageExtToMime(ext);
2760
+ if (mimeType === "application/octet-stream") {
2761
+ const sniffed = detectImageMime(data);
2762
+ if (!sniffed) continue;
2763
+ mimeType = sniffed;
2691
2764
  }
2765
+ imageIndex++;
2766
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2767
+ const img = { filename, data, mimeType };
2768
+ images.push(img);
2769
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType, filename: file.name } });
2770
+ } catch (err) {
2771
+ if (err instanceof KordocError) throw err;
2772
+ warnings?.push({ message: `\uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328: ${file.name}`, code: "SKIPPED_IMAGE" });
2773
+ }
2774
+ }
2775
+ }
2776
+ return images;
2777
+ }
2778
+
2779
+ // src/hwpx/metadata.ts
2780
+ import JSZip from "jszip";
2781
+
2782
+ // src/hwpx/zip-sections.ts
2783
+ import { inflateRawSync as inflateRawSync2 } from "zlib";
2784
+ function extractFromBrokenZip(buffer) {
2785
+ const data = new Uint8Array(buffer);
2786
+ const view = new DataView(buffer);
2787
+ let pos = 0;
2788
+ const blocks = [];
2789
+ const warnings = [
2790
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
2791
+ ];
2792
+ let totalDecompressed = 0;
2793
+ let entryCount = 0;
2794
+ let sectionNum = 0;
2795
+ const shared = createSectionShared();
2796
+ while (pos < data.length - 30) {
2797
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
2798
+ pos++;
2799
+ while (pos < data.length - 30) {
2800
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
2801
+ pos++;
2802
+ }
2803
+ continue;
2804
+ }
2805
+ if (++entryCount > MAX_ZIP_ENTRIES) break;
2806
+ const method = view.getUint16(pos + 8, true);
2807
+ const compSize = view.getUint32(pos + 18, true);
2808
+ const nameLen = view.getUint16(pos + 26, true);
2809
+ const extraLen = view.getUint16(pos + 28, true);
2810
+ if (nameLen > 1024 || extraLen > 65535) {
2811
+ pos += 30 + nameLen + extraLen;
2812
+ continue;
2813
+ }
2814
+ const fileStart = pos + 30 + nameLen + extraLen;
2815
+ if (fileStart + compSize > data.length) break;
2816
+ if (compSize === 0 && method !== 0) {
2817
+ pos = fileStart;
2818
+ continue;
2819
+ }
2820
+ const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
2821
+ const name = new TextDecoder().decode(nameBytes);
2822
+ if (isPathTraversal(name)) {
2823
+ pos = fileStart + compSize;
2824
+ continue;
2825
+ }
2826
+ const fileData = data.slice(fileStart, fileStart + compSize);
2827
+ pos = fileStart + compSize;
2828
+ if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
2829
+ try {
2830
+ let content;
2831
+ if (method === 0) {
2832
+ content = new TextDecoder().decode(fileData);
2833
+ } else if (method === 8) {
2834
+ const decompressed = inflateRawSync2(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
2835
+ content = new TextDecoder().decode(decompressed);
2836
+ } else {
2837
+ continue;
2692
2838
  }
2693
- numberings.push({ levelFormats, numberFormats, startNumbers });
2839
+ totalDecompressed += content.length * 2;
2840
+ if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
2841
+ sectionNum++;
2842
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
2843
+ } catch {
2844
+ continue;
2694
2845
  }
2695
- if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
2696
- const code = rec.data.readUInt16LE(12);
2697
- bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
2846
+ }
2847
+ 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");
2848
+ applyPageText(blocks, shared);
2849
+ const markdown = blocksToMarkdown(blocks);
2850
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
2851
+ }
2852
+ async function readKordocLayout(zip) {
2853
+ const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2854
+ if (!file) return null;
2855
+ try {
2856
+ const xml = await file.async("text");
2857
+ if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2858
+ return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2859
+ } catch {
2860
+ return null;
2861
+ }
2862
+ }
2863
+ async function resolveSectionPaths(zip) {
2864
+ const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2865
+ for (const mp of manifestPaths) {
2866
+ const mpLower = mp.toLowerCase();
2867
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
2868
+ if (!file) continue;
2869
+ const xml = await file.async("text");
2870
+ const paths = parseSectionPathsFromManifest(xml);
2871
+ if (paths.length > 0) return paths;
2872
+ }
2873
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2874
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2875
+ }
2876
+ function parseSectionPathsFromManifest(xml) {
2877
+ const parser = createXmlParser();
2878
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2879
+ const items = doc.getElementsByTagName("opf:item");
2880
+ const spine = doc.getElementsByTagName("opf:itemref");
2881
+ const idToHref = /* @__PURE__ */ new Map();
2882
+ for (let i = 0; i < items.length; i++) {
2883
+ const item = items[i];
2884
+ const id = item.getAttribute("id") || "";
2885
+ const href = normalizeSectionHref(item.getAttribute("href") || "");
2886
+ if (id && href) idToHref.set(id, href);
2887
+ }
2888
+ if (spine.length > 0) {
2889
+ const ordered = [];
2890
+ for (let i = 0; i < spine.length; i++) {
2891
+ const href = idToHref.get(spine[i].getAttribute("idref") || "");
2892
+ if (href) ordered.push(href);
2698
2893
  }
2699
- if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
2700
- if (rec.data.length >= 50) {
2701
- const fontSize = rec.data.readUInt32LE(42);
2702
- const attrFlags = rec.data.readUInt32LE(46);
2703
- charShapes.push({ fontSize, attrFlags });
2704
- } else {
2705
- charShapes.push({ fontSize: 0, attrFlags: 0 });
2894
+ if (ordered.length > 0) return ordered;
2895
+ }
2896
+ return Array.from(idToHref.values()).sort(compareSectionPaths);
2897
+ }
2898
+
2899
+ // src/hwpx/metadata.ts
2900
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
2901
+ try {
2902
+ const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
2903
+ for (const mp of metaPaths) {
2904
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
2905
+ if (!file) continue;
2906
+ const xml = await file.async("text");
2907
+ if (decompressed) {
2908
+ decompressed.total += xml.length * 2;
2909
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2706
2910
  }
2911
+ parseDublinCoreMetadata(xml, metadata);
2912
+ if (metadata.title || metadata.author) return;
2707
2913
  }
2708
- if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
2709
- try {
2710
- let offset = 0;
2711
- const nameLen = rec.data.readUInt16LE(offset);
2712
- offset += 2;
2713
- const nameBytes = nameLen * 2;
2714
- const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
2715
- offset += nameBytes;
2716
- let nameKo = "";
2717
- if (offset + 2 <= rec.data.length) {
2718
- const nameKoLen = rec.data.readUInt16LE(offset);
2719
- offset += 2;
2720
- const nameKoBytes = nameKoLen * 2;
2721
- if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
2722
- nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
2723
- }
2724
- offset += nameKoBytes;
2725
- }
2726
- const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
2727
- offset += 1;
2728
- offset += 1;
2729
- offset += 2;
2730
- const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2731
- offset += 2;
2732
- const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2733
- styles.push({ name, nameKo, charShapeId, paraShapeId, type });
2734
- } catch {
2914
+ } catch {
2915
+ }
2916
+ }
2917
+ function parseDublinCoreMetadata(xml, metadata) {
2918
+ const parser = createXmlParser();
2919
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2920
+ if (!doc.documentElement) return;
2921
+ const getText = (tagNames) => {
2922
+ for (const tag of tagNames) {
2923
+ const els = doc.getElementsByTagName(tag);
2924
+ if (els.length > 0) {
2925
+ const text = els[0].textContent?.trim();
2926
+ if (text) return text;
2735
2927
  }
2736
2928
  }
2929
+ return void 0;
2930
+ };
2931
+ metadata.title = metadata.title || getText(["dc:title", "title"]);
2932
+ metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
2933
+ metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
2934
+ metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
2935
+ metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
2936
+ const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
2937
+ if (keywords && !metadata.keywords) {
2938
+ metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
2737
2939
  }
2738
- return { charShapes, paraShapes, styles, binData, numberings, bullets };
2739
2940
  }
2740
- function createParaTextState() {
2741
- return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2742
- }
2743
- function isExtendedOnlyCtrlChar(ch) {
2744
- return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2941
+ async function extractHwpxMetadataOnly(buffer) {
2942
+ let zip;
2943
+ try {
2944
+ zip = await JSZip.loadAsync(buffer);
2945
+ } catch {
2946
+ throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2947
+ }
2948
+ const metadata = {};
2949
+ await extractHwpxMetadata(zip, metadata);
2950
+ const sectionPaths = await resolveSectionPaths(zip);
2951
+ metadata.pageCount = sectionPaths.length;
2952
+ return metadata;
2745
2953
  }
2746
- function appendParaText(state, data, resolveControl) {
2747
- let result = "";
2748
- let i = 0;
2749
- const base = state.text.length;
2750
- const resolveAt = (byteOffset, extended) => {
2751
- const ctrlId = data.readUInt32LE(byteOffset);
2752
- const idx = extended ? state.ctrlIdx : -1;
2753
- const replacement = resolveControl?.(idx, ctrlId);
2754
- if (replacement) result += replacement;
2755
- if (extended) state.ctrlIdx++;
2756
- };
2757
- while (i + 1 < data.length) {
2758
- const ch = data.readUInt16LE(i);
2759
- i += 2;
2760
- switch (ch) {
2761
- // ── char 타입 (2바이트만, 확장 데이터 없음) ──
2762
- case CHAR_LINE:
2763
- result += "\n";
2764
- break;
2765
- case CHAR_SECTION_BREAK: {
2766
- if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
2767
- resolveAt(i + 2, true);
2768
- i += 16;
2769
- break;
2954
+
2955
+ // src/hwpx/parser.ts
2956
+ async function parseHwpxDocument(buffer, options) {
2957
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
2958
+ let zip;
2959
+ try {
2960
+ zip = await JSZip2.loadAsync(buffer);
2961
+ } catch {
2962
+ return extractFromBrokenZip(buffer);
2963
+ }
2964
+ const actualEntryCount = Object.keys(zip.files).length;
2965
+ if (actualEntryCount > MAX_ZIP_ENTRIES) {
2966
+ throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2967
+ }
2968
+ const manifestFile = zip.file("META-INF/manifest.xml");
2969
+ if (manifestFile) {
2970
+ const manifestXml = await manifestFile.async("text");
2971
+ if (isEncryptedHwpx(manifestXml)) {
2972
+ if (isComFallbackAvailable() && options?.filePath) {
2973
+ const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
2974
+ if (pages.some((p) => p && p.trim().length > 0)) {
2975
+ return comResultToParseResult(pages, pageCount, warnings2);
2770
2976
  }
2771
- result += "\n";
2772
- break;
2773
2977
  }
2774
- case CHAR_PARA:
2775
- break;
2776
- // 문단 끝
2777
- case CHAR_HYPHEN:
2778
- result += "-";
2779
- break;
2780
- case CHAR_NBSP:
2781
- result += " ";
2782
- break;
2783
- case CHAR_FIXED_NBSP:
2784
- result += "\xA0";
2785
- break;
2786
- // 진짜 NBSP
2787
- case CHAR_FIXED_WIDTH:
2788
- result += " ";
2789
- break;
2790
- // 고정폭 공백
2791
- // ── inline 타입 (2바이트 + 14바이트 확장) ──
2792
- case CHAR_TAB:
2793
- result += " ";
2794
- if (i + 14 <= data.length) i += 14;
2795
- break;
2796
- default:
2797
- if (ch >= 1 && ch <= 31) {
2798
- const isExtended = isExtendedOnlyCtrlChar(ch);
2799
- const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
2800
- if ((isExtended || isInline) && i + 14 <= data.length) {
2801
- if (ch === 3) {
2802
- state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
2803
- } else if (ch === 4) {
2804
- const open = state.fieldStack.pop();
2805
- if (open) {
2806
- state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
2807
- }
2808
- }
2809
- resolveAt(i, isExtended);
2810
- i += 14;
2811
- }
2812
- } else if (ch >= 32) {
2813
- if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
2814
- const lo = data.readUInt16LE(i);
2815
- if (lo >= 56320 && lo <= 57343) {
2816
- i += 2;
2817
- const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
2818
- result += String.fromCodePoint(codePoint);
2819
- break;
2820
- }
2821
- }
2822
- result += String.fromCharCode(ch);
2823
- }
2824
- break;
2978
+ 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.");
2979
+ }
2980
+ }
2981
+ const decompressed = { total: 0 };
2982
+ const metadata = {};
2983
+ await extractHwpxMetadata(zip, metadata, decompressed);
2984
+ const styleMap = await extractHwpxStyles(zip, decompressed);
2985
+ const warnings = [];
2986
+ const sectionPaths = await resolveSectionPaths(zip);
2987
+ if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2988
+ metadata.pageCount = sectionPaths.length;
2989
+ const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
2990
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
2991
+ const blocks = [];
2992
+ const shared = createSectionShared();
2993
+ shared.kordocLayout = await readKordocLayout(zip);
2994
+ let parsedSections = 0;
2995
+ for (let si = 0; si < sectionPaths.length; si++) {
2996
+ if (pageFilter && !pageFilter.has(si + 1)) continue;
2997
+ const file = zip.file(sectionPaths[si]);
2998
+ if (!file) continue;
2999
+ try {
3000
+ const xml = await file.async("text");
3001
+ decompressed.total += xml.length * 2;
3002
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3003
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
3004
+ parsedSections++;
3005
+ options?.onProgress?.(parsedSections, totalTarget);
3006
+ } catch (secErr) {
3007
+ if (secErr instanceof KordocError) throw secErr;
3008
+ 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" });
2825
3009
  }
2826
3010
  }
2827
- state.text += result;
2828
- }
2829
- function extractEquationText(data) {
2830
- if (data.length < 6) return null;
2831
- const scriptLength = data.readUInt16LE(4);
2832
- const scriptStart = 6;
2833
- const scriptEnd = scriptStart + scriptLength * 2;
2834
- if (scriptLength <= 0 || scriptEnd > data.length) return null;
2835
- const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
2836
- return equation || null;
3011
+ applyPageText(blocks, shared);
3012
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings, !pageFilter);
3013
+ detectHwpxHeadings(blocks, styleMap);
3014
+ const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3015
+ const markdown = blocksToMarkdown(blocks);
3016
+ 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 };
2837
3017
  }
2838
3018
 
2839
3019
  // src/hwp5/numbering.ts
@@ -3014,282 +3194,6 @@ function expandNumberingFormat(formatStr, counters, numbering) {
3014
3194
  return result;
3015
3195
  }
3016
3196
 
3017
- // src/hwp5/images.ts
3018
- function detectImageMime(data) {
3019
- if (data.length < 4) return null;
3020
- if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
3021
- if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3022
- if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
3023
- if (data[0] === 66 && data[1] === 77) return "image/bmp";
3024
- if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
3025
- if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
3026
- return null;
3027
- }
3028
- function normalizeBinPayload(data) {
3029
- if (detectImageMime(data)) return data;
3030
- try {
3031
- const inflated = decompressStream(data);
3032
- if (inflated.length > 0) return inflated;
3033
- } catch {
3034
- }
3035
- return data;
3036
- }
3037
- var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
3038
- function collectImageBlocks2(blocks, out) {
3039
- for (const b of blocks) {
3040
- if (b.type === "image") out.push(b);
3041
- if (b.table) {
3042
- for (const row of b.table.cells) {
3043
- for (const cell2 of row) {
3044
- if (cell2.blocks) collectImageBlocks2(cell2.blocks, out);
3045
- }
3046
- }
3047
- }
3048
- if (b.children) collectImageBlocks2(b.children, out);
3049
- }
3050
- }
3051
- function forEachTableCell(blocks, fn) {
3052
- for (const b of blocks) {
3053
- if (b.table) {
3054
- for (const row of b.table.cells) {
3055
- for (const cell2 of row) {
3056
- fn(cell2);
3057
- if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
3058
- }
3059
- }
3060
- }
3061
- if (b.children) forEachTableCell(b.children, fn);
3062
- }
3063
- }
3064
- var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
3065
- function resolveCellImageSentinels(blocks, renamed) {
3066
- forEachTableCell(blocks, (cell2) => {
3067
- if (!cell2.text.includes("hwp5bin:")) return;
3068
- cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
3069
- const filename = renamed.get(Number(idStr));
3070
- return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
3071
- });
3072
- });
3073
- }
3074
- function resolveImageBlocks(binDataMap, blocks, warnings) {
3075
- const imageBlocks = [];
3076
- collectImageBlocks2(blocks, imageBlocks);
3077
- if (imageBlocks.length === 0) return [];
3078
- const images = [];
3079
- const renamed = /* @__PURE__ */ new Map();
3080
- const resolved = /* @__PURE__ */ new Map();
3081
- let imageIndex = 0;
3082
- for (const block of imageBlocks) {
3083
- if (!block.text) continue;
3084
- const storageId = parseInt(block.text, 10);
3085
- if (isNaN(storageId)) continue;
3086
- let img = resolved.get(storageId);
3087
- if (img === void 0) {
3088
- const bin = binDataMap.get(storageId);
3089
- if (!bin) {
3090
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
3091
- resolved.set(storageId, null);
3092
- } else {
3093
- const mime = detectImageMime(bin.data);
3094
- if (!mime) {
3095
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
3096
- resolved.set(storageId, null);
3097
- } else {
3098
- imageIndex++;
3099
- const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3100
- img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
3101
- resolved.set(storageId, img);
3102
- images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
3103
- renamed.set(storageId, img.filename);
3104
- }
3105
- }
3106
- img = resolved.get(storageId);
3107
- }
3108
- if (!img) {
3109
- const bin = binDataMap.get(storageId);
3110
- block.type = "paragraph";
3111
- block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
3112
- continue;
3113
- }
3114
- block.text = img.filename;
3115
- block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3116
- }
3117
- resolveCellImageSentinels(blocks, renamed);
3118
- return images;
3119
- }
3120
- function extractHwp5Images(fileIndex, blocks, warnings) {
3121
- const binDataMap = /* @__PURE__ */ new Map();
3122
- if (fileIndex) {
3123
- for (const entry of fileIndex) {
3124
- if (!entry?.name || !entry.content) continue;
3125
- const match = entry.name.match(BIN_ENTRY_RE);
3126
- if (!match) continue;
3127
- const idx = parseInt(match[1], 16);
3128
- const data = normalizeBinPayload(Buffer.from(entry.content));
3129
- binDataMap.set(idx, { data, name: entry.name });
3130
- }
3131
- }
3132
- if (binDataMap.size === 0) {
3133
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3134
- return [];
3135
- }
3136
- return resolveImageBlocks(binDataMap, blocks, warnings);
3137
- }
3138
- function extractHwp5ImagesLenient(lcfb, blocks, warnings) {
3139
- const binDataMap = /* @__PURE__ */ new Map();
3140
- const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
3141
- for (const e of lcfb.entries()) {
3142
- const match = e.name.match(binRe);
3143
- if (!match) continue;
3144
- const idx = parseInt(match[1], 16);
3145
- const raw = lcfb.findStream(e.name);
3146
- if (!raw) continue;
3147
- binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
3148
- }
3149
- if (binDataMap.size === 0) {
3150
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3151
- return [];
3152
- }
3153
- return resolveImageBlocks(binDataMap, blocks, warnings);
3154
- }
3155
-
3156
- // src/image/transcode.ts
3157
- import { deflateSync } from "zlib";
3158
- var CRC_TABLE = (() => {
3159
- const table2 = new Uint32Array(256);
3160
- for (let n = 0; n < 256; n++) {
3161
- let c = n;
3162
- for (let k = 0; k < 8; k++) {
3163
- c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3164
- }
3165
- table2[n] = c >>> 0;
3166
- }
3167
- return table2;
3168
- })();
3169
- function crc32(bytes) {
3170
- let c = 4294967295;
3171
- for (let i = 0; i < bytes.length; i++) {
3172
- c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3173
- }
3174
- return (c ^ 4294967295) >>> 0;
3175
- }
3176
- var BI_RGB = 0;
3177
- var MAX_DIM = 32767;
3178
- var MAX_PIXELS = 36e6;
3179
- function bmpToPng(bmp) {
3180
- if (bmp.length < 54) return null;
3181
- if (bmp[0] !== 66 || bmp[1] !== 77) return null;
3182
- const dv = new DataView(bmp.buffer, bmp.byteOffset, bmp.byteLength);
3183
- const dataOffset = dv.getUint32(10, true);
3184
- const headerSize = dv.getUint32(14, true);
3185
- if (headerSize < 40) return null;
3186
- const width = dv.getInt32(18, true);
3187
- const rawHeight = dv.getInt32(22, true);
3188
- const bitCount = dv.getUint16(28, true);
3189
- const compression = dv.getUint32(30, true);
3190
- if (compression !== BI_RGB) return null;
3191
- if (bitCount !== 24 && bitCount !== 32) return null;
3192
- if (width <= 0 || rawHeight === 0) return null;
3193
- if (width > MAX_DIM || Math.abs(rawHeight) > MAX_DIM) return null;
3194
- const topDown = rawHeight < 0;
3195
- const height = Math.abs(rawHeight);
3196
- if (width * height > MAX_PIXELS) return null;
3197
- const bytesPerPixel = bitCount >> 3;
3198
- const rowStride = width * bytesPerPixel + 3 & ~3;
3199
- if (dataOffset + rowStride * height > bmp.length) return null;
3200
- const rgba = new Uint8Array(width * height * 4);
3201
- let anyAlpha = 0;
3202
- for (let y = 0; y < height; y++) {
3203
- const srcRow = topDown ? y : height - 1 - y;
3204
- let src = dataOffset + srcRow * rowStride;
3205
- let dst = y * width * 4;
3206
- for (let x = 0; x < width; x++) {
3207
- rgba[dst] = bmp[src + 2];
3208
- rgba[dst + 1] = bmp[src + 1];
3209
- rgba[dst + 2] = bmp[src];
3210
- const a = bitCount === 32 ? bmp[src + 3] : 255;
3211
- rgba[dst + 3] = a;
3212
- anyAlpha |= a;
3213
- src += bytesPerPixel;
3214
- dst += 4;
3215
- }
3216
- }
3217
- if (bitCount === 32 && anyAlpha === 0) {
3218
- for (let i = 3; i < rgba.length; i += 4) rgba[i] = 255;
3219
- }
3220
- return encodePng(width, height, rgba);
3221
- }
3222
- var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
3223
- function chunk(type, data) {
3224
- const body = new Uint8Array(4 + data.length);
3225
- body[0] = type.charCodeAt(0);
3226
- body[1] = type.charCodeAt(1);
3227
- body[2] = type.charCodeAt(2);
3228
- body[3] = type.charCodeAt(3);
3229
- body.set(data, 4);
3230
- const out = new Uint8Array(8 + data.length + 4);
3231
- const dv = new DataView(out.buffer);
3232
- dv.setUint32(0, data.length, false);
3233
- out.set(body, 4);
3234
- dv.setUint32(8 + data.length, crc32(body), false);
3235
- return out;
3236
- }
3237
- function encodePng(width, height, rgba) {
3238
- const ihdr = new Uint8Array(13);
3239
- const iv = new DataView(ihdr.buffer);
3240
- iv.setUint32(0, width, false);
3241
- iv.setUint32(4, height, false);
3242
- ihdr[8] = 8;
3243
- ihdr[9] = 6;
3244
- const stride = width * 4;
3245
- const raw = new Uint8Array((stride + 1) * height);
3246
- for (let y = 0; y < height; y++) {
3247
- const rowStart = y * (stride + 1);
3248
- raw[rowStart] = 0;
3249
- raw.set(rgba.subarray(y * stride, y * stride + stride), rowStart + 1);
3250
- }
3251
- const idat = deflateSync(raw);
3252
- const ihdrChunk = chunk("IHDR", ihdr);
3253
- const idatChunk = chunk("IDAT", idat);
3254
- const iendChunk = chunk("IEND", new Uint8Array(0));
3255
- const out = new Uint8Array(PNG_SIGNATURE.length + ihdrChunk.length + idatChunk.length + iendChunk.length);
3256
- let o = 0;
3257
- out.set(PNG_SIGNATURE, o);
3258
- o += PNG_SIGNATURE.length;
3259
- out.set(ihdrChunk, o);
3260
- o += ihdrChunk.length;
3261
- out.set(idatChunk, o);
3262
- o += idatChunk.length;
3263
- out.set(iendChunk, o);
3264
- return out;
3265
- }
3266
- function escapeRegExp(s) {
3267
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3268
- }
3269
- function inlineImagesIntoMarkdown(markdown, images, opts) {
3270
- const compress = opts?.compress !== false;
3271
- let out = markdown;
3272
- for (const img of images) {
3273
- let bytes = img.data;
3274
- let mime = img.mimeType;
3275
- if (compress && mime === "image/bmp") {
3276
- const png = bmpToPng(img.data);
3277
- if (png) {
3278
- bytes = png;
3279
- mime = "image/png";
3280
- }
3281
- }
3282
- const base64 = Buffer.from(bytes).toString("base64");
3283
- const dataUri = `data:${mime};base64,${base64}`;
3284
- const name = escapeRegExp(img.filename);
3285
- const mdRe = new RegExp(`!\\[image\\]\\((?:images/)?${name}\\)`, "g");
3286
- out = out.replace(mdRe, () => `![image](${dataUri})`);
3287
- const imgTagRe = new RegExp(`(<img\\b[^>]*\\bsrc=")(?:images/)?${name}(")`, "g");
3288
- out = out.replace(imgTagRe, (_m, pre, post) => `${pre}${dataUri}${post}`);
3289
- }
3290
- return out;
3291
- }
3292
-
3293
3197
  // src/hwp5/aes.ts
3294
3198
  var S_BOX = new Uint8Array([
3295
3199
  99,
@@ -4337,7 +4241,7 @@ function parseHwp5Document(buffer, options) {
4337
4241
  }
4338
4242
  }
4339
4243
  const blocks = [...doc.headerBlocks, ...bodyBlocks, ...doc.footerBlocks];
4340
- const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings);
4244
+ const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings, !pageFilter) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings, !pageFilter);
4341
4245
  let flatBlocks = flattenLayoutTables(blocks);
4342
4246
  if (options?.dedupeRunningHeaders) {
4343
4247
  const deduped = dedupeRunningHeaders(flatBlocks);
@@ -18812,6 +18716,102 @@ function extractRun(r) {
18812
18716
  }
18813
18717
  return { text, bold, italic };
18814
18718
  }
18719
+ function fieldUrlFromInstr(instr) {
18720
+ if (!/HYPERLINK\b/i.test(instr)) return null;
18721
+ const urlM = instr.match(/HYPERLINK\s+"([^"]*)"/i);
18722
+ const anchorM = instr.match(/\\l\s+"([^"]*)"/i);
18723
+ if (urlM && urlM[1]) {
18724
+ return anchorM && anchorM[1] ? `${urlM[1]}#${anchorM[1]}` : urlM[1];
18725
+ }
18726
+ if (anchorM && anchorM[1]) return `#${anchorM[1]}`;
18727
+ return null;
18728
+ }
18729
+ function makeLink(text, rawHref) {
18730
+ if (!text) return "";
18731
+ if (!rawHref) return text;
18732
+ const href = sanitizeHref(rawHref);
18733
+ return href ? `[${text}](${href})` : text;
18734
+ }
18735
+ function collectInline(p, footnotes, rels) {
18736
+ const parts = [];
18737
+ let bold = false;
18738
+ let italic = false;
18739
+ let footnoteText;
18740
+ let fieldActive = false;
18741
+ let fieldStage = "instr";
18742
+ let fieldInstr = "";
18743
+ let fieldDisplay = "";
18744
+ const flushField = () => {
18745
+ if (!fieldActive) return;
18746
+ if (fieldDisplay) {
18747
+ const url = fieldUrlFromInstr(fieldInstr);
18748
+ parts.push(url ? makeLink(fieldDisplay, url) : fieldDisplay);
18749
+ }
18750
+ fieldActive = false;
18751
+ fieldStage = "instr";
18752
+ fieldInstr = "";
18753
+ fieldDisplay = "";
18754
+ };
18755
+ for (const el of effectiveChildElements(p)) {
18756
+ if (matchesLocal(el, "hyperlink")) {
18757
+ const rId = getAttr(el, "id");
18758
+ const anchor = getAttr(el, "anchor");
18759
+ const t = findElements(el, "r").map((r) => extractRun(r).text).join("");
18760
+ if (!t) continue;
18761
+ let raw = null;
18762
+ if (rId && rels.has(rId)) raw = rels.get(rId);
18763
+ else if (anchor) raw = `#${anchor}`;
18764
+ if (fieldActive && fieldStage === "display") fieldDisplay += t;
18765
+ else parts.push(makeLink(t, raw));
18766
+ continue;
18767
+ }
18768
+ if (matchesLocal(el, "fldSimple")) {
18769
+ const t = findElements(el, "r").map((r) => extractRun(r).text).join("");
18770
+ if (!t) continue;
18771
+ const url = fieldUrlFromInstr(getAttr(el, "instr") ?? "");
18772
+ parts.push(url ? makeLink(t, url) : t);
18773
+ continue;
18774
+ }
18775
+ if (matchesLocal(el, "r")) {
18776
+ for (const fc of getChildElements(el, "fldChar")) {
18777
+ const ty = getAttr(fc, "fldCharType");
18778
+ if (ty === "begin") {
18779
+ flushField();
18780
+ fieldActive = true;
18781
+ fieldStage = "instr";
18782
+ } else if (ty === "separate") {
18783
+ if (fieldActive) fieldStage = "display";
18784
+ } else if (ty === "end") flushField();
18785
+ }
18786
+ if (fieldActive && fieldStage === "instr") {
18787
+ for (const it of getChildElements(el, "instrText")) fieldInstr += it.textContent ?? "";
18788
+ }
18789
+ const rr = extractRun(el);
18790
+ if (rr.bold) bold = true;
18791
+ if (rr.italic) italic = true;
18792
+ const fnRefEls = getChildElements(el, "footnoteReference");
18793
+ if (fnRefEls.length > 0) {
18794
+ const fnId = getAttr(fnRefEls[0], "id");
18795
+ if (fnId && footnotes.has(fnId)) footnoteText = footnotes.get(fnId);
18796
+ }
18797
+ if (rr.text) {
18798
+ if (fieldActive && fieldStage === "display") fieldDisplay += rr.text;
18799
+ else if (fieldActive && fieldStage === "instr") {
18800
+ } else parts.push(rr.text);
18801
+ }
18802
+ continue;
18803
+ }
18804
+ }
18805
+ flushField();
18806
+ for (const om of collectOmmlRoots(p)) {
18807
+ const latex = ommlElementToLatex(om);
18808
+ if (!latex) continue;
18809
+ if (isDisplayMath(om)) parts.push(" $$" + latex + "$$ ");
18810
+ else parts.push(" $" + latex + "$ ");
18811
+ }
18812
+ const text = parts.join("").replace(/[ \t]{2,}/g, " ").trim();
18813
+ return { text, bold, italic, footnoteText };
18814
+ }
18815
18815
  function parseParagraph2(p, styles, numbering, footnotes, rels) {
18816
18816
  const pPrEls = getChildElements(p, "pPr");
18817
18817
  let styleId = "";
@@ -18828,54 +18828,7 @@ function parseParagraph2(p, styles, numbering, footnotes, rels) {
18828
18828
  ilvl = ilvlEls.length > 0 ? parseInt(getAttr(ilvlEls[0], "val") ?? "0", 10) : 0;
18829
18829
  }
18830
18830
  }
18831
- const parts = [];
18832
- let hasBold = false;
18833
- let hasItalic = false;
18834
- let href;
18835
- let footnoteText;
18836
- const hyperlinks = getChildElements(p, "hyperlink");
18837
- const hyperlinkTexts = /* @__PURE__ */ new Set();
18838
- for (const hl of hyperlinks) {
18839
- const rId = getAttr(hl, "id");
18840
- const hlText = [];
18841
- const runs2 = findElements(hl, "r");
18842
- for (const r of runs2) {
18843
- const result = extractRun(r);
18844
- hlText.push(result.text);
18845
- }
18846
- const text2 = hlText.join("");
18847
- if (text2) {
18848
- hyperlinkTexts.add(text2);
18849
- if (rId && rels.has(rId)) {
18850
- href = rels.get(rId);
18851
- parts.push(text2);
18852
- } else {
18853
- parts.push(text2);
18854
- }
18855
- }
18856
- }
18857
- const runs = getChildElements(p, "r");
18858
- for (const r of runs) {
18859
- if (r.parentNode && r.parentNode.localName === "hyperlink") continue;
18860
- const result = extractRun(r);
18861
- if (result.bold) hasBold = true;
18862
- if (result.italic) hasItalic = true;
18863
- const fnRefEls = getChildElements(r, "footnoteReference");
18864
- if (fnRefEls.length > 0) {
18865
- const fnId = getAttr(fnRefEls[0], "id");
18866
- if (fnId && footnotes.has(fnId)) {
18867
- footnoteText = footnotes.get(fnId);
18868
- }
18869
- }
18870
- if (result.text) parts.push(result.text);
18871
- }
18872
- for (const om of collectOmmlRoots(p)) {
18873
- const latex = ommlElementToLatex(om);
18874
- if (!latex) continue;
18875
- if (isDisplayMath(om)) parts.push(" $$" + latex + "$$ ");
18876
- else parts.push(" $" + latex + "$ ");
18877
- }
18878
- const text = parts.join("").replace(/[ \t]{2,}/g, " ").trim();
18831
+ const { text, bold: hasBold, italic: hasItalic, footnoteText } = collectInline(p, footnotes, rels);
18879
18832
  if (!text) return null;
18880
18833
  const style = styles.get(styleId);
18881
18834
  if (style?.outlineLevel !== void 0 && style.outlineLevel >= 0 && style.outlineLevel <= 5) {
@@ -18895,7 +18848,6 @@ function parseParagraph2(p, styles, numbering, footnotes, rels) {
18895
18848
  if (hasBold || hasItalic) {
18896
18849
  block.style = { bold: hasBold || void 0, italic: hasItalic || void 0 };
18897
18850
  }
18898
- if (href) block.href = href;
18899
18851
  if (footnoteText) block.footnoteText = footnoteText;
18900
18852
  return block;
18901
18853
  }
@@ -18996,46 +18948,81 @@ function collectCellText(tc2, styles, numbering, footnotes, rels, depth) {
18996
18948
  }
18997
18949
  return parts;
18998
18950
  }
18999
- async function extractImages(zip, rels, doc, warnings) {
19000
- const blocks = [];
18951
+ async function buildImageMap(zip, rels, doc, warnings) {
18952
+ const imageMap = /* @__PURE__ */ new Map();
19001
18953
  const images = [];
19002
- const drawingElements = findElements(doc.documentElement, "drawing");
19003
18954
  let imgIdx = 0;
19004
- for (const drawing of drawingElements) {
19005
- const blips = findElements(drawing, "blip");
19006
- for (const blip of blips) {
19007
- const embedId = getAttr(blip, "embed");
19008
- if (!embedId) continue;
19009
- const target = rels.get(embedId);
19010
- if (!target) continue;
19011
- const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
19012
- const imgFile = zip.file(imgPath);
19013
- if (!imgFile) continue;
19014
- try {
19015
- const data = await imgFile.async("uint8array");
19016
- imgIdx++;
19017
- const ext = imgPath.split(".").pop()?.toLowerCase() ?? "png";
19018
- const mimeMap = {
19019
- png: "image/png",
19020
- jpg: "image/jpeg",
19021
- jpeg: "image/jpeg",
19022
- gif: "image/gif",
19023
- bmp: "image/bmp",
19024
- wmf: "image/wmf",
19025
- emf: "image/emf"
19026
- };
19027
- const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
19028
- images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
19029
- blocks.push({ type: "image", text: filename });
19030
- } catch (err) {
19031
- warnings.push({
19032
- code: "SKIPPED_IMAGE",
19033
- message: `DOCX \uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328 (${imgPath}): ${err instanceof Error ? err.message : String(err)}`
19034
- });
19035
- }
18955
+ const imageRefs = [];
18956
+ for (const blip of findElements(doc.documentElement, "blip")) {
18957
+ const embedId = getAttr(blip, "embed");
18958
+ if (embedId) imageRefs.push(embedId);
18959
+ }
18960
+ for (const imagedata of collectNonFallbackImagedata(doc.documentElement)) {
18961
+ const embedId = getAttr(imagedata, "id");
18962
+ if (embedId) imageRefs.push(embedId);
18963
+ }
18964
+ for (const embedId of imageRefs) {
18965
+ if (imageMap.has(embedId)) continue;
18966
+ const target = rels.get(embedId);
18967
+ if (!target) continue;
18968
+ const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
18969
+ const imgFile = zip.file(imgPath);
18970
+ if (!imgFile) continue;
18971
+ try {
18972
+ const data = await imgFile.async("uint8array");
18973
+ imgIdx++;
18974
+ const ext = imgPath.split(".").pop()?.toLowerCase() ?? "png";
18975
+ const mimeMap = {
18976
+ png: "image/png",
18977
+ jpg: "image/jpeg",
18978
+ jpeg: "image/jpeg",
18979
+ gif: "image/gif",
18980
+ bmp: "image/bmp",
18981
+ wmf: "image/wmf",
18982
+ emf: "image/emf"
18983
+ };
18984
+ const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
18985
+ images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
18986
+ imageMap.set(embedId, filename);
18987
+ } catch (err) {
18988
+ warnings.push({
18989
+ code: "SKIPPED_IMAGE",
18990
+ message: `DOCX \uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328 (${imgPath}): ${err instanceof Error ? err.message : String(err)}`
18991
+ });
19036
18992
  }
19037
18993
  }
19038
- return { blocks, images };
18994
+ return { imageMap, images };
18995
+ }
18996
+ function collectParagraphBlips(node, out = [], depth = 0) {
18997
+ if (depth > 40) return out;
18998
+ for (const el of effectiveChildElements(node)) {
18999
+ if (matchesLocal(el, "txbxContent") || matchesLocal(el, "Fallback")) continue;
19000
+ if (matchesLocal(el, "blip") || matchesLocal(el, "imagedata")) out.push(el);
19001
+ else collectParagraphBlips(el, out, depth + 1);
19002
+ }
19003
+ return out;
19004
+ }
19005
+ function collectNonFallbackImagedata(node, out = [], depth = 0) {
19006
+ if (depth > 60) return out;
19007
+ const children = node.childNodes;
19008
+ for (let i = 0; i < children.length; i++) {
19009
+ if (children[i].nodeType !== 1) continue;
19010
+ const el = children[i];
19011
+ if (matchesLocal(el, "Fallback")) continue;
19012
+ if (matchesLocal(el, "imagedata")) out.push(el);
19013
+ else collectNonFallbackImagedata(el, out, depth + 1);
19014
+ }
19015
+ return out;
19016
+ }
19017
+ function emitParagraphImages(p, imageMap, linked, out) {
19018
+ for (const blip of collectParagraphBlips(p)) {
19019
+ const embedId = getAttr(blip, "embed") ?? getAttr(blip, "id");
19020
+ if (!embedId) continue;
19021
+ const filename = imageMap.get(embedId);
19022
+ if (!filename) continue;
19023
+ linked.add(embedId);
19024
+ out.push({ type: "image", text: filename });
19025
+ }
19039
19026
  }
19040
19027
  async function parseDocxDocument(buffer, options) {
19041
19028
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
@@ -19092,6 +19079,8 @@ async function parseDocxDocument(buffer, options) {
19092
19079
  if (body.length === 0) {
19093
19080
  throw new KordocError("DOCX \uBCF8\uBB38(w:body)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
19094
19081
  }
19082
+ const { imageMap, images } = await buildImageMap(zip, rels, doc, warnings);
19083
+ const linkedImages = /* @__PURE__ */ new Set();
19095
19084
  const blocks = [];
19096
19085
  const bodyEl = body[0];
19097
19086
  const topLevel = effectiveChildElements(bodyEl);
@@ -19100,16 +19089,20 @@ async function parseDocxDocument(buffer, options) {
19100
19089
  if (localName2 === "p") {
19101
19090
  const block = parseParagraph2(el, styles, numbering, footnotes, rels);
19102
19091
  if (block) blocks.push(block);
19092
+ if (imageMap.size > 0) emitParagraphImages(el, imageMap, linkedImages, blocks);
19103
19093
  for (const tp of collectTextboxParagraphs(el)) {
19104
19094
  const tb = parseParagraph2(tp, styles, numbering, footnotes, rels);
19105
19095
  if (tb) blocks.push(tb);
19096
+ if (imageMap.size > 0) emitParagraphImages(tp, imageMap, linkedImages, blocks);
19106
19097
  }
19107
19098
  } else if (localName2 === "tbl") {
19108
19099
  const block = parseTable(el, styles, numbering, footnotes, rels);
19109
19100
  if (block) blocks.push(block);
19110
19101
  }
19111
19102
  }
19112
- const { blocks: imgBlocks, images } = await extractImages(zip, rels, doc, warnings);
19103
+ for (const [embedId, filename] of imageMap) {
19104
+ if (!linkedImages.has(embedId)) blocks.push({ type: "image", text: filename });
19105
+ }
19113
19106
  const metadata = {};
19114
19107
  const coreFile = zip.file("docProps/core.xml");
19115
19108
  if (coreFile) {
@@ -25102,16 +25095,16 @@ function buildOrigUnits(blocks) {
25102
25095
  for (let i = 0; i < blocks.length; i++) {
25103
25096
  const block = blocks[i];
25104
25097
  let consume = 1;
25105
- let chunk2;
25098
+ let chunk;
25106
25099
  if (block.type === "paragraph" && block.text && /^\[별표\s*\d+/.test(sanitizeText(block.text))) {
25107
25100
  const next = blocks[i + 1];
25108
25101
  if (next?.type === "paragraph" && next.text && /관련\)?$/.test(next.text)) consume = 2;
25109
- chunk2 = blocksToMarkdown(blocks.slice(i, i + consume));
25102
+ chunk = blocksToMarkdown(blocks.slice(i, i + consume));
25110
25103
  } else {
25111
- chunk2 = blocksToMarkdown([block]);
25104
+ chunk = blocksToMarkdown([block]);
25112
25105
  }
25113
- if (chunk2) {
25114
- const subUnits = splitMarkdownUnits(chunk2);
25106
+ if (chunk) {
25107
+ const subUnits = splitMarkdownUnits(chunk);
25115
25108
  const isFragment = consume === 2 || (block.type === "paragraph" || block.type === "heading") && subUnits.length > 1;
25116
25109
  for (let s = 0; s < subUnits.length; s++) {
25117
25110
  const u = { ...subUnits[s], blockIdx: i, fragment: isFragment || void 0 };
@@ -27103,7 +27096,7 @@ async function parseHwp(buffer, options) {
27103
27096
  async function parsePdf(buffer, options) {
27104
27097
  let parsePdfDocument;
27105
27098
  try {
27106
- const mod = await import("./parser-C636QIOB.js");
27099
+ const mod = await import("./parser-TCTCSBYZ.js");
27107
27100
  parsePdfDocument = mod.parsePdfDocument;
27108
27101
  } catch {
27109
27102
  return {
@@ -27114,8 +27107,8 @@ async function parsePdf(buffer, options) {
27114
27107
  };
27115
27108
  }
27116
27109
  try {
27117
- const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary } = await parsePdfDocument(buffer, options);
27118
- return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary };
27110
+ const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images } = await parsePdfDocument(buffer, options);
27111
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images };
27119
27112
  } catch (err) {
27120
27113
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
27121
27114
  return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };
@@ -27236,4 +27229,4 @@ export {
27236
27229
  parseHwpml,
27237
27230
  fillForm
27238
27231
  };
27239
- //# sourceMappingURL=chunk-HC2SEH6I.js.map
27232
+ //# sourceMappingURL=chunk-7S3M4N4E.js.map