kordoc 4.1.0 → 4.2.1

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 (126) hide show
  1. package/README.md +38 -11
  2. package/dist/{-AS4IABL2.js → -BSO3CDQQ.js} +13 -10
  3. package/dist/chunk-35U3MVA2.cjs +560 -0
  4. package/dist/chunk-35U3MVA2.cjs.map +1 -0
  5. package/dist/chunk-54WVWWT4.js +13 -0
  6. package/dist/chunk-54WVWWT4.js.map +1 -0
  7. package/dist/{chunk-UOO7LSDG.js → chunk-7FCNOKOR.js} +1 -9
  8. package/dist/chunk-7FCNOKOR.js.map +1 -0
  9. package/dist/{chunk-4KI23VHA.js → chunk-7IMNZ2BL.js} +16 -130
  10. package/dist/chunk-7IMNZ2BL.js.map +1 -0
  11. package/dist/{chunk-GQVSHGG4.js → chunk-BHILWIE6.js} +2 -2
  12. package/dist/{chunk-WO52HVQJ.js → chunk-BKBYTPSL.js} +2 -2
  13. package/dist/{parser-XX4QTDFQ.js → chunk-EZHWPKCH.js} +1343 -2020
  14. package/dist/chunk-EZHWPKCH.js.map +1 -0
  15. package/dist/chunk-F5ECB53I.js +505 -0
  16. package/dist/chunk-F5ECB53I.js.map +1 -0
  17. package/dist/chunk-GEPINOL6.js +83 -0
  18. package/dist/chunk-GEPINOL6.js.map +1 -0
  19. package/dist/chunk-H7OWEYH2.cjs +243 -0
  20. package/dist/chunk-H7OWEYH2.cjs.map +1 -0
  21. package/dist/chunk-HTCKGSSW.js +130 -0
  22. package/dist/chunk-HTCKGSSW.js.map +1 -0
  23. package/dist/chunk-J2RZ444H.js +243 -0
  24. package/dist/chunk-J2RZ444H.js.map +1 -0
  25. package/dist/chunk-KRAYUNFM.js +560 -0
  26. package/dist/chunk-KRAYUNFM.js.map +1 -0
  27. package/dist/{parser-PEOYBXBO.js → chunk-L5JWKIOT.js} +1345 -2024
  28. package/dist/chunk-L5JWKIOT.js.map +1 -0
  29. package/dist/{chunk-37VKMUST.cjs → chunk-LSXXGUWA.cjs} +34 -148
  30. package/dist/chunk-LSXXGUWA.cjs.map +1 -0
  31. package/dist/chunk-NNDVFNMJ.js +246 -0
  32. package/dist/chunk-NNDVFNMJ.js.map +1 -0
  33. package/dist/{chunk-37HJHCPA.js → chunk-PLTG6ZRF.js} +3 -3
  34. package/dist/{chunk-F2ZU3IZG.js → chunk-QOC7S5VE.js} +2 -2
  35. package/dist/{chunk-SSTK6IKK.js → chunk-QSB2RTX5.js} +14 -5
  36. package/dist/chunk-QSB2RTX5.js.map +1 -0
  37. package/dist/{parser-N4A7UGDK.cjs → chunk-R22KDUKW.cjs} +1309 -1989
  38. package/dist/chunk-R22KDUKW.cjs.map +1 -0
  39. package/dist/chunk-RBUBZ2TA.cjs +130 -0
  40. package/dist/chunk-RBUBZ2TA.cjs.map +1 -0
  41. package/dist/{chunk-YOO6ET6M.js → chunk-TCJRGFYY.js} +11 -1
  42. package/dist/chunk-TCJRGFYY.js.map +1 -0
  43. package/dist/{chunk-AX2R5Q2N.js → chunk-WJSGYVFD.js} +3 -3
  44. package/dist/{chunk-L2XQQTOY.js → chunk-WWYAYZ5U.js} +146 -47
  45. package/dist/chunk-WWYAYZ5U.js.map +1 -0
  46. package/dist/{chunk-O5VS7RIR.js → chunk-Z6LPEWIX.js} +3 -3
  47. package/dist/{chunk-O5VS7RIR.js.map → chunk-Z6LPEWIX.js.map} +1 -1
  48. package/dist/chunks-LRFS5NJE.js +10 -0
  49. package/dist/cli.js +77 -20
  50. package/dist/cli.js.map +1 -1
  51. package/dist/{detect-YEZX2XCF.js → detect-FX2ZHKZP.js} +4 -2
  52. package/dist/{formula-RXVSQPXI.js → formula-KSNAEBZ2.js} +23 -232
  53. package/dist/formula-KSNAEBZ2.js.map +1 -0
  54. package/dist/{formula-5NKVS2LR.cjs → formula-SB3NHDUM.cjs} +30 -240
  55. package/dist/formula-SB3NHDUM.cjs.map +1 -0
  56. package/dist/{formula-JCNF43NE.js → formula-SXC6LRRC.js} +23 -233
  57. package/dist/formula-SXC6LRRC.js.map +1 -0
  58. package/dist/image-ocr-DALT7PFQ.js +65 -0
  59. package/dist/image-ocr-DALT7PFQ.js.map +1 -0
  60. package/dist/image-ocr-N5Z32R4U.cjs +66 -0
  61. package/dist/image-ocr-N5Z32R4U.cjs.map +1 -0
  62. package/dist/image-ocr-SLUEM3FA.js +69 -0
  63. package/dist/image-ocr-SLUEM3FA.js.map +1 -0
  64. package/dist/index.cjs +555 -447
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +11 -5
  67. package/dist/index.d.ts +11 -5
  68. package/dist/index.js +157 -49
  69. package/dist/index.js.map +1 -1
  70. package/dist/mcp.js +24 -22
  71. package/dist/mcp.js.map +1 -1
  72. package/dist/models-KA4GLQK5.js +25 -0
  73. package/dist/parser-33TK5SMH.js +800 -0
  74. package/dist/parser-33TK5SMH.js.map +1 -0
  75. package/dist/parser-F5KY3YSD.js +804 -0
  76. package/dist/parser-F5KY3YSD.js.map +1 -0
  77. package/dist/parser-R4CXIXRN.cjs +803 -0
  78. package/dist/parser-R4CXIXRN.cjs.map +1 -0
  79. package/dist/pdf-ocr-34F5AQKK.js +12 -0
  80. package/dist/pdf-ocr-BXQZKU3N.js +15 -0
  81. package/dist/pdf-ocr-WGWZDHMV.cjs +13 -0
  82. package/dist/pdf-ocr-WGWZDHMV.cjs.map +1 -0
  83. package/dist/{profile-io-PLDQFPJA.js → profile-io-CBYHKLFU.js} +3 -3
  84. package/dist/profile-io-CBYHKLFU.js.map +1 -0
  85. package/dist/{rasterize-WWNQLKYW.js → rasterize-AWMM2CSW.js} +2 -2
  86. package/dist/render-Y3IE2FIK.js +10 -0
  87. package/dist/render-Y3IE2FIK.js.map +1 -0
  88. package/dist/seal-7RXFCRMT.js +10 -0
  89. package/dist/seal-7RXFCRMT.js.map +1 -0
  90. package/dist/{watch-SFHXZALC.js → watch-HKXCISVO.js} +12 -11
  91. package/dist/watch-HKXCISVO.js.map +1 -0
  92. package/package.json +2 -1
  93. package/dist/chunk-37VKMUST.cjs.map +0 -1
  94. package/dist/chunk-4KI23VHA.js.map +0 -1
  95. package/dist/chunk-L2XQQTOY.js.map +0 -1
  96. package/dist/chunk-SSTK6IKK.js.map +0 -1
  97. package/dist/chunk-UOO7LSDG.js.map +0 -1
  98. package/dist/chunk-YOO6ET6M.js.map +0 -1
  99. package/dist/chunks-WVMGF7JL.js +0 -10
  100. package/dist/formula-5NKVS2LR.cjs.map +0 -1
  101. package/dist/formula-JCNF43NE.js.map +0 -1
  102. package/dist/formula-RXVSQPXI.js.map +0 -1
  103. package/dist/parser-N4A7UGDK.cjs.map +0 -1
  104. package/dist/parser-PEOYBXBO.js.map +0 -1
  105. package/dist/parser-XX4QTDFQ.js.map +0 -1
  106. package/dist/provider-4FAYHJ6N.cjs +0 -39
  107. package/dist/provider-4FAYHJ6N.cjs.map +0 -1
  108. package/dist/provider-C6IOGIS5.js +0 -37
  109. package/dist/provider-C6IOGIS5.js.map +0 -1
  110. package/dist/provider-UIBW2MIH.js +0 -39
  111. package/dist/provider-UIBW2MIH.js.map +0 -1
  112. package/dist/render-T7S6H6AN.js +0 -10
  113. package/dist/seal-7PTL6MXH.js +0 -10
  114. package/dist/watch-SFHXZALC.js.map +0 -1
  115. /package/dist/{-AS4IABL2.js.map → -BSO3CDQQ.js.map} +0 -0
  116. /package/dist/{chunk-GQVSHGG4.js.map → chunk-BHILWIE6.js.map} +0 -0
  117. /package/dist/{chunk-WO52HVQJ.js.map → chunk-BKBYTPSL.js.map} +0 -0
  118. /package/dist/{chunk-37HJHCPA.js.map → chunk-PLTG6ZRF.js.map} +0 -0
  119. /package/dist/{chunk-F2ZU3IZG.js.map → chunk-QOC7S5VE.js.map} +0 -0
  120. /package/dist/{chunk-AX2R5Q2N.js.map → chunk-WJSGYVFD.js.map} +0 -0
  121. /package/dist/{chunks-WVMGF7JL.js.map → chunks-LRFS5NJE.js.map} +0 -0
  122. /package/dist/{detect-YEZX2XCF.js.map → detect-FX2ZHKZP.js.map} +0 -0
  123. /package/dist/{profile-io-PLDQFPJA.js.map → models-KA4GLQK5.js.map} +0 -0
  124. /package/dist/{render-T7S6H6AN.js.map → pdf-ocr-34F5AQKK.js.map} +0 -0
  125. /package/dist/{seal-7PTL6MXH.js.map → pdf-ocr-BXQZKU3N.js.map} +0 -0
  126. /package/dist/{rasterize-WWNQLKYW.js.map → rasterize-AWMM2CSW.js.map} +0 -0
@@ -2,18 +2,64 @@ import {
2
2
  HEADING_RATIO_H1,
3
3
  HEADING_RATIO_H2,
4
4
  HEADING_RATIO_H3,
5
- KordocError,
6
- blocksToMarkdown,
7
- encodePng,
8
5
  safeMax,
9
6
  safeMin
10
- } from "./chunk-4KI23VHA.js";
11
- import {
12
- parsePageRange
13
- } from "./chunk-GE43BE46.js";
7
+ } from "./chunk-HTCKGSSW.js";
14
8
 
15
- // src/pdf/line-extract.ts
9
+ // src/pdf/image-regions.ts
16
10
  import { OPS } from "pdfjs-dist/legacy/build/pdf.mjs";
11
+ function multiplyTransform(m, t) {
12
+ return [
13
+ m[0] * t[0] + m[2] * t[1],
14
+ m[1] * t[0] + m[3] * t[1],
15
+ m[0] * t[2] + m[2] * t[3],
16
+ m[1] * t[2] + m[3] * t[3],
17
+ m[0] * t[4] + m[2] * t[5] + m[4],
18
+ m[1] * t[4] + m[3] * t[5] + m[5]
19
+ ];
20
+ }
21
+ function extractImageRegions(fnArray, argsArray) {
22
+ const regions = [];
23
+ let ctm = [1, 0, 0, 1, 0, 0];
24
+ const stack = [];
25
+ for (let i = 0; i < fnArray.length; i++) {
26
+ const op = fnArray[i];
27
+ switch (op) {
28
+ case OPS.save:
29
+ stack.push(ctm);
30
+ break;
31
+ case OPS.restore:
32
+ ctm = stack.pop() || [1, 0, 0, 1, 0, 0];
33
+ break;
34
+ case OPS.transform: {
35
+ const t = argsArray[i];
36
+ if (Array.isArray(t) && t.length >= 6) ctm = multiplyTransform(ctm, t);
37
+ break;
38
+ }
39
+ case OPS.paintImageXObject:
40
+ case OPS.paintInlineImageXObject:
41
+ case OPS.paintImageMaskXObject:
42
+ case OPS.paintImageXObjectRepeat: {
43
+ const corners = [[0, 0], [1, 0], [0, 1], [1, 1]];
44
+ let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
45
+ for (const [u, v] of corners) {
46
+ const x = ctm[0] * u + ctm[2] * v + ctm[4];
47
+ const y = ctm[1] * u + ctm[3] * v + ctm[5];
48
+ if (x < x1) x1 = x;
49
+ if (x > x2) x2 = x;
50
+ if (y < y1) y1 = y;
51
+ if (y > y2) y2 = y;
52
+ }
53
+ if (x2 - x1 > 0 && y2 - y1 > 0) regions.push({ x1, y1, x2, y2 });
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ return regions;
59
+ }
60
+
61
+ // src/pdf/line-extract.ts
62
+ import { OPS as OPS2 } from "pdfjs-dist/legacy/build/pdf.mjs";
17
63
  var ORIENTATION_TOL = 2;
18
64
  var MIN_LINE_LENGTH = 15;
19
65
  var MAX_LINE_WIDTH = 5;
@@ -62,16 +108,16 @@ function extractLines(fnArray, argsArray) {
62
108
  const op = fnArray[i];
63
109
  const args = argsArray[i];
64
110
  switch (op) {
65
- case OPS.setLineWidth:
111
+ case OPS2.setLineWidth:
66
112
  lineWidth = args[0] || 1;
67
113
  break;
68
- case OPS.save:
114
+ case OPS2.save:
69
115
  ctmStack.push(ctm.slice());
70
116
  break;
71
- case OPS.restore:
117
+ case OPS2.restore:
72
118
  ctm = ctmStack.pop() ?? [1, 0, 0, 1, 0, 0];
73
119
  break;
74
- case OPS.transform: {
120
+ case OPS2.transform: {
75
121
  const t = args;
76
122
  ctm = [
77
123
  ctm[0] * t[0] + ctm[2] * t[1],
@@ -83,36 +129,36 @@ function extractLines(fnArray, argsArray) {
83
129
  ];
84
130
  break;
85
131
  }
86
- case OPS.constructPath: {
132
+ case OPS2.constructPath: {
87
133
  const arg0 = args[0];
88
134
  if (Array.isArray(arg0)) {
89
135
  const subOps = arg0;
90
136
  const coords = args[1];
91
137
  let ci = 0;
92
138
  for (const subOp of subOps) {
93
- if (subOp === OPS.moveTo) {
139
+ if (subOp === OPS2.moveTo) {
94
140
  curX = coords[ci++];
95
141
  curY = coords[ci++];
96
142
  pathStartX = curX;
97
143
  pathStartY = curY;
98
- } else if (subOp === OPS.lineTo) {
144
+ } else if (subOp === OPS2.lineTo) {
99
145
  const x2 = coords[ci++], y2 = coords[ci++];
100
146
  pushSeg(curX, curY, x2, y2);
101
147
  curX = x2;
102
148
  curY = y2;
103
- } else if (subOp === OPS.rectangle) {
149
+ } else if (subOp === OPS2.rectangle) {
104
150
  const rx = coords[ci++], ry = coords[ci++];
105
151
  const rw = coords[ci++], rh = coords[ci++];
106
152
  pushRectangle(rx, ry, rw, rh);
107
- } else if (subOp === OPS.closePath) {
153
+ } else if (subOp === OPS2.closePath) {
108
154
  if (curX !== pathStartX || curY !== pathStartY) {
109
155
  pushSeg(curX, curY, pathStartX, pathStartY);
110
156
  }
111
157
  curX = pathStartX;
112
158
  curY = pathStartY;
113
- } else if (subOp === OPS.curveTo) {
159
+ } else if (subOp === OPS2.curveTo) {
114
160
  ci += 6;
115
- } else if (subOp === OPS.curveTo2 || subOp === OPS.curveTo3) {
161
+ } else if (subOp === OPS2.curveTo2 || subOp === OPS2.curveTo3) {
116
162
  ci += 4;
117
163
  }
118
164
  }
@@ -150,33 +196,33 @@ function extractLines(fnArray, argsArray) {
150
196
  }
151
197
  }
152
198
  }
153
- if (afterOp === OPS.stroke || afterOp === OPS.closeStroke) {
199
+ if (afterOp === OPS2.stroke || afterOp === OPS2.closeStroke) {
154
200
  flushPath(true);
155
- } else if (afterOp === OPS.fill || afterOp === OPS.eoFill) {
201
+ } else if (afterOp === OPS2.fill || afterOp === OPS2.eoFill) {
156
202
  flushPath(true, true);
157
- } else if (afterOp === OPS.fillStroke || afterOp === OPS.eoFillStroke || afterOp === OPS.closeFillStroke || afterOp === OPS.closeEOFillStroke) {
203
+ } else if (afterOp === OPS2.fillStroke || afterOp === OPS2.eoFillStroke || afterOp === OPS2.closeFillStroke || afterOp === OPS2.closeEOFillStroke) {
158
204
  flushPath(true);
159
- } else if (afterOp === OPS.endPath) {
205
+ } else if (afterOp === OPS2.endPath) {
160
206
  flushPath(false);
161
207
  }
162
208
  }
163
209
  break;
164
210
  }
165
- case OPS.stroke:
166
- case OPS.closeStroke:
211
+ case OPS2.stroke:
212
+ case OPS2.closeStroke:
167
213
  flushPath(true);
168
214
  break;
169
- case OPS.fill:
170
- case OPS.eoFill:
215
+ case OPS2.fill:
216
+ case OPS2.eoFill:
171
217
  flushPath(true, true);
172
218
  break;
173
- case OPS.fillStroke:
174
- case OPS.eoFillStroke:
175
- case OPS.closeFillStroke:
176
- case OPS.closeEOFillStroke:
219
+ case OPS2.fillStroke:
220
+ case OPS2.eoFillStroke:
221
+ case OPS2.closeFillStroke:
222
+ case OPS2.closeEOFillStroke:
177
223
  flushPath(true);
178
224
  break;
179
- case OPS.endPath:
225
+ case OPS2.endPath:
180
226
  flushPath(false);
181
227
  break;
182
228
  }
@@ -312,9 +358,9 @@ function closeOpenTableEdges(horizontals, verticals) {
312
358
  const groups = [];
313
359
  for (const hl of chainCollinearRules(horizontals)) {
314
360
  let placed = false;
315
- for (const g2 of groups) {
316
- if (Math.abs(g2[0].x1 - hl.x1) <= EDGE_ALIGN_TOL && Math.abs(g2[0].x2 - hl.x2) <= EDGE_ALIGN_TOL) {
317
- g2.push(hl);
361
+ for (const g of groups) {
362
+ if (Math.abs(g[0].x1 - hl.x1) <= EDGE_ALIGN_TOL && Math.abs(g[0].x2 - hl.x2) <= EDGE_ALIGN_TOL) {
363
+ g.push(hl);
318
364
  placed = true;
319
365
  break;
320
366
  }
@@ -322,8 +368,8 @@ function closeOpenTableEdges(horizontals, verticals) {
322
368
  if (!placed) groups.push([hl]);
323
369
  }
324
370
  const splitGroups = [];
325
- for (const g2 of groups) {
326
- const sorted = [...g2].sort((a, b) => a.y1 - b.y1);
371
+ for (const g of groups) {
372
+ const sorted = [...g].sort((a, b) => a.y1 - b.y1);
327
373
  const gaps = [];
328
374
  for (let i = 1; i < sorted.length; i++) gaps.push(sorted[i].y1 - sorted[i - 1].y1);
329
375
  const median = gaps.length ? [...gaps].sort((a, b) => a - b)[gaps.length >> 1] : 0;
@@ -344,21 +390,21 @@ function closeOpenTableEdges(horizontals, verticals) {
344
390
  if (cur.length) splitGroups.push(cur);
345
391
  }
346
392
  const synthesized = [];
347
- for (const g2 of splitGroups) {
348
- if (g2.length < EDGE_MIN_RULES) continue;
393
+ for (const g of splitGroups) {
394
+ if (g.length < EDGE_MIN_RULES) continue;
349
395
  let yMin = Infinity, yMax = -Infinity, x1 = 0, x2 = 0;
350
- for (const hl of g2) {
396
+ for (const hl of g) {
351
397
  if (hl.y1 < yMin) yMin = hl.y1;
352
398
  if (hl.y1 > yMax) yMax = hl.y1;
353
399
  x1 += hl.x1;
354
400
  x2 += hl.x2;
355
401
  }
356
- x1 /= g2.length;
357
- x2 /= g2.length;
402
+ x1 /= g.length;
403
+ x2 /= g.length;
358
404
  if (yMax - yMin < EDGE_MIN_SPAN) continue;
359
405
  const crossCount = (v) => {
360
406
  let n = 0;
361
- for (const hl of g2) {
407
+ for (const hl of g) {
362
408
  if (v.x1 >= hl.x1 - EDGE_CONNECT_TOL && v.x1 <= hl.x2 + EDGE_CONNECT_TOL && hl.y1 >= v.y1 - EDGE_CONNECT_TOL && hl.y1 <= v.y2 + EDGE_CONNECT_TOL) n++;
363
409
  }
364
410
  return n;
@@ -495,58 +541,6 @@ function bridgeSplitColumnVerticals(horizontals, verticals) {
495
541
  return synthesized.length ? [...verticals, ...synthesized] : verticals;
496
542
  }
497
543
 
498
- // src/pdf/image-regions.ts
499
- import { OPS as OPS2 } from "pdfjs-dist/legacy/build/pdf.mjs";
500
- function multiplyTransform(m, t) {
501
- return [
502
- m[0] * t[0] + m[2] * t[1],
503
- m[1] * t[0] + m[3] * t[1],
504
- m[0] * t[2] + m[2] * t[3],
505
- m[1] * t[2] + m[3] * t[3],
506
- m[0] * t[4] + m[2] * t[5] + m[4],
507
- m[1] * t[4] + m[3] * t[5] + m[5]
508
- ];
509
- }
510
- function extractImageRegions(fnArray, argsArray) {
511
- const regions = [];
512
- let ctm = [1, 0, 0, 1, 0, 0];
513
- const stack = [];
514
- for (let i = 0; i < fnArray.length; i++) {
515
- const op = fnArray[i];
516
- switch (op) {
517
- case OPS2.save:
518
- stack.push(ctm);
519
- break;
520
- case OPS2.restore:
521
- ctm = stack.pop() || [1, 0, 0, 1, 0, 0];
522
- break;
523
- case OPS2.transform: {
524
- const t = argsArray[i];
525
- if (Array.isArray(t) && t.length >= 6) ctm = multiplyTransform(ctm, t);
526
- break;
527
- }
528
- case OPS2.paintImageXObject:
529
- case OPS2.paintInlineImageXObject:
530
- case OPS2.paintImageMaskXObject:
531
- case OPS2.paintImageXObjectRepeat: {
532
- const corners = [[0, 0], [1, 0], [0, 1], [1, 1]];
533
- let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
534
- for (const [u, v] of corners) {
535
- const x = ctm[0] * u + ctm[2] * v + ctm[4];
536
- const y = ctm[1] * u + ctm[3] * v + ctm[5];
537
- if (x < x1) x1 = x;
538
- if (x > x2) x2 = x;
539
- if (y < y1) y1 = y;
540
- if (y > y2) y2 = y;
541
- }
542
- if (x2 - x1 > 0 && y2 - y1 > 0) regions.push({ x1, y1, x2, y2 });
543
- break;
544
- }
545
- }
546
- }
547
- return regions;
548
- }
549
-
550
544
  // src/pdf/line-types.ts
551
545
  var VERTEX_MERGE_FACTOR = 4;
552
546
 
@@ -636,8 +630,8 @@ function buildTableGrids(horizontals, verticals) {
636
630
  ...verticals.map((l, i) => ({ ...l, type: "v", id: i + horizontals.length }))
637
631
  ];
638
632
  const groups = [];
639
- for (const g2 of groupConnectedLines(allLines)) {
640
- const bands = splitStackedGroup(g2);
633
+ for (const g of groupConnectedLines(allLines)) {
634
+ const bands = splitStackedGroup(g);
641
635
  for (const b of bands) groups.push({ lines: b, fromSplit: bands.length > 1 });
642
636
  }
643
637
  const grids = [];
@@ -1058,7 +1052,8 @@ function cellTextToString(items) {
1058
1052
  }
1059
1053
  }
1060
1054
  lines.push(curLine);
1061
- const textLines = lines.map((line) => {
1055
+ const merged = mergeSuperscriptRows(lines);
1056
+ const textLines = merged.map((line) => {
1062
1057
  const s = line.sort((a, b) => a.x - b.x);
1063
1058
  if (s.length === 1) return s[0].text;
1064
1059
  const evenSpaced = detectEvenSpacedItems(s);
@@ -1082,6 +1077,40 @@ function cellTextToString(items) {
1082
1077
  });
1083
1078
  return mergeCellTextLines(textLines);
1084
1079
  }
1080
+ function mergeSuperscriptRows(lines) {
1081
+ if (lines.length <= 1) return lines;
1082
+ const band = (line) => {
1083
+ let bottom = Infinity, top = -Infinity;
1084
+ for (const i of line) {
1085
+ const h = i.h > 0 ? i.h : i.fontSize;
1086
+ if (i.y < bottom) bottom = i.y;
1087
+ if (i.y + h > top) top = i.y + h;
1088
+ }
1089
+ return { bottom, top, height: top - bottom };
1090
+ };
1091
+ const isFrag = (line) => {
1092
+ if (line.length > 8) return false;
1093
+ let total = 0;
1094
+ for (const i of line) total += i.text.trim().length;
1095
+ return total > 0 && total <= 10;
1096
+ };
1097
+ const result = [lines[0]];
1098
+ for (let i = 1; i < lines.length; i++) {
1099
+ const prev = result[result.length - 1];
1100
+ const curr = lines[i];
1101
+ const a = band(prev);
1102
+ const b = band(curr);
1103
+ const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
1104
+ const prevIsFrag = isFrag(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
1105
+ const currIsFrag = isFrag(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
1106
+ if (prevIsFrag || currIsFrag) {
1107
+ result[result.length - 1] = [...prev, ...curr];
1108
+ } else {
1109
+ result.push(curr);
1110
+ }
1111
+ }
1112
+ return result;
1113
+ }
1085
1114
  function detectEvenSpacedItems(items) {
1086
1115
  const result = new Array(items.length).fill(false);
1087
1116
  if (items.length < 3) return result;
@@ -1121,12 +1150,12 @@ function markEvenRun(items, result, start, end) {
1121
1150
  for (let i = start + 1; i < end; i++) {
1122
1151
  gaps.push(items[i].x - (items[i - 1].x + items[i - 1].w));
1123
1152
  }
1124
- const posGaps = gaps.filter((g2) => g2 > 0);
1153
+ const posGaps = gaps.filter((g) => g > 0);
1125
1154
  if (posGaps.length < 2) return;
1126
1155
  let minGap = Infinity, maxGap = -Infinity;
1127
- for (const g2 of posGaps) {
1128
- if (g2 < minGap) minGap = g2;
1129
- if (g2 > maxGap) maxGap = g2;
1156
+ for (const g of posGaps) {
1157
+ if (g < minGap) minGap = g;
1158
+ if (g > maxGap) maxGap = g;
1130
1159
  }
1131
1160
  const avgFs = items[start].fontSize;
1132
1161
  if (minGap >= avgFs * 0.1 && maxGap <= avgFs * 3 && maxGap / Math.max(minGap, 0.1) <= 3) {
@@ -1271,303 +1300,6 @@ function normalizeUndersegmentedTable(originalCells, colXs, items) {
1271
1300
  return rebuilt;
1272
1301
  }
1273
1302
 
1274
- // src/pdf/image-extract.ts
1275
- import { OPS as OPS3, ImageKind } from "pdfjs-dist/legacy/build/pdf.mjs";
1276
- var IMAGE_RESOLVE_TIMEOUT_MS = 5e3;
1277
- function resolveImgData(page, objId) {
1278
- const store = objId.startsWith("g_") ? page.commonObjs : page.objs;
1279
- return new Promise((resolve) => {
1280
- let done = false;
1281
- const timer = setTimeout(() => {
1282
- if (!done) {
1283
- done = true;
1284
- resolve(null);
1285
- }
1286
- }, IMAGE_RESOLVE_TIMEOUT_MS);
1287
- try {
1288
- store.get(objId, (data) => {
1289
- if (!done) {
1290
- done = true;
1291
- clearTimeout(timer);
1292
- resolve(data ?? null);
1293
- }
1294
- });
1295
- } catch {
1296
- if (!done) {
1297
- done = true;
1298
- clearTimeout(timer);
1299
- resolve(null);
1300
- }
1301
- }
1302
- });
1303
- }
1304
- var MIN_DIM = 8;
1305
- var MAX_PIXELS = 36e6;
1306
- var MAX_IMAGES_PER_DOC = 200;
1307
- var MAX_TOTAL_IMAGE_BYTES = 128 * 1024 * 1024;
1308
- function createPdfImageState() {
1309
- return { imageIndex: 0, totalBytes: 0, seen: /* @__PURE__ */ new Map(), capWarned: false };
1310
- }
1311
- function fnv1a(data) {
1312
- let h = 2166136261;
1313
- for (let i = 0; i < data.length; i++) {
1314
- h ^= data[i];
1315
- h = Math.imul(h, 16777619);
1316
- }
1317
- return h >>> 0;
1318
- }
1319
- function toRgba(img) {
1320
- const { width: w, height: h, kind, data } = img;
1321
- if (!data || !w || !h) return null;
1322
- const rgba = new Uint8Array(w * h * 4);
1323
- if (kind === ImageKind.RGBA_32BPP) {
1324
- if (data.length < w * h * 4) return null;
1325
- rgba.set(data.subarray(0, w * h * 4));
1326
- return rgba;
1327
- }
1328
- if (kind === ImageKind.RGB_24BPP) {
1329
- if (data.length < w * h * 3) return null;
1330
- for (let i = 0, s = 0, d = 0; i < w * h; i++, s += 3, d += 4) {
1331
- rgba[d] = data[s];
1332
- rgba[d + 1] = data[s + 1];
1333
- rgba[d + 2] = data[s + 2];
1334
- rgba[d + 3] = 255;
1335
- }
1336
- return rgba;
1337
- }
1338
- if (kind === ImageKind.GRAYSCALE_1BPP) {
1339
- const stride = w + 7 >> 3;
1340
- if (data.length < stride * h) return null;
1341
- for (let y = 0; y < h; y++) {
1342
- for (let x = 0; x < w; x++) {
1343
- const bit = data[y * stride + (x >> 3)] >> 7 - (x & 7) & 1;
1344
- const v = bit ? 255 : 0;
1345
- const d = (y * w + x) * 4;
1346
- rgba[d] = v;
1347
- rgba[d + 1] = v;
1348
- rgba[d + 2] = v;
1349
- rgba[d + 3] = 255;
1350
- }
1351
- }
1352
- return rgba;
1353
- }
1354
- return null;
1355
- }
1356
- async function extractPageImages(page, fnArray, argsArray, pageNumber, state, warnings) {
1357
- const blocks = [];
1358
- const images = [];
1359
- const pageSeenIds = /* @__PURE__ */ new Set();
1360
- for (let i = 0; i < fnArray.length; i++) {
1361
- const op = fnArray[i];
1362
- let imgData = null;
1363
- let dedupeId = null;
1364
- if (op === OPS3.paintImageXObject || op === OPS3.paintImageXObjectRepeat) {
1365
- const objId = argsArray[i]?.[0];
1366
- if (typeof objId !== "string" || pageSeenIds.has(objId)) continue;
1367
- pageSeenIds.add(objId);
1368
- dedupeId = objId;
1369
- } else if (op !== OPS3.paintInlineImageXObject) {
1370
- continue;
1371
- }
1372
- if (state.imageIndex >= MAX_IMAGES_PER_DOC || state.totalBytes >= MAX_TOTAL_IMAGE_BYTES) {
1373
- if (!state.capWarned) {
1374
- state.capWarned = true;
1375
- warnings.push({ page: pageNumber, message: `\uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC0C1\uD55C \uB3C4\uB2EC (${MAX_IMAGES_PER_DOC}\uAC1C/128MB) \u2014 \uC774\uD6C4 \uC774\uBBF8\uC9C0 \uC0DD\uB7B5`, code: "SKIPPED_IMAGE" });
1376
- }
1377
- continue;
1378
- }
1379
- if (dedupeId) {
1380
- imgData = await resolveImgData(page, dedupeId);
1381
- } else {
1382
- imgData = argsArray[i]?.[0];
1383
- }
1384
- if (!imgData?.data || !imgData.width || !imgData.height) continue;
1385
- const { width: w, height: h } = imgData;
1386
- if (w < MIN_DIM || h < MIN_DIM) continue;
1387
- if (w * h > MAX_PIXELS) {
1388
- warnings.push({ page: pageNumber, message: `\uC774\uBBF8\uC9C0 \uD06C\uAE30 \uCD08\uACFC\uB85C \uCD94\uCD9C \uC0DD\uB7B5 (${w}\xD7${h})`, code: "SKIPPED_IMAGE" });
1389
- continue;
1390
- }
1391
- const hash = `${w}x${h}k${imgData.kind ?? "?"}h${fnv1a(imgData.data)}${dedupeId ? "" : "inline"}`;
1392
- if (state.seen.has(hash)) continue;
1393
- const rgba = toRgba(imgData);
1394
- if (!rgba) continue;
1395
- const png = encodePng(w, h, rgba);
1396
- state.imageIndex++;
1397
- state.totalBytes += png.length;
1398
- const filename = `image_${String(state.imageIndex).padStart(3, "0")}.png`;
1399
- state.seen.set(hash, filename);
1400
- images.push({ filename, data: png, mimeType: "image/png" });
1401
- blocks.push({ type: "image", text: filename, pageNumber });
1402
- }
1403
- return { blocks, images };
1404
- }
1405
- function injectPageImageBlocks(blocks, pageImages) {
1406
- if (pageImages.size === 0) return;
1407
- const lastIndexForPage = /* @__PURE__ */ new Map();
1408
- for (let i = 0; i < blocks.length; i++) {
1409
- const p = blocks[i].pageNumber;
1410
- if (p !== void 0) lastIndexForPage.set(p, i);
1411
- }
1412
- const pageAtIndex = /* @__PURE__ */ new Map();
1413
- for (const [p, last] of lastIndexForPage) pageAtIndex.set(last, p);
1414
- const result = [];
1415
- const injected = /* @__PURE__ */ new Set();
1416
- for (let i = 0; i < blocks.length; i++) {
1417
- result.push(blocks[i]);
1418
- const p = pageAtIndex.get(i);
1419
- if (p !== void 0 && pageImages.has(p)) {
1420
- result.push(...pageImages.get(p));
1421
- injected.add(p);
1422
- }
1423
- }
1424
- for (const p of [...pageImages.keys()].sort((a, b) => a - b)) {
1425
- if (injected.has(p)) continue;
1426
- let at = result.length;
1427
- for (let i = 0; i < result.length; i++) {
1428
- const bp = result[i].pageNumber;
1429
- if (bp !== void 0 && bp > p) {
1430
- at = i;
1431
- break;
1432
- }
1433
- }
1434
- result.splice(at, 0, ...pageImages.get(p));
1435
- }
1436
- blocks.length = 0;
1437
- blocks.push(...result);
1438
- }
1439
-
1440
- // src/pdf/quality.ts
1441
- var RARE_JONG = /* @__PURE__ */ new Set([3, 5, 6, 9, 10, 11, 12, 13, 14, 15, 18, 24, 25, 26]);
1442
- var MOJIBAKE_MIN_HANGUL = 30;
1443
- var MOJIBAKE_MAX_NOBATCHIM = 0.15;
1444
- var MOJIBAKE_MIN_RAREBATCHIM = 0.25;
1445
- function computePageQuality(page, text) {
1446
- let total = 0;
1447
- let hangul = 0;
1448
- let hangulNoBatchim = 0;
1449
- let hangulRareBatchim = 0;
1450
- let control = 0;
1451
- let replacement = 0;
1452
- let pua = 0;
1453
- for (let i = 0; i < text.length; i++) {
1454
- const code = text.charCodeAt(i);
1455
- if (code === 32 || code === 9 || code === 10 || code === 13) continue;
1456
- total++;
1457
- if (code < 32 || code === 127 || code >= 128 && code <= 159) {
1458
- control++;
1459
- continue;
1460
- }
1461
- if (code === 65533) {
1462
- replacement++;
1463
- continue;
1464
- }
1465
- if (code >= 44032 && code <= 55203) {
1466
- hangul++;
1467
- const jong = (code - 44032) % 28;
1468
- if (jong === 0) hangulNoBatchim++;
1469
- else if (RARE_JONG.has(jong)) hangulRareBatchim++;
1470
- continue;
1471
- }
1472
- if (code >= 57344 && code <= 63743 || code >= 56192 && code <= 56319) {
1473
- pua++;
1474
- continue;
1475
- }
1476
- }
1477
- const denom = total || 1;
1478
- const puaRatio = pua / denom;
1479
- const controlCharRatio = control / denom;
1480
- const replacementCharRatio = replacement / denom;
1481
- const hangulNoBatchimRatio = hangul > 0 ? hangulNoBatchim / hangul : 0;
1482
- const hangulRareBatchimRatio = hangul > 0 ? hangulRareBatchim / hangul : 0;
1483
- const garbledHangul = hangul >= MOJIBAKE_MIN_HANGUL && hangulNoBatchimRatio < MOJIBAKE_MAX_NOBATCHIM && hangulRareBatchimRatio >= MOJIBAKE_MIN_RAREBATCHIM;
1484
- let needsOcr = false;
1485
- let ocrReason;
1486
- if (total < LOW_TEXT_THRESHOLD) {
1487
- needsOcr = true;
1488
- ocrReason = "low_text";
1489
- } else if (puaRatio >= HIGH_PUA_THRESHOLD) {
1490
- needsOcr = true;
1491
- ocrReason = "high_pua";
1492
- } else if (controlCharRatio >= HIGH_CONTROL_THRESHOLD) {
1493
- needsOcr = true;
1494
- ocrReason = "high_control";
1495
- } else if (replacementCharRatio >= HIGH_REPLACEMENT_THRESHOLD) {
1496
- needsOcr = true;
1497
- ocrReason = "high_replacement";
1498
- } else if (garbledHangul) {
1499
- needsOcr = true;
1500
- ocrReason = "garbled_hangul";
1501
- }
1502
- return {
1503
- page,
1504
- textChars: total,
1505
- hangulRatio: hangul / denom,
1506
- controlCharRatio,
1507
- replacementCharRatio,
1508
- puaRatio,
1509
- hangulNoBatchimRatio,
1510
- hangulRareBatchimRatio,
1511
- needsOcr,
1512
- ocrReason
1513
- };
1514
- }
1515
- var LOW_TEXT_THRESHOLD = 20;
1516
- var HIGH_PUA_THRESHOLD = 0.2;
1517
- var HIGH_CONTROL_THRESHOLD = 0.05;
1518
- var HIGH_REPLACEMENT_THRESHOLD = 0.05;
1519
- var DOC_NEEDS_OCR_PAGE_RATIO = 0.3;
1520
- function stripControlChars(text) {
1521
- return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
1522
- }
1523
- function summarizeDocumentQuality(pages) {
1524
- if (pages.length === 0) {
1525
- return {
1526
- totalPages: 0,
1527
- totalTextChars: 0,
1528
- avgHangulRatio: 0,
1529
- avgControlCharRatio: 0,
1530
- avgReplacementCharRatio: 0,
1531
- avgPuaRatio: 0,
1532
- lowTextPageCount: 0,
1533
- highPuaPageCount: 0,
1534
- needsOcr: false,
1535
- ocrCandidatePages: []
1536
- };
1537
- }
1538
- let textChars = 0;
1539
- let hangul = 0;
1540
- let control = 0;
1541
- let replacement = 0;
1542
- let pua = 0;
1543
- let lowText = 0;
1544
- let highPua = 0;
1545
- const ocrCandidatePages = [];
1546
- for (const p of pages) {
1547
- textChars += p.textChars;
1548
- hangul += p.hangulRatio;
1549
- control += p.controlCharRatio;
1550
- replacement += p.replacementCharRatio;
1551
- pua += p.puaRatio;
1552
- if (p.textChars < LOW_TEXT_THRESHOLD) lowText++;
1553
- if (p.puaRatio >= HIGH_PUA_THRESHOLD) highPua++;
1554
- if (p.needsOcr) ocrCandidatePages.push(p.page);
1555
- }
1556
- const n = pages.length;
1557
- return {
1558
- totalPages: n,
1559
- totalTextChars: textChars,
1560
- avgHangulRatio: hangul / n,
1561
- avgControlCharRatio: control / n,
1562
- avgReplacementCharRatio: replacement / n,
1563
- avgPuaRatio: pua / n,
1564
- lowTextPageCount: lowText,
1565
- highPuaPageCount: highPua,
1566
- needsOcr: ocrCandidatePages.length / n >= DOC_NEEDS_OCR_PAGE_RATIO,
1567
- ocrCandidatePages
1568
- };
1569
- }
1570
-
1571
1303
  // src/pdf/text-line.ts
1572
1304
  function filterHiddenText(items, pageWidth, pageHeight) {
1573
1305
  let hiddenCount = 0;
@@ -1662,7 +1394,8 @@ function normalizeItems(rawItems) {
1662
1394
  for (let j = deduped.length - 1; j >= 0; j--) {
1663
1395
  const prev = deduped[j];
1664
1396
  if (prev.y - sorted[i].y > 3) break;
1665
- if (Math.abs(prev.y - sorted[i].y) <= 3 && prev.text === sorted[i].text && Math.abs(prev.x - sorted[i].x) <= 3) {
1397
+ const xTol = Math.min(3, Math.max(0.5, sorted[i].w * 0.5));
1398
+ if (Math.abs(prev.y - sorted[i].y) <= 3 && prev.text === sorted[i].text && Math.abs(prev.x - sorted[i].x) <= xTol) {
1666
1399
  isDup = true;
1667
1400
  break;
1668
1401
  }
@@ -1724,7 +1457,12 @@ function mergeSuperscriptLines(lines) {
1724
1457
  }
1725
1458
  return { bottom, top, height: top - bottom };
1726
1459
  };
1727
- const isFrag = (line) => line.length <= 3 && line.every((i) => i.text.trim().length <= 8);
1460
+ const isFrag = (line) => {
1461
+ if (line.length > 8) return false;
1462
+ let total = 0;
1463
+ for (const i of line) total += i.text.trim().length;
1464
+ return total > 0 && total <= 10;
1465
+ };
1728
1466
  const result = [lines[0]];
1729
1467
  for (let i = 1; i < lines.length; i++) {
1730
1468
  const prev = result[result.length - 1];
@@ -1776,1348 +1514,1367 @@ function mergeLineSimple(items) {
1776
1514
  return result;
1777
1515
  }
1778
1516
 
1779
- // src/pdf/cluster-detector.ts
1780
- var Y_TOL = 3;
1781
- var COL_CLUSTER_TOL = 15;
1782
- var MIN_ROWS = 3;
1783
- var MIN_COLS = 2;
1784
- var MIN_GAP_FACTOR = 2;
1785
- var MIN_GAP_ABSOLUTE = 20;
1786
- var MIN_COL_FILL_RATIO = 0.4;
1787
- function detectClusterTables(items, pageNum) {
1788
- if (items.length < MIN_ROWS * MIN_COLS) return [];
1789
- const { merged, originMap } = mergeEvenSpacedClusters(items);
1790
- const rows = mergeOverlappingRows(groupByBaseline(merged));
1791
- if (rows.length < MIN_ROWS) return [];
1792
- const results = [];
1793
- const headerResult = detectHeaderRow(rows);
1794
- if (headerResult) {
1795
- const { columns, headerIdx } = headerResult;
1796
- const headerRow = rows[headerIdx];
1797
- const headerItems = [...headerRow.items].sort((a, b) => a.x - b.x);
1798
- const headerAndBelow = rows.slice(headerIdx);
1799
- const mergedRows = mergeMultiLineRows(headerAndBelow, columns);
1800
- const tableRegions = findTableRegionsByHeader(mergedRows, columns, headerItems);
1801
- for (const region of tableRegions) {
1802
- const table = buildClusterTable(region.rows, columns, pageNum);
1803
- if (table) {
1804
- expandUsedItems(table.usedItems, originMap);
1805
- results.push(table);
1806
- }
1807
- }
1808
- }
1809
- if (results.length === 0) {
1810
- const suspiciousRows = rows.filter((row) => hasSuspiciousGaps(row));
1811
- if (suspiciousRows.length >= MIN_ROWS) {
1812
- const columns = extractColumnClusters(suspiciousRows);
1813
- if (columns.length >= MIN_COLS) {
1814
- const tableRegions = findTableRegions(rows, columns);
1815
- for (const region of tableRegions) {
1816
- const mergedRows = mergeMultiLineRows(region.rows, columns);
1817
- const table = buildClusterTable(mergedRows, columns, pageNum);
1818
- if (table) {
1819
- expandUsedItems(table.usedItems, originMap);
1820
- results.push(table);
1821
- }
1822
- }
1823
- }
1824
- }
1517
+ // src/pdf/block-detect.ts
1518
+ function computeMedianFontSizeFromFreq(freq) {
1519
+ if (freq.size === 0) return 0;
1520
+ let total = 0;
1521
+ for (const count of freq.values()) total += count;
1522
+ const sorted = [...freq.entries()].sort((a, b) => a[0] - b[0]);
1523
+ const mid = Math.floor(total / 2);
1524
+ let cumulative = 0;
1525
+ for (const [size, count] of sorted) {
1526
+ cumulative += count;
1527
+ if (cumulative > mid) return size;
1825
1528
  }
1826
- return results.filter((r) => !isTwoColumnProse(r));
1529
+ return sorted[sorted.length - 1][0];
1827
1530
  }
1828
- function isTwoColumnProse(r) {
1829
- const t = r.table;
1830
- if (t.rows < 8) return false;
1831
- const dense = [];
1832
- for (let c = 0; c < t.cols; c++) {
1833
- const filled = t.cells.filter((row) => row[c]?.text.trim()).length;
1834
- if (filled / t.rows >= 0.3) dense.push(c);
1835
- }
1836
- if (dense.length !== 2) return false;
1837
- for (const c of dense) {
1838
- const lens = t.cells.map((row) => row[c]?.text.replace(/\s+/g, "").length ?? 0).filter((n) => n > 0);
1839
- const avg = lens.reduce((s, v) => s + v, 0) / (lens.length || 1);
1840
- if (avg < 12) return false;
1841
- }
1842
- return findTwoColumnProseCutX([...r.usedItems]) !== null;
1843
- }
1844
- function findTwoColumnProseCutX(items) {
1845
- const lines = groupByBaseline(items);
1846
- if (lines.length < 8) return null;
1847
- let minX = Infinity;
1848
- let maxX = -Infinity;
1849
- for (const i of items) {
1850
- if (i.x < minX) minX = i.x;
1851
- if (i.x + i.w > maxX) maxX = i.x + i.w;
1852
- }
1853
- if (!Number.isFinite(maxX - minX)) return null;
1854
- if (maxX - minX < 100) return null;
1855
- const lo = minX + (maxX - minX) * 0.3;
1856
- const hi = minX + (maxX - minX) * 0.7;
1857
- const step = Math.max(2, (hi - lo) / 400);
1858
- let cutX = 0;
1859
- let bestCover = Infinity;
1860
- for (let x = lo; x <= hi; x += step) {
1861
- let cover = 0;
1862
- for (const line of lines) {
1863
- if (line.items.some((i) => i.x < x && i.x + i.w > x)) cover++;
1864
- }
1865
- if (cover < bestCover) {
1866
- bestCover = cover;
1867
- cutX = x;
1868
- }
1869
- }
1870
- if (bestCover / lines.length > 0.15) return null;
1871
- const left = [];
1872
- const right = [];
1873
- let twoSide = 0;
1874
- for (const line of lines) {
1875
- let hasL = false;
1876
- let hasR = false;
1877
- for (const i of line.items) {
1878
- if (i.x + i.w <= cutX) {
1879
- left.push(i);
1880
- hasL = true;
1881
- } else if (i.x >= cutX) {
1882
- right.push(i);
1883
- hasR = true;
1884
- }
1885
- }
1886
- if (hasL && hasR) twoSide++;
1887
- }
1888
- if (twoSide / lines.length < 0.55) return null;
1889
- if (left.length < 5 || right.length < 5) return null;
1890
- const allText = items.map((i) => i.text).join("").replace(/\s+/g, "");
1891
- const digits = (allText.match(/[\d,.%△—-]/g) || []).length;
1892
- if (allText.length === 0 || digits / allText.length > 0.15) return null;
1893
- const stats = [left, right].map((side) => {
1894
- let sMinX = Infinity;
1895
- let sMaxR = -Infinity;
1896
- let fsSum = 0;
1897
- const rowEnds = /* @__PURE__ */ new Map();
1898
- const rowLens = /* @__PURE__ */ new Map();
1899
- const rowFirst = /* @__PURE__ */ new Map();
1900
- for (const i of side) {
1901
- if (i.x < sMinX) sMinX = i.x;
1902
- const rgt = i.x + i.w;
1903
- if (rgt > sMaxR) sMaxR = rgt;
1904
- fsSum += i.fontSize;
1905
- const key = Math.round(i.y / 4);
1906
- rowEnds.set(key, Math.max(rowEnds.get(key) ?? -Infinity, rgt));
1907
- rowLens.set(key, (rowLens.get(key) ?? 0) + i.text.replace(/\s+/g, "").length);
1908
- const f = rowFirst.get(key);
1909
- if (!f || i.x < f.x) rowFirst.set(key, i);
1910
- }
1911
- const ends = [...rowEnds.values()].sort((a, b) => a - b);
1912
- const p85 = ends[Math.floor(ends.length * 0.85)] ?? sMaxR;
1913
- const fs = fsSum / side.length;
1914
- const justified = ends.filter((e) => Math.abs(e - p85) <= fs).length / ends.length;
1915
- const lens = [...rowLens.values()];
1916
- const avgLen = lens.reduce((s, v) => s + v, 0) / (lens.length || 1);
1917
- const markers = [...rowFirst.values()].filter((i) => /^[•▪◦‣∙·\-–—□■◇◆▶※*]/.test(i.text)).length;
1918
- return { width: sMaxR - sMinX, justified, avgLen, markerRatio: markers / (rowFirst.size || 1) };
1919
- });
1920
- if (stats.some((s) => s.avgLen < 12)) return null;
1921
- if (stats.some((s) => s.markerRatio > 0.1)) return null;
1922
- const widthSym = Math.min(stats[0].width, stats[1].width) / Math.max(stats[0].width, stats[1].width);
1923
- if (widthSym < 0.6) return null;
1924
- if (Math.min(stats[0].justified, stats[1].justified) < 0.55) return null;
1925
- return cutX;
1926
- }
1927
- function mergeEvenSpacedClusters(items) {
1928
- const originMap = /* @__PURE__ */ new Map();
1929
- const rows = groupByBaseline(items);
1930
- const merged = [];
1931
- for (const row of rows) {
1932
- const sorted = [...row.items].sort((a, b) => a.x - b.x);
1933
- let i = 0;
1934
- while (i < sorted.length) {
1935
- if (/^[가-힣\d]$/.test(sorted[i].text)) {
1936
- let runEnd = i + 1;
1937
- while (runEnd < sorted.length && /^[가-힣\d]$/.test(sorted[runEnd].text)) {
1938
- if (sorted[runEnd].hasSpaceBefore) break;
1939
- const gap = sorted[runEnd].x - (sorted[runEnd - 1].x + sorted[runEnd - 1].w);
1940
- const fs = sorted[runEnd].fontSize;
1941
- if (gap < fs * 0.1 || gap > fs * 3) break;
1942
- runEnd++;
1943
- }
1944
- if (runEnd - i >= 3) {
1945
- const gaps = [];
1946
- for (let g2 = i + 1; g2 < runEnd; g2++) {
1947
- gaps.push(sorted[g2].x - (sorted[g2 - 1].x + sorted[g2 - 1].w));
1948
- }
1949
- let minG = Infinity, maxG = -Infinity;
1950
- for (const g2 of gaps) {
1951
- if (g2 < minG) minG = g2;
1952
- if (g2 > maxG) maxG = g2;
1953
- }
1954
- if (minG > 0 && maxG / minG <= 3) {
1955
- const run = sorted.slice(i, runEnd);
1956
- const text = run.map((r) => r.text).join("");
1957
- const first = run[0], last = run[runEnd - i - 1];
1958
- const item = {
1959
- text,
1960
- x: first.x,
1961
- y: first.y,
1962
- w: last.x + last.w - first.x,
1963
- h: first.h,
1964
- fontSize: first.fontSize,
1965
- fontName: first.fontName
1966
- };
1967
- originMap.set(item, run);
1968
- merged.push(item);
1969
- i = runEnd;
1970
- continue;
1971
- }
1972
- }
1973
- }
1974
- merged.push(sorted[i]);
1975
- i++;
1976
- }
1977
- }
1978
- return { merged, originMap };
1979
- }
1980
- function expandUsedItems(usedItems, originMap) {
1981
- const toAdd = [];
1982
- for (const item of usedItems) {
1983
- const origins = originMap.get(item);
1984
- if (origins) for (const o of origins) toAdd.push(o);
1985
- }
1986
- for (const a of toAdd) usedItems.add(a);
1987
- }
1988
- function detectHeaderRow(rows) {
1989
- const allItems = rows.flatMap((r) => r.items);
1990
- if (allItems.length === 0) return null;
1991
- let allMinX = Infinity, allMaxX = -Infinity;
1992
- for (const i of allItems) {
1993
- if (i.x < allMinX) allMinX = i.x;
1994
- const r = i.x + i.w;
1995
- if (r > allMaxX) allMaxX = r;
1996
- }
1997
- const pageSpan = allMaxX - allMinX;
1998
- if (pageSpan <= 0) return null;
1999
- for (let ri = 0; ri < rows.length; ri++) {
2000
- const row = rows[ri];
2001
- if (row.items.length < MIN_COLS || row.items.length > 6) continue;
2002
- if (row.items.some((i) => i.text.length > 8)) continue;
2003
- if (!row.items.some((i) => /[가-힣]/.test(i.text))) continue;
2004
- if (row.items.some((i) => /^[□■○●·※▶▷◆◇\-]/.test(i.text))) continue;
2005
- const sorted = [...row.items].sort((a, b) => a.x - b.x);
2006
- const xSpan = sorted[sorted.length - 1].x + sorted[sorted.length - 1].w - sorted[0].x;
2007
- if (xSpan / pageSpan < 0.4) continue;
2008
- const avgFs = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
2009
- let hasLargeGap = false;
2010
- for (let i = 1; i < sorted.length; i++) {
2011
- const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
2012
- if (gap >= avgFs * 2.5) {
2013
- hasLargeGap = true;
2014
- break;
2015
- }
2016
- }
2017
- if (!hasLargeGap) continue;
2018
- const columns = sorted.map((item) => ({ x: item.x, count: 0 }));
2019
- let matchCount = 0;
2020
- for (let j = ri + 1; j < rows.length && matchCount < MIN_ROWS + 2; j++) {
2021
- const matched = countMatchedColumnsRange(rows[j], columns, sorted);
2022
- if (matched >= MIN_COLS) matchCount++;
2023
- }
2024
- if (matchCount < MIN_ROWS) continue;
2025
- return { columns, headerIdx: ri };
2026
- }
2027
- return null;
2028
- }
2029
- function mergeOverlappingRows(rows) {
2030
- if (rows.length <= 1) return rows;
2031
- const result = [rows[0]];
2032
- for (let i = 1; i < rows.length; i++) {
2033
- const prev = result[result.length - 1];
2034
- const curr = rows[i];
2035
- const a = rowBand(prev);
2036
- const b = rowBand(curr);
2037
- const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
2038
- const prevIsFrag = isFragmentRow(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
2039
- const currIsFrag = isFragmentRow(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
2040
- if (prevIsFrag || currIsFrag) {
2041
- const baseY = prevIsFrag ? curr.y : prev.y;
2042
- result[result.length - 1] = { y: baseY, items: [...prev.items, ...curr.items] };
2043
- } else {
2044
- result.push(curr);
2045
- }
2046
- }
2047
- return result;
2048
- }
2049
- function isFragmentRow(row) {
2050
- return row.items.length <= 3 && row.items.every((i) => i.text.length <= 8);
2051
- }
2052
- function rowBand(row) {
2053
- let bottom = Infinity, top = -Infinity;
2054
- for (const i of row.items) {
2055
- const h = i.h > 0 ? i.h : i.fontSize;
2056
- if (i.y < bottom) bottom = i.y;
2057
- if (i.y + h > top) top = i.y + h;
2058
- }
2059
- return { bottom, top, height: top - bottom };
2060
- }
2061
- function mergeMultiLineRows(rows, columns) {
2062
- if (rows.length <= 1) return rows;
2063
- const result = [rows[0]];
2064
- const allFontSizes = rows.flatMap((r) => r.items).map((i) => i.fontSize);
2065
- const avgFontSize = allFontSizes.length > 0 ? allFontSizes.reduce((s, v) => s + v, 0) / allFontSizes.length : 12;
2066
- for (let i = 1; i < rows.length; i++) {
2067
- const prev = result[result.length - 1];
2068
- const curr = rows[i];
2069
- const yGap = Math.abs(prev.y - curr.y);
2070
- const matchedCols = countMatchedColumns(curr, columns);
2071
- if (yGap < avgFontSize * 1.8 && curr.items.length <= 2 && (matchedCols < MIN_COLS || curr.items.length === 1)) {
2072
- result[result.length - 1] = {
2073
- y: prev.y,
2074
- items: [...prev.items, ...curr.items]
2075
- };
2076
- } else {
2077
- result.push(curr);
2078
- }
2079
- }
2080
- return result;
2081
- }
2082
- function groupByBaseline(items) {
2083
- if (items.length === 0) return [];
2084
- const sorted = [...items].sort((a, b) => b.y - a.y || a.x - b.x);
2085
- const rows = [];
2086
- let curItems = [sorted[0]];
2087
- let curY = sorted[0].y;
2088
- for (let i = 1; i < sorted.length; i++) {
2089
- if (Math.abs(sorted[i].y - curY) <= Y_TOL) {
2090
- curItems.push(sorted[i]);
2091
- } else {
2092
- rows.push({ y: curY, items: curItems });
2093
- curItems = [sorted[i]];
2094
- curY = sorted[i].y;
1531
+ function detectHeadings(blocks, medianFontSize) {
1532
+ for (const block of blocks) {
1533
+ if (block.type !== "paragraph" || !block.text || !block.style?.fontSize) continue;
1534
+ const text = block.text.trim();
1535
+ if (text.length === 0 || text.length > 200) continue;
1536
+ if (/^\d+$/.test(text)) continue;
1537
+ const ratio = block.style.fontSize / medianFontSize;
1538
+ let level = 0;
1539
+ if (ratio >= HEADING_RATIO_H1) level = 1;
1540
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
1541
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
1542
+ if (level > 0) {
1543
+ block.type = "heading";
1544
+ block.level = level;
1545
+ block.text = collapseEvenSpacing(text);
2095
1546
  }
2096
1547
  }
2097
- if (curItems.length > 0) rows.push({ y: curY, items: curItems });
2098
- return rows;
2099
1548
  }
2100
- function hasSuspiciousGaps(row) {
2101
- if (row.items.length < 2) return false;
2102
- const sorted = [...row.items].sort((a, b) => a.x - b.x);
2103
- if (sorted.length === 2 && sorted[1].text.length > 20) return false;
2104
- const avgFontSize = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
2105
- const minGap = Math.max(avgFontSize * MIN_GAP_FACTOR, MIN_GAP_ABSOLUTE);
2106
- for (let i = 1; i < sorted.length; i++) {
2107
- const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
2108
- if (gap >= minGap) return true;
1549
+ function shouldDemoteTable(table) {
1550
+ const allCells = table.cells.flatMap((row) => row.map((c) => c.text.trim())).filter(Boolean);
1551
+ const allText = allCells.join(" ");
1552
+ if (table.rows >= 2 && table.cols >= 2 && table.cells[0].every((c) => {
1553
+ const t = c.text.trim();
1554
+ return t.length > 0 && t.length <= 12 && !/[□■◆○●▶ㅇ<>]/.test(t);
1555
+ }) && table.cells.slice(1).some((row) => row.some((c) => c.text.trim() !== ""))) return false;
1556
+ if (table.rows <= 3 && table.cols <= 3 && allText.length <= 200) {
1557
+ const totalCells2 = table.rows * table.cols;
1558
+ const emptyCells2 = totalCells2 - allCells.length;
1559
+ if (emptyCells2 >= totalCells2 * 0.3) return true;
1560
+ if (/[□■◆○●▶ㅇ]/.test(allText)) return true;
1561
+ if (/<[^>]+>/.test(allText)) return true;
2109
1562
  }
1563
+ if (allText.length > 200) return false;
1564
+ if (/[□■◆○●▶]/.test(allText) && table.rows <= 3) return true;
1565
+ const totalCells = table.rows * table.cols;
1566
+ const emptyCells = totalCells - allCells.length;
1567
+ if (table.rows <= 2 && emptyCells > totalCells * 0.5) return true;
1568
+ if (table.rows === 1 && !/\d{2,}/.test(allText)) return true;
2110
1569
  return false;
2111
1570
  }
2112
- function extractColumnClusters(rows) {
2113
- const allX = [];
2114
- for (const row of rows) {
2115
- for (const item of row.items) allX.push(item.x);
2116
- }
2117
- if (allX.length === 0) return [];
2118
- allX.sort((a, b) => a - b);
2119
- const clusters = [];
2120
- let clusterStart = 0;
2121
- for (let i = 1; i <= allX.length; i++) {
2122
- if (i === allX.length || allX[i] - allX[i - 1] > COL_CLUSTER_TOL) {
2123
- const slice = allX.slice(clusterStart, i);
2124
- const avg = Math.round(slice.reduce((s, v) => s + v, 0) / slice.length);
2125
- clusters.push({ x: avg, count: slice.length });
2126
- clusterStart = i;
1571
+ function demoteTableToText(table) {
1572
+ const lines = [];
1573
+ for (let r = 0; r < table.rows; r++) {
1574
+ const cells = table.cells[r].map((c) => c.text.trim()).filter(Boolean);
1575
+ if (cells.length === 0) continue;
1576
+ if (table.cols === 2 && cells.length === 2) {
1577
+ lines.push(`${cells[0]} : ${cells[1]}`);
1578
+ } else {
1579
+ lines.push(cells.join(" "));
2127
1580
  }
2128
1581
  }
2129
- const minCount = Math.max(2, Math.floor(rows.length * MIN_COL_FILL_RATIO));
2130
- return clusters.filter((c) => c.count >= minCount).sort((a, b) => a.x - b.x);
1582
+ return lines.join("\n");
2131
1583
  }
2132
- function findTableRegionsByHeader(allRows, columns, headerItems) {
2133
- const regions = [];
2134
- let currentRegion = [];
2135
- let missStreak = 0;
2136
- for (const row of allRows) {
2137
- const matchedCols = countMatchedColumnsRange(row, columns, headerItems);
2138
- if (matchedCols >= MIN_COLS) {
2139
- currentRegion.push(row);
2140
- missStreak = 0;
2141
- } else if (currentRegion.length > 0 && (row.items.length <= 2 || missStreak === 0)) {
2142
- currentRegion.push(row);
2143
- missStreak++;
2144
- } else {
2145
- while (currentRegion.length > 0) {
2146
- const last = currentRegion[currentRegion.length - 1];
2147
- if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
2148
- currentRegion.pop();
2149
- }
2150
- if (currentRegion.length >= MIN_ROWS) {
2151
- regions.push({ rows: [...currentRegion] });
1584
+ function detectMarkerHeadings(blocks) {
1585
+ for (let i = 0; i < blocks.length; i++) {
1586
+ const block = blocks[i];
1587
+ if (block.type !== "paragraph" || !block.text) continue;
1588
+ const text = block.text.trim();
1589
+ if (text.length < 50 && /^[□■◆◇▶]\s*[가-힣]/.test(text)) {
1590
+ block.type = "heading";
1591
+ block.level = 4;
1592
+ continue;
1593
+ }
1594
+ if (/^[가-힣]{2,6}$/.test(text) && block.style?.fontSize) {
1595
+ const prev = blocks[i - 1];
1596
+ const next = blocks[i + 1];
1597
+ const prevIsStructural = !prev || prev.type === "table" || prev.type === "heading" || prev.type === "separator";
1598
+ const nextIsStructural = !next || next.type === "table" || next.type === "heading" || next.type === "paragraph" && next.text && /^[□■◆○●]/.test(next.text.trim());
1599
+ if (prevIsStructural || nextIsStructural) {
1600
+ block.type = "heading";
1601
+ block.level = 3;
2152
1602
  }
2153
- currentRegion = [];
2154
- missStreak = 0;
2155
1603
  }
2156
1604
  }
2157
- while (currentRegion.length > 0) {
2158
- const last = currentRegion[currentRegion.length - 1];
2159
- if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
2160
- currentRegion.pop();
1605
+ }
1606
+ var TABLE_CAPTION_RE = /^[<\[(【〈]?\s*(표|그림|도표|Table|Figure|Fig\.?)\s*[\d①-⑮][\d.\-]*\s*[\])】〉>]?[.:]?\s*/i;
1607
+ var CAPTION_MAX_LENGTH = 100;
1608
+ var CAPTION_MAX_GAP = 30;
1609
+ function detectTableCaptions(blocks) {
1610
+ const isCaptionCandidate = (b, table) => {
1611
+ if (!b || b.type !== "paragraph" || !b.text) return false;
1612
+ if (b.pageNumber !== table.pageNumber) return false;
1613
+ const text = b.text.trim();
1614
+ if (!text || text.length > CAPTION_MAX_LENGTH || text.includes("\n")) return false;
1615
+ if (!TABLE_CAPTION_RE.test(text)) return false;
1616
+ if (b.bbox && table.bbox) {
1617
+ const capTop = b.bbox.y + b.bbox.height;
1618
+ const capBottom = b.bbox.y;
1619
+ const tblTop = table.bbox.y + table.bbox.height;
1620
+ const tblBottom = table.bbox.y;
1621
+ const gap = capBottom >= tblTop ? capBottom - tblTop : tblBottom - capTop;
1622
+ if (gap > CAPTION_MAX_GAP) return false;
1623
+ const overlap = Math.min(b.bbox.x + b.bbox.width, table.bbox.x + table.bbox.width) - Math.max(b.bbox.x, table.bbox.x);
1624
+ if (overlap < Math.min(b.bbox.width, table.bbox.width) * 0.3) return false;
1625
+ }
1626
+ return true;
1627
+ };
1628
+ for (let i = 0; i < blocks.length; i++) {
1629
+ const block = blocks[i];
1630
+ if (block.type !== "table" || !block.table || block.table.caption) continue;
1631
+ if (isCaptionCandidate(blocks[i - 1], block)) {
1632
+ block.table.caption = blocks[i - 1].text.trim();
1633
+ blocks.splice(i - 1, 1);
1634
+ i--;
1635
+ } else if (isCaptionCandidate(blocks[i + 1], block)) {
1636
+ block.table.caption = blocks[i + 1].text.trim();
1637
+ blocks.splice(i + 1, 1);
1638
+ }
2161
1639
  }
2162
- if (currentRegion.length >= MIN_ROWS) {
2163
- regions.push({ rows: currentRegion });
1640
+ }
1641
+ var KOREAN_LIST_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
1642
+ function parseListLabel(text) {
1643
+ let m = text.match(/^(\d{1,2})\.(?!\d)\s+/);
1644
+ if (m) return { family: "arabicDot", ord: parseInt(m[1], 10) };
1645
+ m = text.match(/^([가-하])\.\s+/);
1646
+ if (m) {
1647
+ const idx = KOREAN_LIST_SEQ.indexOf(m[1]);
1648
+ if (idx >= 0) return { family: "korDot", ord: idx + 1 };
2164
1649
  }
2165
- return regions;
1650
+ m = text.match(/^(\d{1,2})\)\s*/);
1651
+ if (m) return { family: "arabicParen", ord: parseInt(m[1], 10) };
1652
+ m = text.match(/^([가-하])\)\s*/);
1653
+ if (m) {
1654
+ const idx = KOREAN_LIST_SEQ.indexOf(m[1]);
1655
+ if (idx >= 0) return { family: "korParen", ord: idx + 1 };
1656
+ }
1657
+ m = text.match(/^([①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮])\s*/);
1658
+ if (m) return { family: "circled", ord: m[1].charCodeAt(0) - 9312 + 1 };
1659
+ return null;
2166
1660
  }
2167
- function findTableRegions(allRows, columns) {
2168
- const regions = [];
2169
- let currentRegion = [];
2170
- for (const row of allRows) {
2171
- const matchedCols = countMatchedColumns(row, columns);
2172
- if (matchedCols >= MIN_COLS) {
2173
- currentRegion.push(row);
2174
- } else if (row.items.length === 1) {
2175
- if (currentRegion.length > 0) {
2176
- currentRegion.push(row);
1661
+ var ATTACHMENT_RE = /^붙\s*임\s*(\d+[.:]?)?\s/;
1662
+ function detectKoreanListBlocks(blocks) {
1663
+ const labeled = [];
1664
+ for (let i = 0; i < blocks.length; i++) {
1665
+ const b = blocks[i];
1666
+ if (b.type !== "paragraph" && b.type !== "list" || !b.text) continue;
1667
+ const label = parseListLabel(b.text.trim());
1668
+ if (label) labeled.push({ idx: i, label });
1669
+ }
1670
+ const validated = /* @__PURE__ */ new Set();
1671
+ const byFamily = /* @__PURE__ */ new Map();
1672
+ for (const l of labeled) {
1673
+ const arr = byFamily.get(l.label.family) || [];
1674
+ arr.push(l);
1675
+ byFamily.set(l.label.family, arr);
1676
+ }
1677
+ for (const arr of byFamily.values()) {
1678
+ let chain = [];
1679
+ for (const item of arr) {
1680
+ const prev = chain[chain.length - 1];
1681
+ if (prev && item.label.ord === prev.label.ord + 1 && item.idx - prev.idx <= 20) {
1682
+ chain.push(item);
1683
+ } else {
1684
+ if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
1685
+ chain = [item];
2177
1686
  }
1687
+ }
1688
+ if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
1689
+ }
1690
+ let familyStack = [];
1691
+ let lastTopLevelList = null;
1692
+ let prevListIdx = -2;
1693
+ const toRemove = /* @__PURE__ */ new Set();
1694
+ for (let i = 0; i < blocks.length; i++) {
1695
+ const b = blocks[i];
1696
+ if (b.type === "table" || b.type === "heading" || b.type === "separator") {
1697
+ familyStack = [];
1698
+ lastTopLevelList = null;
1699
+ continue;
1700
+ }
1701
+ if (b.type !== "paragraph" && b.type !== "list" || !b.text) continue;
1702
+ const text = b.text.trim();
1703
+ if (b.type === "paragraph" && ATTACHMENT_RE.test(text)) {
1704
+ blocks[i] = { ...b, type: "list", listType: "unordered" };
1705
+ continue;
1706
+ }
1707
+ if (!validated.has(i)) continue;
1708
+ const label = parseListLabel(text);
1709
+ let depth = familyStack.indexOf(label.family);
1710
+ if (depth < 0) {
1711
+ familyStack.push(label.family);
1712
+ depth = familyStack.length - 1;
2178
1713
  } else {
2179
- if (currentRegion.length >= MIN_ROWS) {
2180
- regions.push({ rows: [...currentRegion] });
2181
- }
2182
- currentRegion = [];
1714
+ familyStack = familyStack.slice(0, depth + 1);
1715
+ }
1716
+ const listType = label.family === "arabicDot" ? "ordered" : "unordered";
1717
+ const listBlock = { ...b, type: "list", listType };
1718
+ if (depth === 0) {
1719
+ blocks[i] = listBlock;
1720
+ lastTopLevelList = listBlock;
1721
+ } else if (lastTopLevelList && i === prevListIdx + 1) {
1722
+ if (!lastTopLevelList.children) lastTopLevelList.children = [];
1723
+ lastTopLevelList.children.push(listBlock);
1724
+ toRemove.add(i);
1725
+ } else {
1726
+ blocks[i] = listBlock;
1727
+ lastTopLevelList = listBlock;
2183
1728
  }
1729
+ prevListIdx = i;
2184
1730
  }
2185
- if (currentRegion.length >= MIN_ROWS) {
2186
- regions.push({ rows: currentRegion });
1731
+ if (toRemove.size > 0) {
1732
+ const sorted = [...toRemove].sort((a, b) => b - a);
1733
+ for (const idx of sorted) blocks.splice(idx, 1);
2187
1734
  }
2188
- return regions;
2189
1735
  }
2190
- function countMatchedColumns(row, columns) {
2191
- const matched = /* @__PURE__ */ new Set();
2192
- for (const item of row.items) {
2193
- for (let ci = 0; ci < columns.length; ci++) {
2194
- if (Math.abs(item.x - columns[ci].x) <= COL_CLUSTER_TOL * 2) {
2195
- matched.add(ci);
2196
- break;
1736
+ function detectListBlocks(blocks) {
1737
+ const result = [];
1738
+ for (let i = 0; i < blocks.length; i++) {
1739
+ const block = blocks[i];
1740
+ if (block.type === "paragraph" && block.text) {
1741
+ const text = block.text.trim();
1742
+ if (/^\d+\.\s/.test(text)) {
1743
+ result.push({ ...block, type: "list", listType: "ordered", text: block.text });
1744
+ continue;
2197
1745
  }
2198
- }
2199
- }
2200
- return matched.size;
2201
- }
2202
- function countMatchedColumnsRange(row, columns, headerItems) {
2203
- const boundaries = [];
2204
- for (let ci = 0; ci < headerItems.length; ci++) {
2205
- const left = ci === 0 ? 0 : (headerItems[ci - 1].x + headerItems[ci - 1].w + headerItems[ci].x) / 2;
2206
- const right = ci === headerItems.length - 1 ? Infinity : (headerItems[ci].x + headerItems[ci].w + headerItems[ci + 1].x) / 2;
2207
- boundaries.push({ left, right });
2208
- }
2209
- const matched = /* @__PURE__ */ new Set();
2210
- for (const item of row.items) {
2211
- for (let ci = 0; ci < boundaries.length; ci++) {
2212
- if (item.x >= boundaries[ci].left && item.x < boundaries[ci].right) {
2213
- matched.add(ci);
2214
- break;
1746
+ if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
1747
+ result.push({ ...block, type: "list", listType: "unordered", text: block.text });
1748
+ continue;
2215
1749
  }
2216
1750
  }
1751
+ result.push(block);
2217
1752
  }
2218
- return matched.size;
1753
+ return result;
2219
1754
  }
2220
- function assignRowItems(items, columns, numCols) {
2221
- if (items.length === 0) return [];
2222
- const sorted = [...items].sort((a, b) => a.x - b.x);
2223
- const colCenters = columns.map((c) => c.x);
2224
- const gaps = [];
2225
- for (let i = 1; i < sorted.length; i++) {
2226
- gaps.push({ idx: i, size: sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w) });
2227
- }
2228
- const gapSizes = gaps.map((g2) => g2.size).sort((a, b) => a - b);
2229
- const medianGap = gapSizes.length > 0 ? gapSizes[Math.floor(gapSizes.length / 2)] : 0;
2230
- const gapThreshold = sorted.length <= numCols + 1 ? 12 : Math.max(medianGap * 2.5, 12);
2231
- const significantGaps = gaps.filter((g2) => g2.size >= gapThreshold).sort((a, b) => b.size - a.size).slice(0, numCols - 1).sort((a, b) => a.idx - b.idx);
2232
- const groups = [];
2233
- let start = 0;
2234
- for (const gap of significantGaps) {
2235
- groups.push(sorted.slice(start, gap.idx));
2236
- start = gap.idx;
2237
- }
2238
- groups.push(sorted.slice(start));
1755
+ var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
1756
+ var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
1757
+ function detectSpecialKoreanTables(blocks) {
2239
1758
  const result = [];
2240
- const usedCols = /* @__PURE__ */ new Set();
2241
- const groupCenters = groups.map((g2) => {
2242
- let minX = Infinity, maxX = -Infinity;
2243
- for (const i of g2) {
2244
- if (i.x < minX) minX = i.x;
2245
- const r = i.x + i.w;
2246
- if (r > maxX) maxX = r;
1759
+ let kvLines = [];
1760
+ const flushKvTable = () => {
1761
+ if (kvLines.length < 2) {
1762
+ for (const kv of kvLines) result.push(kv.block);
1763
+ kvLines = [];
1764
+ return;
2247
1765
  }
2248
- return (minX + maxX) / 2;
2249
- });
2250
- const assignments = [];
2251
- for (let gi = 0; gi < groups.length; gi++) {
2252
- for (let ci = 0; ci < numCols; ci++) {
2253
- assignments.push({ gi, ci, dist: Math.abs(groupCenters[gi] - colCenters[ci]) });
1766
+ const cells = kvLines.map((kv) => {
1767
+ if (kv.value) {
1768
+ return [
1769
+ { text: kv.key, colSpan: 1, rowSpan: 1 },
1770
+ { text: kv.value, colSpan: 1, rowSpan: 1 }
1771
+ ];
1772
+ }
1773
+ return [
1774
+ { text: kv.key, colSpan: 2, rowSpan: 1 },
1775
+ { text: "", colSpan: 1, rowSpan: 1 }
1776
+ ];
1777
+ });
1778
+ const irTable = {
1779
+ rows: cells.length,
1780
+ cols: 2,
1781
+ cells,
1782
+ hasHeader: true
1783
+ };
1784
+ const firstBlock = kvLines[0].block;
1785
+ result.push({
1786
+ type: "table",
1787
+ table: irTable,
1788
+ pageNumber: firstBlock.pageNumber,
1789
+ bbox: firstBlock.bbox
1790
+ });
1791
+ kvLines = [];
1792
+ };
1793
+ for (const block of blocks) {
1794
+ if (block.type !== "paragraph" || !block.text) {
1795
+ flushKvTable();
1796
+ result.push(block);
1797
+ continue;
2254
1798
  }
2255
- }
2256
- assignments.sort((a, b) => a.dist - b.dist);
2257
- const assignedGroups = /* @__PURE__ */ new Set();
2258
- for (const { gi, ci } of assignments) {
2259
- if (assignedGroups.has(gi) || usedCols.has(ci)) continue;
2260
- result.push({ col: ci, items: groups[gi] });
2261
- assignedGroups.add(gi);
2262
- usedCols.add(ci);
2263
- }
2264
- for (let gi = 0; gi < groups.length; gi++) {
2265
- if (assignedGroups.has(gi)) continue;
2266
- let bestCol = 0, bestDist = Infinity;
2267
- for (let ci = 0; ci < numCols; ci++) {
2268
- const d = Math.abs(groupCenters[gi] - colCenters[ci]);
2269
- if (d < bestDist) {
2270
- bestDist = d;
2271
- bestCol = ci;
1799
+ const text = block.text.trim();
1800
+ if (KOREAN_TABLE_HEADER_RE.test(text)) {
1801
+ const colonIdx = text.indexOf(":");
1802
+ if (colonIdx >= 0) {
1803
+ kvLines.push({
1804
+ key: text.slice(0, colonIdx).trim(),
1805
+ value: text.slice(colonIdx + 1).trim(),
1806
+ block
1807
+ });
1808
+ } else {
1809
+ const spaceIdx = text.search(/\s/);
1810
+ if (spaceIdx > 0) {
1811
+ kvLines.push({
1812
+ key: text.slice(0, spaceIdx).trim(),
1813
+ value: text.slice(spaceIdx + 1).trim(),
1814
+ block
1815
+ });
1816
+ } else {
1817
+ kvLines.push({ key: text, value: "", block });
1818
+ }
2272
1819
  }
1820
+ continue;
2273
1821
  }
2274
- result.push({ col: bestCol, items: groups[gi] });
1822
+ if (kvLines.length > 0 && text.includes(":")) {
1823
+ if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
1824
+ const colonIdx = text.indexOf(":");
1825
+ const key = text.slice(0, colonIdx).trim();
1826
+ if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
1827
+ kvLines.push({
1828
+ key,
1829
+ value: text.slice(colonIdx + 1).trim(),
1830
+ block
1831
+ });
1832
+ continue;
1833
+ }
1834
+ }
1835
+ }
1836
+ flushKvTable();
1837
+ result.push(block);
2275
1838
  }
1839
+ flushKvTable();
2276
1840
  return result;
2277
1841
  }
2278
- function buildClusterTable(rows, columns, pageNum) {
2279
- const numCols = columns.length;
2280
- const numRows = rows.length;
2281
- if (numRows < MIN_ROWS || numCols < MIN_COLS) return null;
2282
- const cells = Array.from(
2283
- { length: numRows },
2284
- () => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
2285
- );
2286
- const usedItems = /* @__PURE__ */ new Set();
2287
- for (let r = 0; r < numRows; r++) {
2288
- const row = rows[r];
2289
- if (row.items.length === 1 && numCols > 1) {
2290
- cells[r][0] = { text: row.items[0].text, colSpan: numCols, rowSpan: 1 };
2291
- usedItems.add(row.items[0]);
2292
- continue;
1842
+ function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
1843
+ const ZONE_RATIO = 0.12;
1844
+ const MIN_REPEAT = 3;
1845
+ const topEntries = [];
1846
+ const bottomEntries = [];
1847
+ for (let bi = 0; bi < blocks.length; bi++) {
1848
+ const b = blocks[bi];
1849
+ if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
1850
+ const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
1851
+ if (!ph) continue;
1852
+ const blockTop = ph - (b.bbox.y + b.bbox.height);
1853
+ const blockBottom = ph - b.bbox.y;
1854
+ const entry = { blockIdx: bi, page: b.pageNumber, text: b.text.trim() };
1855
+ if (blockBottom <= ph * ZONE_RATIO) topEntries.push(entry);
1856
+ else if (blockTop >= ph * (1 - ZONE_RATIO)) bottomEntries.push(entry);
1857
+ }
1858
+ const removeSet = /* @__PURE__ */ new Set();
1859
+ for (const entries of [topEntries, bottomEntries]) {
1860
+ if (entries.length === 0) continue;
1861
+ const patternCount = /* @__PURE__ */ new Map();
1862
+ const patternPages = /* @__PURE__ */ new Map();
1863
+ for (const e of entries) {
1864
+ const norm = e.text.replace(/\d+/g, "#");
1865
+ patternCount.set(norm, (patternCount.get(norm) || 0) + 1);
1866
+ const pages = patternPages.get(norm) || /* @__PURE__ */ new Set();
1867
+ pages.add(e.page);
1868
+ patternPages.set(norm, pages);
2293
1869
  }
2294
- const assignments = assignRowItems(row.items, columns, numCols);
2295
- for (const { col, items } of assignments) {
2296
- const text = items.map((i) => i.text).join(" ");
2297
- const existing = cells[r][col].text;
2298
- cells[r][col].text = existing ? existing + " " + text : text;
2299
- for (const item of items) usedItems.add(item);
1870
+ const repeatedPatterns = /* @__PURE__ */ new Set();
1871
+ for (const [p, count] of patternCount) {
1872
+ if (count >= MIN_REPEAT && (patternPages.get(p)?.size ?? 0) >= MIN_REPEAT) {
1873
+ repeatedPatterns.add(p);
1874
+ }
1875
+ }
1876
+ for (const e of entries) {
1877
+ const norm = e.text.replace(/\d+/g, "#");
1878
+ if (!repeatedPatterns.has(norm)) continue;
1879
+ const cand = blocks[e.blockIdx];
1880
+ const cb = cand.bbox;
1881
+ let sharesLine = false;
1882
+ for (let bi = 0; bi < blocks.length; bi++) {
1883
+ if (bi === e.blockIdx) continue;
1884
+ const o = blocks[bi];
1885
+ if (!o.bbox || o.bbox.page !== cb.page) continue;
1886
+ const oNorm = o.text?.trim().replace(/\d+/g, "#");
1887
+ if (oNorm && repeatedPatterns.has(oNorm)) continue;
1888
+ const overlap = Math.min(cb.y + cb.height, o.bbox.y + o.bbox.height) - Math.max(cb.y, o.bbox.y);
1889
+ if (overlap >= cb.height * 0.5) {
1890
+ sharesLine = true;
1891
+ break;
1892
+ }
1893
+ }
1894
+ if (!sharesLine) removeSet.add(e.blockIdx);
2300
1895
  }
2301
1896
  }
2302
- let emptyRows = 0;
2303
- for (const row of cells) {
2304
- if (row.every((c) => c.text === "")) emptyRows++;
2305
- }
2306
- if (emptyRows > numRows * 0.5) return null;
2307
- for (let c = 0; c < numCols; c++) {
2308
- const hasValue = cells.some((row) => row[c].text !== "");
2309
- if (!hasValue) return null;
1897
+ if (removeSet.size > 0) {
1898
+ warnings.push({ message: `${removeSet.size}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
2310
1899
  }
2311
- for (let r = numRows - 1; r >= 1; r--) {
2312
- const nonEmptyCols = cells[r].filter((c) => c.text.trim()).length;
2313
- if (nonEmptyCols !== 1) continue;
2314
- if (cells[r][0].text.trim() !== "") continue;
2315
- const contentText = cells[r].find((c) => c.text.trim())?.text.trim() || "";
2316
- if (/^[○●▶\-·]/.test(contentText)) continue;
2317
- for (let pr = r - 1; pr >= 0; pr--) {
2318
- if (cells[pr].some((c) => c.text.trim())) {
2319
- for (let c = 0; c < numCols; c++) {
2320
- const prev = cells[pr][c].text.trim();
2321
- const curr = cells[r][c].text.trim();
2322
- if (curr) cells[pr][c].text = prev ? prev + " " + curr : curr;
2323
- }
2324
- for (let c = 0; c < numCols; c++) cells[r][c].text = "";
2325
- break;
1900
+ return [...removeSet].sort((a, b) => a - b);
1901
+ }
1902
+
1903
+ // src/pdf/cluster-detector.ts
1904
+ var Y_TOL = 3;
1905
+ var COL_CLUSTER_TOL = 15;
1906
+ var MIN_ROWS = 3;
1907
+ var MIN_COLS = 2;
1908
+ var MIN_GAP_FACTOR = 2;
1909
+ var MIN_GAP_ABSOLUTE = 20;
1910
+ var MIN_COL_FILL_RATIO = 0.4;
1911
+ function detectClusterTables(items, pageNum) {
1912
+ if (items.length < MIN_ROWS * MIN_COLS) return [];
1913
+ const { merged, originMap } = mergeEvenSpacedClusters(items);
1914
+ const rows = mergeOverlappingRows(groupByBaseline(merged));
1915
+ if (rows.length < MIN_ROWS) return [];
1916
+ const results = [];
1917
+ const headerResult = detectHeaderRow(rows);
1918
+ if (headerResult) {
1919
+ const { columns, headerIdx } = headerResult;
1920
+ const headerRow = rows[headerIdx];
1921
+ const headerItems = [...headerRow.items].sort((a, b) => a.x - b.x);
1922
+ const headerAndBelow = rows.slice(headerIdx);
1923
+ const mergedRows = mergeMultiLineRows(headerAndBelow, columns);
1924
+ const tableRegions = findTableRegionsByHeader(mergedRows, columns, headerItems);
1925
+ for (const region of tableRegions) {
1926
+ const table = buildClusterTable(region.rows, columns, pageNum);
1927
+ if (table) {
1928
+ expandUsedItems(table.usedItems, originMap);
1929
+ results.push(table);
2326
1930
  }
2327
1931
  }
2328
1932
  }
2329
- for (let r = 0; r < cells.length - 1; r++) {
2330
- const row = cells[r];
2331
- const hasCol0 = row[0].text.trim() !== "";
2332
- const hasColLast = numCols > 1 && row[numCols - 1].text.trim() !== "";
2333
- const midEmpty = row.slice(1, numCols - 1).every((c) => c.text.trim() === "");
2334
- if (hasCol0 && hasColLast && midEmpty) {
2335
- const next = cells[r + 1];
2336
- if (next[0].text.trim() === "" && next.some((c) => c.text.trim())) {
2337
- for (let c = 1; c < numCols; c++) {
2338
- const curr = next[c].text.trim();
2339
- if (curr) row[c].text = row[c].text.trim() ? row[c].text.trim() + " " + curr : curr;
1933
+ if (results.length === 0) {
1934
+ const suspiciousRows = rows.filter((row) => hasSuspiciousGaps(row));
1935
+ if (suspiciousRows.length >= MIN_ROWS) {
1936
+ const columns = extractColumnClusters(suspiciousRows);
1937
+ if (columns.length >= MIN_COLS) {
1938
+ const tableRegions = findTableRegions(rows, columns);
1939
+ for (const region of tableRegions) {
1940
+ const mergedRows = mergeMultiLineRows(region.rows, columns);
1941
+ const table = buildClusterTable(mergedRows, columns, pageNum);
1942
+ if (table) {
1943
+ expandUsedItems(table.usedItems, originMap);
1944
+ results.push(table);
1945
+ }
2340
1946
  }
2341
- for (let c = 0; c < numCols; c++) next[c].text = "";
2342
1947
  }
2343
1948
  }
2344
1949
  }
2345
- const filteredCells = cells.filter((row) => row.some((c) => c.text.trim()));
2346
- const finalRowCount = filteredCells.length;
2347
- if (finalRowCount < MIN_ROWS) return null;
2348
- const irTable = {
2349
- rows: finalRowCount,
2350
- cols: numCols,
2351
- cells: filteredCells,
2352
- hasHeader: finalRowCount > 1
2353
- };
2354
- const allItems = rows.flatMap((r) => r.items);
2355
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2356
- for (const i of allItems) {
1950
+ return results.filter((r) => !isTwoColumnProse(r));
1951
+ }
1952
+ function isTwoColumnProse(r) {
1953
+ const t = r.table;
1954
+ if (t.rows < 8) return false;
1955
+ const dense = [];
1956
+ for (let c = 0; c < t.cols; c++) {
1957
+ const filled = t.cells.filter((row) => row[c]?.text.trim()).length;
1958
+ if (filled / t.rows >= 0.3) dense.push(c);
1959
+ }
1960
+ if (dense.length !== 2) return false;
1961
+ for (const c of dense) {
1962
+ const lens = t.cells.map((row) => row[c]?.text.replace(/\s+/g, "").length ?? 0).filter((n) => n > 0);
1963
+ const avg = lens.reduce((s, v) => s + v, 0) / (lens.length || 1);
1964
+ if (avg < 12) return false;
1965
+ }
1966
+ return findTwoColumnProseCutX([...r.usedItems]) !== null;
1967
+ }
1968
+ function findTwoColumnProseCutX(items) {
1969
+ const lines = groupByBaseline(items);
1970
+ if (lines.length < 8) return null;
1971
+ let minX = Infinity;
1972
+ let maxX = -Infinity;
1973
+ for (const i of items) {
2357
1974
  if (i.x < minX) minX = i.x;
2358
- if (i.y < minY) minY = i.y;
2359
1975
  if (i.x + i.w > maxX) maxX = i.x + i.w;
2360
- const h = i.h > 0 ? i.h : i.fontSize;
2361
- if (i.y + h > maxY) maxY = i.y + h;
2362
1976
  }
2363
- return {
2364
- table: irTable,
2365
- bbox: { page: pageNum, x: minX, y: minY, width: maxX - minX, height: maxY - minY },
2366
- usedItems
2367
- };
2368
- }
2369
-
2370
- // src/pdf/xy-cut.ts
2371
- var MAX_XYCUT_DEPTH = 50;
2372
- var XYCUT_MIN_GAP = 5;
2373
- var CROSS_LAYOUT_BETA = 2;
2374
- var CROSS_OVERLAP_RATIO = 0.1;
2375
- var CROSS_MIN_OVERLAPS = 2;
2376
- var CROSS_MAX_MASK_RATIO = 0.2;
2377
- var NARROW_ELEMENT_WIDTH_RATIO = 0.1;
2378
- function xyCutOrder(items, gapThreshold, depth = 0) {
2379
- if (items.length === 0) return [];
2380
- if (items.length <= 2 || depth >= MAX_XYCUT_DEPTH) return [items];
2381
- if (depth === 0 && items.length >= 3) {
2382
- const cross = identifyCrossLayoutItems(items);
2383
- if (cross.size > 0 && cross.size <= items.length * CROSS_MAX_MASK_RATIO) {
2384
- const rest = items.filter((i) => !cross.has(i));
2385
- if (rest.length > 0) {
2386
- const groups = xyCutOrder(rest, gapThreshold, 1);
2387
- return mergeCrossLayoutGroups(groups, [...cross]);
1977
+ if (!Number.isFinite(maxX - minX)) return null;
1978
+ if (maxX - minX < 100) return null;
1979
+ const lo = minX + (maxX - minX) * 0.3;
1980
+ const hi = minX + (maxX - minX) * 0.7;
1981
+ const step = Math.max(2, (hi - lo) / 400);
1982
+ let cutX = 0;
1983
+ let bestCover = Infinity;
1984
+ for (let x = lo; x <= hi; x += step) {
1985
+ let cover = 0;
1986
+ for (const line of lines) {
1987
+ if (line.items.some((i) => i.x < x && i.x + i.w > x)) cover++;
1988
+ }
1989
+ if (cover < bestCover) {
1990
+ bestCover = cover;
1991
+ cutX = x;
1992
+ }
1993
+ }
1994
+ if (bestCover / lines.length > 0.15) return null;
1995
+ const left = [];
1996
+ const right = [];
1997
+ let twoSide = 0;
1998
+ for (const line of lines) {
1999
+ let hasL = false;
2000
+ let hasR = false;
2001
+ for (const i of line.items) {
2002
+ if (i.x + i.w <= cutX) {
2003
+ left.push(i);
2004
+ hasL = true;
2005
+ } else if (i.x >= cutX) {
2006
+ right.push(i);
2007
+ hasR = true;
2388
2008
  }
2389
2009
  }
2010
+ if (hasL && hasR) twoSide++;
2390
2011
  }
2391
- const minGap = Math.max(XYCUT_MIN_GAP, gapThreshold);
2392
- const hCut = findHorizontalCut(items);
2393
- const vCut = findVerticalCutWithOutlierFilter(items, minGap);
2394
- const hValid = hCut.gap >= minGap;
2395
- const vValid = vCut.gap >= minGap;
2396
- let useHorizontal;
2397
- if (hValid && vValid) useHorizontal = vCut.gap <= hCut.gap * 1.5;
2398
- else if (hValid) useHorizontal = true;
2399
- else if (vValid) useHorizontal = false;
2400
- else return [items];
2401
- if (useHorizontal) {
2402
- const upper = items.filter((i) => i.y > hCut.position);
2403
- const lower = items.filter((i) => i.y <= hCut.position);
2404
- if (upper.length > 0 && lower.length > 0 && upper.length < items.length) {
2405
- return [...xyCutOrder(upper, gapThreshold, depth + 1), ...xyCutOrder(lower, gapThreshold, depth + 1)];
2012
+ if (twoSide / lines.length < 0.55) return null;
2013
+ if (left.length < 5 || right.length < 5) return null;
2014
+ const allText = items.map((i) => i.text).join("").replace(/\s+/g, "");
2015
+ const digits = (allText.match(/[\d,.%△—-]/g) || []).length;
2016
+ if (allText.length === 0 || digits / allText.length > 0.15) return null;
2017
+ const stats = [left, right].map((side) => {
2018
+ let sMinX = Infinity;
2019
+ let sMaxR = -Infinity;
2020
+ let fsSum = 0;
2021
+ const rowEnds = /* @__PURE__ */ new Map();
2022
+ const rowLens = /* @__PURE__ */ new Map();
2023
+ const rowFirst = /* @__PURE__ */ new Map();
2024
+ for (const i of side) {
2025
+ if (i.x < sMinX) sMinX = i.x;
2026
+ const rgt = i.x + i.w;
2027
+ if (rgt > sMaxR) sMaxR = rgt;
2028
+ fsSum += i.fontSize;
2029
+ const key = Math.round(i.y / 4);
2030
+ rowEnds.set(key, Math.max(rowEnds.get(key) ?? -Infinity, rgt));
2031
+ rowLens.set(key, (rowLens.get(key) ?? 0) + i.text.replace(/\s+/g, "").length);
2032
+ const f = rowFirst.get(key);
2033
+ if (!f || i.x < f.x) rowFirst.set(key, i);
2406
2034
  }
2407
- } else {
2408
- const left = items.filter((i) => i.x + i.w / 2 < vCut.position);
2409
- const right = items.filter((i) => i.x + i.w / 2 >= vCut.position);
2410
- if (left.length > 0 && right.length > 0 && left.length < items.length) {
2411
- return [...xyCutOrder(left, gapThreshold, depth + 1), ...xyCutOrder(right, gapThreshold, depth + 1)];
2035
+ const ends = [...rowEnds.values()].sort((a, b) => a - b);
2036
+ const p85 = ends[Math.floor(ends.length * 0.85)] ?? sMaxR;
2037
+ const fs = fsSum / side.length;
2038
+ const justified = ends.filter((e) => Math.abs(e - p85) <= fs).length / ends.length;
2039
+ const lens = [...rowLens.values()];
2040
+ const avgLen = lens.reduce((s, v) => s + v, 0) / (lens.length || 1);
2041
+ const markers = [...rowFirst.values()].filter((i) => /^[•▪◦‣∙·\-–—□■◇◆▶※*]/.test(i.text)).length;
2042
+ return { width: sMaxR - sMinX, justified, avgLen, markerRatio: markers / (rowFirst.size || 1) };
2043
+ });
2044
+ if (stats.some((s) => s.avgLen < 12)) return null;
2045
+ if (stats.some((s) => s.markerRatio > 0.1)) return null;
2046
+ const widthSym = Math.min(stats[0].width, stats[1].width) / Math.max(stats[0].width, stats[1].width);
2047
+ if (widthSym < 0.6) return null;
2048
+ if (Math.min(stats[0].justified, stats[1].justified) < 0.55) return null;
2049
+ return cutX;
2050
+ }
2051
+ function mergeEvenSpacedClusters(items) {
2052
+ const originMap = /* @__PURE__ */ new Map();
2053
+ const rows = groupByBaseline(items);
2054
+ const merged = [];
2055
+ for (const row of rows) {
2056
+ const sorted = [...row.items].sort((a, b) => a.x - b.x);
2057
+ let i = 0;
2058
+ while (i < sorted.length) {
2059
+ if (/^[가-힣\d]$/.test(sorted[i].text)) {
2060
+ let runEnd = i + 1;
2061
+ while (runEnd < sorted.length && /^[가-힣\d]$/.test(sorted[runEnd].text)) {
2062
+ if (sorted[runEnd].hasSpaceBefore) break;
2063
+ const gap = sorted[runEnd].x - (sorted[runEnd - 1].x + sorted[runEnd - 1].w);
2064
+ const fs = sorted[runEnd].fontSize;
2065
+ if (gap < fs * 0.1 || gap > fs * 3) break;
2066
+ runEnd++;
2067
+ }
2068
+ if (runEnd - i >= 3) {
2069
+ const gaps = [];
2070
+ for (let g = i + 1; g < runEnd; g++) {
2071
+ gaps.push(sorted[g].x - (sorted[g - 1].x + sorted[g - 1].w));
2072
+ }
2073
+ let minG = Infinity, maxG = -Infinity;
2074
+ for (const g of gaps) {
2075
+ if (g < minG) minG = g;
2076
+ if (g > maxG) maxG = g;
2077
+ }
2078
+ if (minG > 0 && maxG / minG <= 3) {
2079
+ const run = sorted.slice(i, runEnd);
2080
+ const text = run.map((r) => r.text).join("");
2081
+ const first = run[0], last = run[runEnd - i - 1];
2082
+ const item = {
2083
+ text,
2084
+ x: first.x,
2085
+ y: first.y,
2086
+ w: last.x + last.w - first.x,
2087
+ h: first.h,
2088
+ fontSize: first.fontSize,
2089
+ fontName: first.fontName
2090
+ };
2091
+ originMap.set(item, run);
2092
+ merged.push(item);
2093
+ i = runEnd;
2094
+ continue;
2095
+ }
2096
+ }
2097
+ }
2098
+ merged.push(sorted[i]);
2099
+ i++;
2412
2100
  }
2413
2101
  }
2414
- return [items];
2102
+ return { merged, originMap };
2415
2103
  }
2416
- function identifyCrossLayoutItems(items) {
2417
- const cross = /* @__PURE__ */ new Set();
2418
- if (items.length < 3) return cross;
2419
- let maxWidth = 0;
2420
- for (const i of items) {
2421
- if (i.w > maxWidth) maxWidth = i.w;
2104
+ function expandUsedItems(usedItems, originMap) {
2105
+ const toAdd = [];
2106
+ for (const item of usedItems) {
2107
+ const origins = originMap.get(item);
2108
+ if (origins) for (const o of origins) toAdd.push(o);
2422
2109
  }
2423
- const threshold = CROSS_LAYOUT_BETA * maxWidth;
2424
- for (const item of items) {
2425
- if (item.w < threshold) continue;
2426
- let overlaps = 0;
2427
- for (const other of items) {
2428
- if (other === item) continue;
2429
- const left = Math.max(item.x, other.x);
2430
- const right = Math.min(item.x + item.w, other.x + other.w);
2431
- const overlapW = right - left;
2432
- if (overlapW <= 0) continue;
2433
- const smaller = Math.min(item.w, other.w);
2434
- if (smaller > 0 && overlapW / smaller >= CROSS_OVERLAP_RATIO) {
2435
- overlaps++;
2436
- if (overlaps >= CROSS_MIN_OVERLAPS) break;
2110
+ for (const a of toAdd) usedItems.add(a);
2111
+ }
2112
+ function detectHeaderRow(rows) {
2113
+ const allItems = rows.flatMap((r) => r.items);
2114
+ if (allItems.length === 0) return null;
2115
+ let allMinX = Infinity, allMaxX = -Infinity;
2116
+ for (const i of allItems) {
2117
+ if (i.x < allMinX) allMinX = i.x;
2118
+ const r = i.x + i.w;
2119
+ if (r > allMaxX) allMaxX = r;
2120
+ }
2121
+ const pageSpan = allMaxX - allMinX;
2122
+ if (pageSpan <= 0) return null;
2123
+ for (let ri = 0; ri < rows.length; ri++) {
2124
+ const row = rows[ri];
2125
+ if (row.items.length < MIN_COLS || row.items.length > 6) continue;
2126
+ if (row.items.some((i) => i.text.length > 8)) continue;
2127
+ if (!row.items.some((i) => /[가-힣]/.test(i.text))) continue;
2128
+ if (row.items.some((i) => /^[□■○●·※▶▷◆◇\-]/.test(i.text))) continue;
2129
+ const sorted = [...row.items].sort((a, b) => a.x - b.x);
2130
+ const xSpan = sorted[sorted.length - 1].x + sorted[sorted.length - 1].w - sorted[0].x;
2131
+ if (xSpan / pageSpan < 0.4) continue;
2132
+ const avgFs = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
2133
+ let hasLargeGap = false;
2134
+ for (let i = 1; i < sorted.length; i++) {
2135
+ const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
2136
+ if (gap >= avgFs * 2.5) {
2137
+ hasLargeGap = true;
2138
+ break;
2437
2139
  }
2438
2140
  }
2439
- if (overlaps >= CROSS_MIN_OVERLAPS) cross.add(item);
2141
+ if (!hasLargeGap) continue;
2142
+ const columns = sorted.map((item) => ({ x: item.x, count: 0 }));
2143
+ let matchCount = 0;
2144
+ for (let j = ri + 1; j < rows.length && matchCount < MIN_ROWS + 2; j++) {
2145
+ const matched = countMatchedColumnsRange(rows[j], columns, sorted);
2146
+ if (matched >= MIN_COLS) matchCount++;
2147
+ }
2148
+ if (matchCount < MIN_ROWS) continue;
2149
+ return { columns, headerIdx: ri };
2440
2150
  }
2441
- return cross;
2151
+ return null;
2442
2152
  }
2443
- function mergeCrossLayoutGroups(groups, cross) {
2444
- if (cross.length === 0) return groups;
2445
- const sortedCross = [...cross].sort((a, b) => b.y + b.h - (a.y + a.h) || a.x - b.x);
2446
- const groupTop = (g2) => {
2447
- let top = -Infinity;
2448
- for (const i of g2) {
2449
- const t = i.y + i.h;
2450
- if (t > top) top = t;
2451
- }
2452
- return top;
2453
- };
2454
- const result = [];
2455
- let gi = 0, ci = 0;
2456
- while (gi < groups.length || ci < sortedCross.length) {
2457
- if (ci >= sortedCross.length) {
2458
- result.push(groups[gi++]);
2459
- continue;
2460
- }
2461
- if (gi >= groups.length) {
2462
- result.push([sortedCross[ci++]]);
2463
- continue;
2153
+ function mergeOverlappingRows(rows) {
2154
+ if (rows.length <= 1) return rows;
2155
+ const result = [rows[0]];
2156
+ for (let i = 1; i < rows.length; i++) {
2157
+ const prev = result[result.length - 1];
2158
+ const curr = rows[i];
2159
+ const a = rowBand(prev);
2160
+ const b = rowBand(curr);
2161
+ const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
2162
+ const prevIsFrag = isFragmentRow(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
2163
+ const currIsFrag = isFragmentRow(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
2164
+ if (prevIsFrag || currIsFrag) {
2165
+ const baseY = prevIsFrag ? curr.y : prev.y;
2166
+ result[result.length - 1] = { y: baseY, items: [...prev.items, ...curr.items] };
2167
+ } else {
2168
+ result.push(curr);
2464
2169
  }
2465
- const crossTop = sortedCross[ci].y + sortedCross[ci].h;
2466
- if (crossTop >= groupTop(groups[gi])) result.push([sortedCross[ci++]]);
2467
- else result.push(groups[gi++]);
2468
2170
  }
2469
2171
  return result;
2470
2172
  }
2471
- function findHorizontalCut(items) {
2472
- if (items.length < 2) return { position: 0, gap: 0 };
2473
- const sorted = [...items].sort((a, b) => b.y - a.y);
2474
- let largestGap = 0;
2475
- let position = 0;
2476
- for (let i = 1; i < sorted.length; i++) {
2477
- const prevBottom = sorted[i - 1].y - sorted[i - 1].h;
2478
- const currTop = sorted[i].y;
2479
- const gap = prevBottom - currTop;
2480
- if (gap > largestGap) {
2481
- largestGap = gap;
2482
- position = (prevBottom + currTop) / 2;
2483
- }
2173
+ function isFragmentRow(row) {
2174
+ return row.items.length <= 3 && row.items.every((i) => i.text.length <= 8);
2175
+ }
2176
+ function rowBand(row) {
2177
+ let bottom = Infinity, top = -Infinity;
2178
+ for (const i of row.items) {
2179
+ const h = i.h > 0 ? i.h : i.fontSize;
2180
+ if (i.y < bottom) bottom = i.y;
2181
+ if (i.y + h > top) top = i.y + h;
2484
2182
  }
2485
- return { position, gap: largestGap };
2183
+ return { bottom, top, height: top - bottom };
2486
2184
  }
2487
- function findVerticalCutWithOutlierFilter(items, minGap) {
2488
- const edgeCut = findVerticalCut(items);
2489
- if (edgeCut.gap >= minGap) return edgeCut;
2490
- if (items.length >= 3) {
2491
- let minX = Infinity, maxX = -Infinity;
2492
- for (const i of items) {
2493
- if (i.x < minX) minX = i.x;
2494
- const r = i.x + i.w;
2495
- if (r > maxX) maxX = r;
2496
- }
2497
- const narrowThreshold = (maxX - minX) * NARROW_ELEMENT_WIDTH_RATIO;
2498
- const filtered = items.filter((i) => i.w >= narrowThreshold);
2499
- if (filtered.length >= 2 && filtered.length < items.length && filtered.length >= items.length * 0.7) {
2500
- const filteredCut = findVerticalCut(filtered);
2501
- if (filteredCut.gap > edgeCut.gap && filteredCut.gap >= minGap) {
2502
- return filteredCut;
2503
- }
2185
+ function mergeMultiLineRows(rows, columns) {
2186
+ if (rows.length <= 1) return rows;
2187
+ const result = [rows[0]];
2188
+ const allFontSizes = rows.flatMap((r) => r.items).map((i) => i.fontSize);
2189
+ const avgFontSize = allFontSizes.length > 0 ? allFontSizes.reduce((s, v) => s + v, 0) / allFontSizes.length : 12;
2190
+ for (let i = 1; i < rows.length; i++) {
2191
+ const prev = result[result.length - 1];
2192
+ const curr = rows[i];
2193
+ const yGap = Math.abs(prev.y - curr.y);
2194
+ const matchedCols = countMatchedColumns(curr, columns);
2195
+ if (yGap < avgFontSize * 1.8 && curr.items.length <= 2 && (matchedCols < MIN_COLS || curr.items.length === 1)) {
2196
+ result[result.length - 1] = {
2197
+ y: prev.y,
2198
+ items: [...prev.items, ...curr.items]
2199
+ };
2200
+ } else {
2201
+ result.push(curr);
2504
2202
  }
2505
2203
  }
2506
- return edgeCut;
2204
+ return result;
2507
2205
  }
2508
- function findVerticalCut(items) {
2509
- if (items.length < 2) return { position: 0, gap: 0 };
2510
- const sorted = [...items].sort((a, b) => a.x - b.x || a.x + a.w - (b.x + b.w));
2511
- let largestGap = 0;
2512
- let position = 0;
2513
- let prevRight = null;
2514
- for (const it of sorted) {
2515
- const left = it.x;
2516
- const right = it.x + it.w;
2517
- if (prevRight !== null && left > prevRight) {
2518
- const gap = left - prevRight;
2519
- if (gap > largestGap) {
2520
- largestGap = gap;
2521
- position = (prevRight + left) / 2;
2522
- }
2206
+ function groupByBaseline(items) {
2207
+ if (items.length === 0) return [];
2208
+ const sorted = [...items].sort((a, b) => b.y - a.y || a.x - b.x);
2209
+ const rows = [];
2210
+ let curItems = [sorted[0]];
2211
+ let curY = sorted[0].y;
2212
+ for (let i = 1; i < sorted.length; i++) {
2213
+ if (Math.abs(sorted[i].y - curY) <= Y_TOL) {
2214
+ curItems.push(sorted[i]);
2215
+ } else {
2216
+ rows.push({ y: curY, items: curItems });
2217
+ curItems = [sorted[i]];
2218
+ curY = sorted[i].y;
2523
2219
  }
2524
- prevRight = prevRight === null ? right : Math.max(prevRight, right);
2525
2220
  }
2526
- return { position, gap: largestGap };
2221
+ if (curItems.length > 0) rows.push({ y: curY, items: curItems });
2222
+ return rows;
2527
2223
  }
2528
-
2529
- // src/pdf/columns.ts
2530
- function isProseSpread(items) {
2531
- if (items.length < 4) return false;
2532
- const sorted = [...items].sort((a, b) => a.x - b.x);
2533
- const gaps = [];
2224
+ function hasSuspiciousGaps(row) {
2225
+ if (row.items.length < 2) return false;
2226
+ const sorted = [...row.items].sort((a, b) => a.x - b.x);
2227
+ if (sorted.length === 2 && sorted[1].text.length > 20) return false;
2228
+ const avgFontSize = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
2229
+ const minGap = Math.max(avgFontSize * MIN_GAP_FACTOR, MIN_GAP_ABSOLUTE);
2534
2230
  for (let i = 1; i < sorted.length; i++) {
2535
- gaps.push(sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w));
2231
+ const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
2232
+ if (gap >= minGap) return true;
2536
2233
  }
2537
- const maxGap = safeMax(gaps);
2538
- const avgLen = items.reduce((s, i) => s + i.text.length, 0) / items.length;
2539
- return maxGap < 40 && avgLen < 5;
2234
+ return false;
2540
2235
  }
2541
- function detectColumns(yLines) {
2542
- const allItems = yLines.flat();
2543
- if (allItems.length === 0) return null;
2544
- const pageWidth = safeMax(allItems.map((i) => i.x + i.w)) - safeMin(allItems.map((i) => i.x));
2545
- if (pageWidth < 100) return null;
2546
- let bigoLineIdx = -1;
2547
- for (let i = 0; i < yLines.length; i++) {
2548
- if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
2549
- bigoLineIdx = i;
2550
- break;
2236
+ function extractColumnClusters(rows) {
2237
+ const allX = [];
2238
+ for (const row of rows) {
2239
+ for (const item of row.items) allX.push(item.x);
2240
+ }
2241
+ if (allX.length === 0) return [];
2242
+ allX.sort((a, b) => a - b);
2243
+ const clusters = [];
2244
+ let clusterStart = 0;
2245
+ for (let i = 1; i <= allX.length; i++) {
2246
+ if (i === allX.length || allX[i] - allX[i - 1] > COL_CLUSTER_TOL) {
2247
+ const slice = allX.slice(clusterStart, i);
2248
+ const avg = Math.round(slice.reduce((s, v) => s + v, 0) / slice.length);
2249
+ clusters.push({ x: avg, count: slice.length });
2250
+ clusterStart = i;
2551
2251
  }
2552
2252
  }
2553
- const tableYLines = bigoLineIdx >= 0 ? yLines.slice(0, bigoLineIdx) : yLines;
2554
- const CLUSTER_TOL = 22;
2555
- const xClusters = [];
2556
- for (const line of tableYLines) {
2557
- if (isProseSpread(line)) continue;
2558
- for (const item of line) {
2559
- let found = false;
2560
- for (const c of xClusters) {
2561
- if (Math.abs(item.x - c.center) <= CLUSTER_TOL) {
2562
- c.center = Math.round((c.center * c.count + item.x) / (c.count + 1));
2563
- c.minX = Math.min(c.minX, item.x);
2564
- c.count++;
2565
- found = true;
2566
- break;
2567
- }
2253
+ const minCount = Math.max(2, Math.floor(rows.length * MIN_COL_FILL_RATIO));
2254
+ return clusters.filter((c) => c.count >= minCount).sort((a, b) => a.x - b.x);
2255
+ }
2256
+ function findTableRegionsByHeader(allRows, columns, headerItems) {
2257
+ const regions = [];
2258
+ let currentRegion = [];
2259
+ let missStreak = 0;
2260
+ for (const row of allRows) {
2261
+ const matchedCols = countMatchedColumnsRange(row, columns, headerItems);
2262
+ if (matchedCols >= MIN_COLS) {
2263
+ currentRegion.push(row);
2264
+ missStreak = 0;
2265
+ } else if (currentRegion.length > 0 && (row.items.length <= 2 || missStreak === 0)) {
2266
+ currentRegion.push(row);
2267
+ missStreak++;
2268
+ } else {
2269
+ while (currentRegion.length > 0) {
2270
+ const last = currentRegion[currentRegion.length - 1];
2271
+ if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
2272
+ currentRegion.pop();
2568
2273
  }
2569
- if (!found) {
2570
- xClusters.push({ center: item.x, count: 1, minX: item.x });
2274
+ if (currentRegion.length >= MIN_ROWS) {
2275
+ regions.push({ rows: [...currentRegion] });
2571
2276
  }
2277
+ currentRegion = [];
2278
+ missStreak = 0;
2572
2279
  }
2573
2280
  }
2574
- const peaks = xClusters.filter((c) => c.count >= 3).sort((a, b) => a.minX - b.minX);
2575
- if (peaks.length < 3) return null;
2576
- const MERGE_TOL = 40;
2577
- const merged = [peaks[0]];
2578
- for (let i = 1; i < peaks.length; i++) {
2579
- const prev = merged[merged.length - 1];
2580
- if (peaks[i].minX - prev.minX < MERGE_TOL) {
2581
- if (peaks[i].count > prev.count) {
2582
- prev.center = peaks[i].center;
2281
+ while (currentRegion.length > 0) {
2282
+ const last = currentRegion[currentRegion.length - 1];
2283
+ if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
2284
+ currentRegion.pop();
2285
+ }
2286
+ if (currentRegion.length >= MIN_ROWS) {
2287
+ regions.push({ rows: currentRegion });
2288
+ }
2289
+ return regions;
2290
+ }
2291
+ function findTableRegions(allRows, columns) {
2292
+ const regions = [];
2293
+ let currentRegion = [];
2294
+ for (const row of allRows) {
2295
+ const matchedCols = countMatchedColumns(row, columns);
2296
+ if (matchedCols >= MIN_COLS) {
2297
+ currentRegion.push(row);
2298
+ } else if (row.items.length === 1) {
2299
+ if (currentRegion.length > 0) {
2300
+ currentRegion.push(row);
2583
2301
  }
2584
- prev.count += peaks[i].count;
2585
- prev.minX = Math.min(prev.minX, peaks[i].minX);
2586
2302
  } else {
2587
- merged.push({ ...peaks[i] });
2303
+ if (currentRegion.length >= MIN_ROWS) {
2304
+ regions.push({ rows: [...currentRegion] });
2305
+ }
2306
+ currentRegion = [];
2588
2307
  }
2589
2308
  }
2590
- const rawColumns = merged.filter((c) => c.count >= 3).map((c) => c.minX);
2591
- if (rawColumns.length < 3) return null;
2592
- const MIN_DETECT_COL_WIDTH = 30;
2593
- const columns = [rawColumns[0]];
2594
- for (let i = 1; i < rawColumns.length; i++) {
2595
- if (rawColumns[i] - columns[columns.length - 1] < MIN_DETECT_COL_WIDTH) continue;
2596
- columns.push(rawColumns[i]);
2309
+ if (currentRegion.length >= MIN_ROWS) {
2310
+ regions.push({ rows: currentRegion });
2597
2311
  }
2598
- return columns.length >= 3 ? columns : null;
2312
+ return regions;
2599
2313
  }
2600
- function findColumn(x, columns) {
2601
- for (let i = columns.length - 1; i >= 0; i--) {
2602
- if (x >= columns[i] - 10) return i;
2314
+ function countMatchedColumns(row, columns) {
2315
+ const matched = /* @__PURE__ */ new Set();
2316
+ for (const item of row.items) {
2317
+ for (let ci = 0; ci < columns.length; ci++) {
2318
+ if (Math.abs(item.x - columns[ci].x) <= COL_CLUSTER_TOL * 2) {
2319
+ matched.add(ci);
2320
+ break;
2321
+ }
2322
+ }
2603
2323
  }
2604
- return 0;
2324
+ return matched.size;
2605
2325
  }
2606
- function extractWithColumns(yLines, columns) {
2607
- const result = [];
2608
- const colMin = columns[0];
2609
- const colMax = columns[columns.length - 1];
2610
- let bigoIdx = -1;
2611
- for (let i = 0; i < yLines.length; i++) {
2612
- if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
2613
- bigoIdx = i;
2614
- break;
2615
- }
2326
+ function countMatchedColumnsRange(row, columns, headerItems) {
2327
+ const boundaries = [];
2328
+ for (let ci = 0; ci < headerItems.length; ci++) {
2329
+ const left = ci === 0 ? 0 : (headerItems[ci - 1].x + headerItems[ci - 1].w + headerItems[ci].x) / 2;
2330
+ const right = ci === headerItems.length - 1 ? Infinity : (headerItems[ci].x + headerItems[ci].w + headerItems[ci + 1].x) / 2;
2331
+ boundaries.push({ left, right });
2616
2332
  }
2617
- let tableStart = -1;
2618
- for (let i = 0; i < (bigoIdx >= 0 ? bigoIdx : yLines.length); i++) {
2619
- const usedCols = new Set(yLines[i].map((item) => findColumn(item.x, columns)));
2620
- if (usedCols.size >= 3) {
2621
- tableStart = i;
2622
- break;
2333
+ const matched = /* @__PURE__ */ new Set();
2334
+ for (const item of row.items) {
2335
+ for (let ci = 0; ci < boundaries.length; ci++) {
2336
+ if (item.x >= boundaries[ci].left && item.x < boundaries[ci].right) {
2337
+ matched.add(ci);
2338
+ break;
2339
+ }
2623
2340
  }
2624
2341
  }
2625
- const tableEnd = bigoIdx >= 0 ? bigoIdx : yLines.length;
2626
- for (let i = 0; i < (tableStart >= 0 ? tableStart : tableEnd); i++) {
2627
- result.push(mergeLineSimple(yLines[i]));
2342
+ return matched.size;
2343
+ }
2344
+ function assignRowItems(items, columns, numCols) {
2345
+ if (items.length === 0) return [];
2346
+ const sorted = [...items].sort((a, b) => a.x - b.x);
2347
+ const colCenters = columns.map((c) => c.x);
2348
+ const gaps = [];
2349
+ for (let i = 1; i < sorted.length; i++) {
2350
+ gaps.push({ idx: i, size: sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w) });
2628
2351
  }
2629
- if (tableStart >= 0) {
2630
- const tableLines = yLines.slice(tableStart, tableEnd);
2631
- const gridLines = [];
2632
- for (const line of tableLines) {
2633
- const inRange = line.some(
2634
- (item) => item.x >= colMin - 20 && item.x <= colMax + 200
2635
- );
2636
- if (inRange && !isProseSpread(line)) {
2637
- gridLines.push(line);
2638
- } else {
2639
- if (gridLines.length > 0) {
2640
- result.push(buildGridTable(gridLines.splice(0), columns));
2641
- }
2642
- result.push(mergeLineSimple(line));
2643
- }
2352
+ const gapSizes = gaps.map((g) => g.size).sort((a, b) => a - b);
2353
+ const medianGap = gapSizes.length > 0 ? gapSizes[Math.floor(gapSizes.length / 2)] : 0;
2354
+ const gapThreshold = sorted.length <= numCols + 1 ? 12 : Math.max(medianGap * 2.5, 12);
2355
+ const significantGaps = gaps.filter((g) => g.size >= gapThreshold).sort((a, b) => b.size - a.size).slice(0, numCols - 1).sort((a, b) => a.idx - b.idx);
2356
+ const groups = [];
2357
+ let start = 0;
2358
+ for (const gap of significantGaps) {
2359
+ groups.push(sorted.slice(start, gap.idx));
2360
+ start = gap.idx;
2361
+ }
2362
+ groups.push(sorted.slice(start));
2363
+ const result = [];
2364
+ const usedCols = /* @__PURE__ */ new Set();
2365
+ const groupCenters = groups.map((g) => {
2366
+ let minX = Infinity, maxX = -Infinity;
2367
+ for (const i of g) {
2368
+ if (i.x < minX) minX = i.x;
2369
+ const r = i.x + i.w;
2370
+ if (r > maxX) maxX = r;
2644
2371
  }
2645
- if (gridLines.length > 0) {
2646
- result.push(buildGridTable(gridLines, columns));
2372
+ return (minX + maxX) / 2;
2373
+ });
2374
+ const assignments = [];
2375
+ for (let gi = 0; gi < groups.length; gi++) {
2376
+ for (let ci = 0; ci < numCols; ci++) {
2377
+ assignments.push({ gi, ci, dist: Math.abs(groupCenters[gi] - colCenters[ci]) });
2647
2378
  }
2648
2379
  }
2649
- if (bigoIdx >= 0) {
2650
- result.push("");
2651
- for (let i = bigoIdx; i < yLines.length; i++) {
2652
- result.push(mergeLineSimple(yLines[i]));
2380
+ assignments.sort((a, b) => a.dist - b.dist);
2381
+ const assignedGroups = /* @__PURE__ */ new Set();
2382
+ for (const { gi, ci } of assignments) {
2383
+ if (assignedGroups.has(gi) || usedCols.has(ci)) continue;
2384
+ result.push({ col: ci, items: groups[gi] });
2385
+ assignedGroups.add(gi);
2386
+ usedCols.add(ci);
2387
+ }
2388
+ for (let gi = 0; gi < groups.length; gi++) {
2389
+ if (assignedGroups.has(gi)) continue;
2390
+ let bestCol = 0, bestDist = Infinity;
2391
+ for (let ci = 0; ci < numCols; ci++) {
2392
+ const d = Math.abs(groupCenters[gi] - colCenters[ci]);
2393
+ if (d < bestDist) {
2394
+ bestDist = d;
2395
+ bestCol = ci;
2396
+ }
2653
2397
  }
2398
+ result.push({ col: bestCol, items: groups[gi] });
2654
2399
  }
2655
- return result.join("\n");
2400
+ return result;
2656
2401
  }
2657
- function buildGridTable(lines, columns) {
2402
+ function buildClusterTable(rows, columns, pageNum) {
2658
2403
  const numCols = columns.length;
2659
- const yRows = lines.map((items) => {
2660
- const row = Array(numCols).fill("");
2661
- for (const item of items) {
2662
- const col = findColumn(item.x, columns);
2663
- row[col] = row[col] ? row[col] + " " + item.text : item.text;
2664
- }
2665
- return row;
2666
- });
2667
- const dataColStart = Math.max(2, Math.floor(numCols / 2));
2668
- const merged = [];
2669
- for (const row of yRows) {
2670
- if (row.every((c) => c === "")) continue;
2671
- if (merged.length === 0) {
2672
- merged.push([...row]);
2404
+ const numRows = rows.length;
2405
+ if (numRows < MIN_ROWS || numCols < MIN_COLS) return null;
2406
+ const cells = Array.from(
2407
+ { length: numRows },
2408
+ () => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
2409
+ );
2410
+ const usedItems = /* @__PURE__ */ new Set();
2411
+ for (let r = 0; r < numRows; r++) {
2412
+ const row = rows[r];
2413
+ if (row.items.length === 1 && numCols > 1) {
2414
+ cells[r][0] = { text: row.items[0].text, colSpan: numCols, rowSpan: 1 };
2415
+ usedItems.add(row.items[0]);
2673
2416
  continue;
2674
2417
  }
2675
- const prev = merged[merged.length - 1];
2676
- const filledCols = row.map((c, i) => c ? i : -1).filter((i) => i >= 0);
2677
- const filledCount = filledCols.length;
2678
- let isNewRow = false;
2679
- if (row[0] && row[0].length >= 3) {
2680
- isNewRow = true;
2681
- }
2682
- if (!isNewRow && numCols > 1 && row[1]) {
2683
- isNewRow = true;
2684
- }
2685
- if (!isNewRow) {
2686
- const hasData = row.slice(dataColStart).some((c) => c !== "");
2687
- const prevHasData = prev.slice(dataColStart).some((c) => c !== "");
2688
- if (hasData && prevHasData) {
2689
- isNewRow = true;
2690
- }
2691
- }
2692
- if (isNewRow && filledCount === 1 && row[0] && row[0].length <= 2) {
2693
- isNewRow = false;
2694
- }
2695
- if (isNewRow) {
2696
- merged.push([...row]);
2697
- } else {
2698
- for (let c = 0; c < numCols; c++) {
2699
- if (row[c]) {
2700
- prev[c] = prev[c] ? prev[c] + " " + row[c] : row[c];
2701
- }
2702
- }
2418
+ const assignments = assignRowItems(row.items, columns, numCols);
2419
+ for (const { col, items } of assignments) {
2420
+ const text = items.map((i) => i.text).join(" ");
2421
+ const existing = cells[r][col].text;
2422
+ cells[r][col].text = existing ? existing + " " + text : text;
2423
+ for (const item of items) usedItems.add(item);
2703
2424
  }
2704
2425
  }
2705
- if (merged.length < 2) {
2706
- return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
2426
+ let emptyRows = 0;
2427
+ for (const row of cells) {
2428
+ if (row.every((c) => c.text === "")) emptyRows++;
2707
2429
  }
2708
- let headerEnd = 0;
2709
- for (let r = 0; r < merged.length; r++) {
2710
- const hasDataValues = merged[r].slice(dataColStart).some((c) => c && /\d/.test(c));
2711
- if (hasDataValues) break;
2712
- headerEnd = r + 1;
2430
+ if (emptyRows > numRows * 0.5) return null;
2431
+ for (let c = 0; c < numCols; c++) {
2432
+ const hasValue = cells.some((row) => row[c].text !== "");
2433
+ if (!hasValue) return null;
2713
2434
  }
2714
- if (headerEnd > 1) {
2715
- const headerRow = Array(numCols).fill("");
2716
- for (let r = 0; r < headerEnd; r++) {
2717
- for (let c = 0; c < numCols; c++) {
2718
- if (merged[r][c]) {
2719
- headerRow[c] = headerRow[c] ? headerRow[c] + " " + merged[r][c] : merged[r][c];
2435
+ for (let r = numRows - 1; r >= 1; r--) {
2436
+ const nonEmptyCols = cells[r].filter((c) => c.text.trim()).length;
2437
+ if (nonEmptyCols !== 1) continue;
2438
+ if (cells[r][0].text.trim() !== "") continue;
2439
+ const contentText = cells[r].find((c) => c.text.trim())?.text.trim() || "";
2440
+ if (/^[○●▶\-·]/.test(contentText)) continue;
2441
+ for (let pr = r - 1; pr >= 0; pr--) {
2442
+ if (cells[pr].some((c) => c.text.trim())) {
2443
+ for (let c = 0; c < numCols; c++) {
2444
+ const prev = cells[pr][c].text.trim();
2445
+ const curr = cells[r][c].text.trim();
2446
+ if (curr) cells[pr][c].text = prev ? prev + " " + curr : curr;
2720
2447
  }
2448
+ for (let c = 0; c < numCols; c++) cells[r][c].text = "";
2449
+ break;
2721
2450
  }
2722
2451
  }
2723
- merged.splice(0, headerEnd, headerRow);
2724
2452
  }
2725
- for (const row of merged) {
2726
- for (let c = 0; c < row.length; c++) {
2727
- if (row[c]) row[c] = collapseEvenSpacing(row[c]);
2453
+ for (let r = 0; r < cells.length - 1; r++) {
2454
+ const row = cells[r];
2455
+ const hasCol0 = row[0].text.trim() !== "";
2456
+ const hasColLast = numCols > 1 && row[numCols - 1].text.trim() !== "";
2457
+ const midEmpty = row.slice(1, numCols - 1).every((c) => c.text.trim() === "");
2458
+ if (hasCol0 && hasColLast && midEmpty) {
2459
+ const next = cells[r + 1];
2460
+ if (next[0].text.trim() === "" && next.some((c) => c.text.trim())) {
2461
+ for (let c = 1; c < numCols; c++) {
2462
+ const curr = next[c].text.trim();
2463
+ if (curr) row[c].text = row[c].text.trim() ? row[c].text.trim() + " " + curr : curr;
2464
+ }
2465
+ for (let c = 0; c < numCols; c++) next[c].text = "";
2466
+ }
2728
2467
  }
2729
2468
  }
2730
- const totalCells = merged.length * numCols;
2731
- const filledCells = merged.reduce((s, row) => s + row.filter((c) => c).length, 0);
2732
- if (filledCells < totalCells * 0.35 || merged.length < 2 || merged.length <= 3 && numCols >= 7) {
2733
- return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
2734
- }
2735
- const escCell = (c) => c.replace(/\t/g, " ").replace(/\|/g, "\\|");
2736
- const md = [];
2737
- md.push("| " + merged[0].map(escCell).join(" | ") + " |");
2738
- md.push("| " + merged[0].map(() => "---").join(" | ") + " |");
2739
- for (let r = 1; r < merged.length; r++) {
2740
- md.push("| " + merged[r].map(escCell).join(" | ") + " |");
2469
+ const filteredCells = cells.filter((row) => row.some((c) => c.text.trim()));
2470
+ const finalRowCount = filteredCells.length;
2471
+ if (finalRowCount < MIN_ROWS) return null;
2472
+ const irTable = {
2473
+ rows: finalRowCount,
2474
+ cols: numCols,
2475
+ cells: filteredCells,
2476
+ hasHeader: finalRowCount > 1
2477
+ };
2478
+ const allItems = rows.flatMap((r) => r.items);
2479
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
2480
+ for (const i of allItems) {
2481
+ if (i.x < minX) minX = i.x;
2482
+ if (i.y < minY) minY = i.y;
2483
+ if (i.x + i.w > maxX) maxX = i.x + i.w;
2484
+ const h = i.h > 0 ? i.h : i.fontSize;
2485
+ if (i.y + h > maxY) maxY = i.y + h;
2741
2486
  }
2742
- return md.join("\n");
2487
+ return {
2488
+ table: irTable,
2489
+ bbox: { page: pageNum, x: minX, y: minY, width: maxX - minX, height: maxY - minY },
2490
+ usedItems
2491
+ };
2743
2492
  }
2744
2493
 
2745
- // src/pdf/block-detect.ts
2746
- function computeMedianFontSizeFromFreq(freq) {
2747
- if (freq.size === 0) return 0;
2748
- let total = 0;
2749
- for (const count of freq.values()) total += count;
2750
- const sorted = [...freq.entries()].sort((a, b) => a[0] - b[0]);
2751
- const mid = Math.floor(total / 2);
2752
- let cumulative = 0;
2753
- for (const [size, count] of sorted) {
2754
- cumulative += count;
2755
- if (cumulative > mid) return size;
2494
+ // src/pdf/xy-cut.ts
2495
+ var MAX_XYCUT_DEPTH = 50;
2496
+ var XYCUT_MIN_GAP = 5;
2497
+ var CROSS_LAYOUT_BETA = 2;
2498
+ var CROSS_OVERLAP_RATIO = 0.1;
2499
+ var CROSS_MIN_OVERLAPS = 2;
2500
+ var CROSS_MAX_MASK_RATIO = 0.2;
2501
+ var NARROW_ELEMENT_WIDTH_RATIO = 0.1;
2502
+ function xyCutOrder(items, gapThreshold, depth = 0) {
2503
+ if (items.length === 0) return [];
2504
+ if (items.length <= 2 || depth >= MAX_XYCUT_DEPTH) return [items];
2505
+ if (depth === 0 && items.length >= 3) {
2506
+ const cross = identifyCrossLayoutItems(items);
2507
+ if (cross.size > 0 && cross.size <= items.length * CROSS_MAX_MASK_RATIO) {
2508
+ const rest = items.filter((i) => !cross.has(i));
2509
+ if (rest.length > 0) {
2510
+ const groups = xyCutOrder(rest, gapThreshold, 1);
2511
+ return mergeCrossLayoutGroups(groups, [...cross]);
2512
+ }
2513
+ }
2756
2514
  }
2757
- return sorted[sorted.length - 1][0];
2758
- }
2759
- function detectHeadings(blocks, medianFontSize) {
2760
- for (const block of blocks) {
2761
- if (block.type !== "paragraph" || !block.text || !block.style?.fontSize) continue;
2762
- const text = block.text.trim();
2763
- if (text.length === 0 || text.length > 200) continue;
2764
- if (/^\d+$/.test(text)) continue;
2765
- const ratio = block.style.fontSize / medianFontSize;
2766
- let level = 0;
2767
- if (ratio >= HEADING_RATIO_H1) level = 1;
2768
- else if (ratio >= HEADING_RATIO_H2) level = 2;
2769
- else if (ratio >= HEADING_RATIO_H3) level = 3;
2770
- if (level > 0) {
2771
- block.type = "heading";
2772
- block.level = level;
2773
- block.text = collapseEvenSpacing(text);
2515
+ const minGap = Math.max(XYCUT_MIN_GAP, gapThreshold);
2516
+ const hCut = findHorizontalCut(items);
2517
+ const vCut = findVerticalCutWithOutlierFilter(items, minGap);
2518
+ const hValid = hCut.gap >= minGap;
2519
+ const vValid = vCut.gap >= minGap;
2520
+ let useHorizontal;
2521
+ if (hValid && vValid) useHorizontal = vCut.gap <= hCut.gap * 1.5;
2522
+ else if (hValid) useHorizontal = true;
2523
+ else if (vValid) useHorizontal = false;
2524
+ else return [items];
2525
+ if (useHorizontal) {
2526
+ const upper = items.filter((i) => i.y > hCut.position);
2527
+ const lower = items.filter((i) => i.y <= hCut.position);
2528
+ if (upper.length > 0 && lower.length > 0 && upper.length < items.length) {
2529
+ return [...xyCutOrder(upper, gapThreshold, depth + 1), ...xyCutOrder(lower, gapThreshold, depth + 1)];
2530
+ }
2531
+ } else {
2532
+ const left = items.filter((i) => i.x + i.w / 2 < vCut.position);
2533
+ const right = items.filter((i) => i.x + i.w / 2 >= vCut.position);
2534
+ if (left.length > 0 && right.length > 0 && left.length < items.length) {
2535
+ return [...xyCutOrder(left, gapThreshold, depth + 1), ...xyCutOrder(right, gapThreshold, depth + 1)];
2774
2536
  }
2775
2537
  }
2538
+ return [items];
2776
2539
  }
2777
- function shouldDemoteTable(table) {
2778
- const allCells = table.cells.flatMap((row) => row.map((c) => c.text.trim())).filter(Boolean);
2779
- const allText = allCells.join(" ");
2780
- if (table.rows >= 2 && table.cols >= 2 && table.cells[0].every((c) => {
2781
- const t = c.text.trim();
2782
- return t.length > 0 && t.length <= 12 && !/[□■◆○●▶ㅇ<>]/.test(t);
2783
- }) && table.cells.slice(1).some((row) => row.some((c) => c.text.trim() !== ""))) return false;
2784
- if (table.rows <= 3 && table.cols <= 3 && allText.length <= 200) {
2785
- const totalCells2 = table.rows * table.cols;
2786
- const emptyCells2 = totalCells2 - allCells.length;
2787
- if (emptyCells2 >= totalCells2 * 0.3) return true;
2788
- if (/[□■◆○●▶ㅇ]/.test(allText)) return true;
2789
- if (/<[^>]+>/.test(allText)) return true;
2540
+ function identifyCrossLayoutItems(items) {
2541
+ const cross = /* @__PURE__ */ new Set();
2542
+ if (items.length < 3) return cross;
2543
+ let maxWidth = 0;
2544
+ for (const i of items) {
2545
+ if (i.w > maxWidth) maxWidth = i.w;
2790
2546
  }
2791
- if (allText.length > 200) return false;
2792
- if (/[□■◆○●▶]/.test(allText) && table.rows <= 3) return true;
2793
- const totalCells = table.rows * table.cols;
2794
- const emptyCells = totalCells - allCells.length;
2795
- if (table.rows <= 2 && emptyCells > totalCells * 0.5) return true;
2796
- if (table.rows === 1 && !/\d{2,}/.test(allText)) return true;
2797
- return false;
2798
- }
2799
- function demoteTableToText(table) {
2800
- const lines = [];
2801
- for (let r = 0; r < table.rows; r++) {
2802
- const cells = table.cells[r].map((c) => c.text.trim()).filter(Boolean);
2803
- if (cells.length === 0) continue;
2804
- if (table.cols === 2 && cells.length === 2) {
2805
- lines.push(`${cells[0]} : ${cells[1]}`);
2806
- } else {
2807
- lines.push(cells.join(" "));
2547
+ const threshold = CROSS_LAYOUT_BETA * maxWidth;
2548
+ for (const item of items) {
2549
+ if (item.w < threshold) continue;
2550
+ let overlaps = 0;
2551
+ for (const other of items) {
2552
+ if (other === item) continue;
2553
+ const left = Math.max(item.x, other.x);
2554
+ const right = Math.min(item.x + item.w, other.x + other.w);
2555
+ const overlapW = right - left;
2556
+ if (overlapW <= 0) continue;
2557
+ const smaller = Math.min(item.w, other.w);
2558
+ if (smaller > 0 && overlapW / smaller >= CROSS_OVERLAP_RATIO) {
2559
+ overlaps++;
2560
+ if (overlaps >= CROSS_MIN_OVERLAPS) break;
2561
+ }
2808
2562
  }
2563
+ if (overlaps >= CROSS_MIN_OVERLAPS) cross.add(item);
2809
2564
  }
2810
- return lines.join("\n");
2565
+ return cross;
2811
2566
  }
2812
- function detectMarkerHeadings(blocks) {
2813
- for (let i = 0; i < blocks.length; i++) {
2814
- const block = blocks[i];
2815
- if (block.type !== "paragraph" || !block.text) continue;
2816
- const text = block.text.trim();
2817
- if (text.length < 50 && /^[□■◆◇▶]\s*[가-힣]/.test(text)) {
2818
- block.type = "heading";
2819
- block.level = 4;
2567
+ function mergeCrossLayoutGroups(groups, cross) {
2568
+ if (cross.length === 0) return groups;
2569
+ const sortedCross = [...cross].sort((a, b) => b.y + b.h - (a.y + a.h) || a.x - b.x);
2570
+ const groupTop = (g) => {
2571
+ let top = -Infinity;
2572
+ for (const i of g) {
2573
+ const t = i.y + i.h;
2574
+ if (t > top) top = t;
2575
+ }
2576
+ return top;
2577
+ };
2578
+ const result = [];
2579
+ let gi = 0, ci = 0;
2580
+ while (gi < groups.length || ci < sortedCross.length) {
2581
+ if (ci >= sortedCross.length) {
2582
+ result.push(groups[gi++]);
2820
2583
  continue;
2821
2584
  }
2822
- if (/^[가-힣]{2,6}$/.test(text) && block.style?.fontSize) {
2823
- const prev = blocks[i - 1];
2824
- const next = blocks[i + 1];
2825
- const prevIsStructural = !prev || prev.type === "table" || prev.type === "heading" || prev.type === "separator";
2826
- const nextIsStructural = !next || next.type === "table" || next.type === "heading" || next.type === "paragraph" && next.text && /^[□■◆○●]/.test(next.text.trim());
2827
- if (prevIsStructural || nextIsStructural) {
2828
- block.type = "heading";
2829
- block.level = 3;
2830
- }
2585
+ if (gi >= groups.length) {
2586
+ result.push([sortedCross[ci++]]);
2587
+ continue;
2831
2588
  }
2589
+ const crossTop = sortedCross[ci].y + sortedCross[ci].h;
2590
+ if (crossTop >= groupTop(groups[gi])) result.push([sortedCross[ci++]]);
2591
+ else result.push(groups[gi++]);
2832
2592
  }
2593
+ return result;
2833
2594
  }
2834
- var TABLE_CAPTION_RE = /^[<\[(【〈]?\s*(표|그림|도표|Table|Figure|Fig\.?)\s*[\d①-⑮][\d.\-]*\s*[\])】〉>]?[.:]?\s*/i;
2835
- var CAPTION_MAX_LENGTH = 100;
2836
- var CAPTION_MAX_GAP = 30;
2837
- function detectTableCaptions(blocks) {
2838
- const isCaptionCandidate = (b, table) => {
2839
- if (!b || b.type !== "paragraph" || !b.text) return false;
2840
- if (b.pageNumber !== table.pageNumber) return false;
2841
- const text = b.text.trim();
2842
- if (!text || text.length > CAPTION_MAX_LENGTH || text.includes("\n")) return false;
2843
- if (!TABLE_CAPTION_RE.test(text)) return false;
2844
- if (b.bbox && table.bbox) {
2845
- const capTop = b.bbox.y + b.bbox.height;
2846
- const capBottom = b.bbox.y;
2847
- const tblTop = table.bbox.y + table.bbox.height;
2848
- const tblBottom = table.bbox.y;
2849
- const gap = capBottom >= tblTop ? capBottom - tblTop : tblBottom - capTop;
2850
- if (gap > CAPTION_MAX_GAP) return false;
2851
- const overlap = Math.min(b.bbox.x + b.bbox.width, table.bbox.x + table.bbox.width) - Math.max(b.bbox.x, table.bbox.x);
2852
- if (overlap < Math.min(b.bbox.width, table.bbox.width) * 0.3) return false;
2595
+ function findHorizontalCut(items) {
2596
+ if (items.length < 2) return { position: 0, gap: 0 };
2597
+ const sorted = [...items].sort((a, b) => b.y - a.y);
2598
+ let largestGap = 0;
2599
+ let position = 0;
2600
+ for (let i = 1; i < sorted.length; i++) {
2601
+ const prevBottom = sorted[i - 1].y - sorted[i - 1].h;
2602
+ const currTop = sorted[i].y;
2603
+ const gap = prevBottom - currTop;
2604
+ if (gap > largestGap) {
2605
+ largestGap = gap;
2606
+ position = (prevBottom + currTop) / 2;
2853
2607
  }
2854
- return true;
2855
- };
2856
- for (let i = 0; i < blocks.length; i++) {
2857
- const block = blocks[i];
2858
- if (block.type !== "table" || !block.table || block.table.caption) continue;
2859
- if (isCaptionCandidate(blocks[i - 1], block)) {
2860
- block.table.caption = blocks[i - 1].text.trim();
2861
- blocks.splice(i - 1, 1);
2862
- i--;
2863
- } else if (isCaptionCandidate(blocks[i + 1], block)) {
2864
- block.table.caption = blocks[i + 1].text.trim();
2865
- blocks.splice(i + 1, 1);
2608
+ }
2609
+ return { position, gap: largestGap };
2610
+ }
2611
+ function findVerticalCutWithOutlierFilter(items, minGap) {
2612
+ const edgeCut = findVerticalCut(items);
2613
+ if (edgeCut.gap >= minGap) return edgeCut;
2614
+ if (items.length >= 3) {
2615
+ let minX = Infinity, maxX = -Infinity;
2616
+ for (const i of items) {
2617
+ if (i.x < minX) minX = i.x;
2618
+ const r = i.x + i.w;
2619
+ if (r > maxX) maxX = r;
2620
+ }
2621
+ const narrowThreshold = (maxX - minX) * NARROW_ELEMENT_WIDTH_RATIO;
2622
+ const filtered = items.filter((i) => i.w >= narrowThreshold);
2623
+ if (filtered.length >= 2 && filtered.length < items.length && filtered.length >= items.length * 0.7) {
2624
+ const filteredCut = findVerticalCut(filtered);
2625
+ if (filteredCut.gap > edgeCut.gap && filteredCut.gap >= minGap) {
2626
+ return filteredCut;
2627
+ }
2866
2628
  }
2867
2629
  }
2630
+ return edgeCut;
2868
2631
  }
2869
- var KOREAN_LIST_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
2870
- function parseListLabel(text) {
2871
- let m = text.match(/^(\d{1,2})\.(?!\d)\s+/);
2872
- if (m) return { family: "arabicDot", ord: parseInt(m[1], 10) };
2873
- m = text.match(/^([가-하])\.\s+/);
2874
- if (m) {
2875
- const idx = KOREAN_LIST_SEQ.indexOf(m[1]);
2876
- if (idx >= 0) return { family: "korDot", ord: idx + 1 };
2877
- }
2878
- m = text.match(/^(\d{1,2})\)\s*/);
2879
- if (m) return { family: "arabicParen", ord: parseInt(m[1], 10) };
2880
- m = text.match(/^([가-하])\)\s*/);
2881
- if (m) {
2882
- const idx = KOREAN_LIST_SEQ.indexOf(m[1]);
2883
- if (idx >= 0) return { family: "korParen", ord: idx + 1 };
2632
+ function findVerticalCut(items) {
2633
+ if (items.length < 2) return { position: 0, gap: 0 };
2634
+ const sorted = [...items].sort((a, b) => a.x - b.x || a.x + a.w - (b.x + b.w));
2635
+ let largestGap = 0;
2636
+ let position = 0;
2637
+ let prevRight = null;
2638
+ for (const it of sorted) {
2639
+ const left = it.x;
2640
+ const right = it.x + it.w;
2641
+ if (prevRight !== null && left > prevRight) {
2642
+ const gap = left - prevRight;
2643
+ if (gap > largestGap) {
2644
+ largestGap = gap;
2645
+ position = (prevRight + left) / 2;
2646
+ }
2647
+ }
2648
+ prevRight = prevRight === null ? right : Math.max(prevRight, right);
2884
2649
  }
2885
- m = text.match(/^([①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮])\s*/);
2886
- if (m) return { family: "circled", ord: m[1].charCodeAt(0) - 9312 + 1 };
2887
- return null;
2650
+ return { position, gap: largestGap };
2888
2651
  }
2889
- var ATTACHMENT_RE = /^붙\s*임\s*(\d+[.:]?)?\s/;
2890
- function detectKoreanListBlocks(blocks) {
2891
- const labeled = [];
2892
- for (let i = 0; i < blocks.length; i++) {
2893
- const b = blocks[i];
2894
- if (b.type !== "paragraph" && b.type !== "list" || !b.text) continue;
2895
- const label = parseListLabel(b.text.trim());
2896
- if (label) labeled.push({ idx: i, label });
2652
+
2653
+ // src/pdf/columns.ts
2654
+ function isProseSpread(items) {
2655
+ if (items.length < 4) return false;
2656
+ const sorted = [...items].sort((a, b) => a.x - b.x);
2657
+ const gaps = [];
2658
+ for (let i = 1; i < sorted.length; i++) {
2659
+ gaps.push(sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w));
2897
2660
  }
2898
- const validated = /* @__PURE__ */ new Set();
2899
- const byFamily = /* @__PURE__ */ new Map();
2900
- for (const l of labeled) {
2901
- const arr = byFamily.get(l.label.family) || [];
2902
- arr.push(l);
2903
- byFamily.set(l.label.family, arr);
2661
+ const maxGap = safeMax(gaps);
2662
+ const avgLen = items.reduce((s, i) => s + i.text.length, 0) / items.length;
2663
+ return maxGap < 40 && avgLen < 5;
2664
+ }
2665
+ function detectColumns(yLines) {
2666
+ const allItems = yLines.flat();
2667
+ if (allItems.length === 0) return null;
2668
+ const pageWidth = safeMax(allItems.map((i) => i.x + i.w)) - safeMin(allItems.map((i) => i.x));
2669
+ if (pageWidth < 100) return null;
2670
+ let bigoLineIdx = -1;
2671
+ for (let i = 0; i < yLines.length; i++) {
2672
+ if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
2673
+ bigoLineIdx = i;
2674
+ break;
2675
+ }
2904
2676
  }
2905
- for (const arr of byFamily.values()) {
2906
- let chain = [];
2907
- for (const item of arr) {
2908
- const prev = chain[chain.length - 1];
2909
- if (prev && item.label.ord === prev.label.ord + 1 && item.idx - prev.idx <= 20) {
2910
- chain.push(item);
2911
- } else {
2912
- if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
2913
- chain = [item];
2677
+ const tableYLines = bigoLineIdx >= 0 ? yLines.slice(0, bigoLineIdx) : yLines;
2678
+ const CLUSTER_TOL = 22;
2679
+ const xClusters = [];
2680
+ for (const line of tableYLines) {
2681
+ if (isProseSpread(line)) continue;
2682
+ for (const item of line) {
2683
+ let found = false;
2684
+ for (const c of xClusters) {
2685
+ if (Math.abs(item.x - c.center) <= CLUSTER_TOL) {
2686
+ c.center = Math.round((c.center * c.count + item.x) / (c.count + 1));
2687
+ c.minX = Math.min(c.minX, item.x);
2688
+ c.count++;
2689
+ found = true;
2690
+ break;
2691
+ }
2692
+ }
2693
+ if (!found) {
2694
+ xClusters.push({ center: item.x, count: 1, minX: item.x });
2914
2695
  }
2915
2696
  }
2916
- if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
2917
2697
  }
2918
- let familyStack = [];
2919
- let lastTopLevelList = null;
2920
- let prevListIdx = -2;
2921
- const toRemove = /* @__PURE__ */ new Set();
2922
- for (let i = 0; i < blocks.length; i++) {
2923
- const b = blocks[i];
2924
- if (b.type === "table" || b.type === "heading" || b.type === "separator") {
2925
- familyStack = [];
2926
- lastTopLevelList = null;
2927
- continue;
2928
- }
2929
- if (b.type !== "paragraph" && b.type !== "list" || !b.text) continue;
2930
- const text = b.text.trim();
2931
- if (b.type === "paragraph" && ATTACHMENT_RE.test(text)) {
2932
- blocks[i] = { ...b, type: "list", listType: "unordered" };
2933
- continue;
2934
- }
2935
- if (!validated.has(i)) continue;
2936
- const label = parseListLabel(text);
2937
- let depth = familyStack.indexOf(label.family);
2938
- if (depth < 0) {
2939
- familyStack.push(label.family);
2940
- depth = familyStack.length - 1;
2941
- } else {
2942
- familyStack = familyStack.slice(0, depth + 1);
2943
- }
2944
- const listType = label.family === "arabicDot" ? "ordered" : "unordered";
2945
- const listBlock = { ...b, type: "list", listType };
2946
- if (depth === 0) {
2947
- blocks[i] = listBlock;
2948
- lastTopLevelList = listBlock;
2949
- } else if (lastTopLevelList && i === prevListIdx + 1) {
2950
- if (!lastTopLevelList.children) lastTopLevelList.children = [];
2951
- lastTopLevelList.children.push(listBlock);
2952
- toRemove.add(i);
2698
+ const peaks = xClusters.filter((c) => c.count >= 3).sort((a, b) => a.minX - b.minX);
2699
+ if (peaks.length < 3) return null;
2700
+ const MERGE_TOL = 40;
2701
+ const merged = [peaks[0]];
2702
+ for (let i = 1; i < peaks.length; i++) {
2703
+ const prev = merged[merged.length - 1];
2704
+ if (peaks[i].minX - prev.minX < MERGE_TOL) {
2705
+ if (peaks[i].count > prev.count) {
2706
+ prev.center = peaks[i].center;
2707
+ }
2708
+ prev.count += peaks[i].count;
2709
+ prev.minX = Math.min(prev.minX, peaks[i].minX);
2953
2710
  } else {
2954
- blocks[i] = listBlock;
2955
- lastTopLevelList = listBlock;
2711
+ merged.push({ ...peaks[i] });
2956
2712
  }
2957
- prevListIdx = i;
2958
2713
  }
2959
- if (toRemove.size > 0) {
2960
- const sorted = [...toRemove].sort((a, b) => b - a);
2961
- for (const idx of sorted) blocks.splice(idx, 1);
2714
+ const rawColumns = merged.filter((c) => c.count >= 3).map((c) => c.minX);
2715
+ if (rawColumns.length < 3) return null;
2716
+ const MIN_DETECT_COL_WIDTH = 30;
2717
+ const columns = [rawColumns[0]];
2718
+ for (let i = 1; i < rawColumns.length; i++) {
2719
+ if (rawColumns[i] - columns[columns.length - 1] < MIN_DETECT_COL_WIDTH) continue;
2720
+ columns.push(rawColumns[i]);
2962
2721
  }
2722
+ return columns.length >= 3 ? columns : null;
2963
2723
  }
2964
- function detectListBlocks(blocks) {
2965
- const result = [];
2966
- for (let i = 0; i < blocks.length; i++) {
2967
- const block = blocks[i];
2968
- if (block.type === "paragraph" && block.text) {
2969
- const text = block.text.trim();
2970
- if (/^\d+\.\s/.test(text)) {
2971
- result.push({ ...block, type: "list", listType: "ordered", text: block.text });
2972
- continue;
2973
- }
2974
- if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
2975
- result.push({ ...block, type: "list", listType: "unordered", text: block.text });
2976
- continue;
2977
- }
2978
- }
2979
- result.push(block);
2724
+ function findColumn(x, columns) {
2725
+ for (let i = columns.length - 1; i >= 0; i--) {
2726
+ if (x >= columns[i] - 10) return i;
2980
2727
  }
2981
- return result;
2728
+ return 0;
2982
2729
  }
2983
- var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
2984
- var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
2985
- function detectSpecialKoreanTables(blocks) {
2730
+ function extractWithColumns(yLines, columns) {
2986
2731
  const result = [];
2987
- let kvLines = [];
2988
- const flushKvTable = () => {
2989
- if (kvLines.length < 2) {
2990
- for (const kv of kvLines) result.push(kv.block);
2991
- kvLines = [];
2992
- return;
2993
- }
2994
- const cells = kvLines.map((kv) => {
2995
- if (kv.value) {
2996
- return [
2997
- { text: kv.key, colSpan: 1, rowSpan: 1 },
2998
- { text: kv.value, colSpan: 1, rowSpan: 1 }
2999
- ];
3000
- }
3001
- return [
3002
- { text: kv.key, colSpan: 2, rowSpan: 1 },
3003
- { text: "", colSpan: 1, rowSpan: 1 }
3004
- ];
3005
- });
3006
- const irTable = {
3007
- rows: cells.length,
3008
- cols: 2,
3009
- cells,
3010
- hasHeader: true
3011
- };
3012
- const firstBlock = kvLines[0].block;
3013
- result.push({
3014
- type: "table",
3015
- table: irTable,
3016
- pageNumber: firstBlock.pageNumber,
3017
- bbox: firstBlock.bbox
3018
- });
3019
- kvLines = [];
3020
- };
3021
- for (const block of blocks) {
3022
- if (block.type !== "paragraph" || !block.text) {
3023
- flushKvTable();
3024
- result.push(block);
3025
- continue;
2732
+ const colMin = columns[0];
2733
+ const colMax = columns[columns.length - 1];
2734
+ let bigoIdx = -1;
2735
+ for (let i = 0; i < yLines.length; i++) {
2736
+ if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
2737
+ bigoIdx = i;
2738
+ break;
3026
2739
  }
3027
- const text = block.text.trim();
3028
- if (KOREAN_TABLE_HEADER_RE.test(text)) {
3029
- const colonIdx = text.indexOf(":");
3030
- if (colonIdx >= 0) {
3031
- kvLines.push({
3032
- key: text.slice(0, colonIdx).trim(),
3033
- value: text.slice(colonIdx + 1).trim(),
3034
- block
3035
- });
2740
+ }
2741
+ let tableStart = -1;
2742
+ for (let i = 0; i < (bigoIdx >= 0 ? bigoIdx : yLines.length); i++) {
2743
+ const usedCols = new Set(yLines[i].map((item) => findColumn(item.x, columns)));
2744
+ if (usedCols.size >= 3) {
2745
+ tableStart = i;
2746
+ break;
2747
+ }
2748
+ }
2749
+ const tableEnd = bigoIdx >= 0 ? bigoIdx : yLines.length;
2750
+ for (let i = 0; i < (tableStart >= 0 ? tableStart : tableEnd); i++) {
2751
+ result.push(mergeLineSimple(yLines[i]));
2752
+ }
2753
+ if (tableStart >= 0) {
2754
+ const tableLines = yLines.slice(tableStart, tableEnd);
2755
+ const gridLines = [];
2756
+ for (const line of tableLines) {
2757
+ const inRange = line.some(
2758
+ (item) => item.x >= colMin - 20 && item.x <= colMax + 200
2759
+ );
2760
+ if (inRange && !isProseSpread(line)) {
2761
+ gridLines.push(line);
3036
2762
  } else {
3037
- const spaceIdx = text.search(/\s/);
3038
- if (spaceIdx > 0) {
3039
- kvLines.push({
3040
- key: text.slice(0, spaceIdx).trim(),
3041
- value: text.slice(spaceIdx + 1).trim(),
3042
- block
3043
- });
3044
- } else {
3045
- kvLines.push({ key: text, value: "", block });
2763
+ if (gridLines.length > 0) {
2764
+ result.push(buildGridTable(gridLines.splice(0), columns));
3046
2765
  }
2766
+ result.push(mergeLineSimple(line));
3047
2767
  }
2768
+ }
2769
+ if (gridLines.length > 0) {
2770
+ result.push(buildGridTable(gridLines, columns));
2771
+ }
2772
+ }
2773
+ if (bigoIdx >= 0) {
2774
+ result.push("");
2775
+ for (let i = bigoIdx; i < yLines.length; i++) {
2776
+ result.push(mergeLineSimple(yLines[i]));
2777
+ }
2778
+ }
2779
+ return result.join("\n");
2780
+ }
2781
+ function buildGridTable(lines, columns) {
2782
+ const numCols = columns.length;
2783
+ const yRows = lines.map((items) => {
2784
+ const row = Array(numCols).fill("");
2785
+ for (const item of items) {
2786
+ const col = findColumn(item.x, columns);
2787
+ row[col] = row[col] ? row[col] + " " + item.text : item.text;
2788
+ }
2789
+ return row;
2790
+ });
2791
+ const dataColStart = Math.max(2, Math.floor(numCols / 2));
2792
+ const merged = [];
2793
+ for (const row of yRows) {
2794
+ if (row.every((c) => c === "")) continue;
2795
+ if (merged.length === 0) {
2796
+ merged.push([...row]);
3048
2797
  continue;
3049
2798
  }
3050
- if (kvLines.length > 0 && text.includes(":")) {
3051
- if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
3052
- const colonIdx = text.indexOf(":");
3053
- const key = text.slice(0, colonIdx).trim();
3054
- if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
3055
- kvLines.push({
3056
- key,
3057
- value: text.slice(colonIdx + 1).trim(),
3058
- block
3059
- });
3060
- continue;
2799
+ const prev = merged[merged.length - 1];
2800
+ const filledCols = row.map((c, i) => c ? i : -1).filter((i) => i >= 0);
2801
+ const filledCount = filledCols.length;
2802
+ let isNewRow = false;
2803
+ if (row[0] && row[0].length >= 3) {
2804
+ isNewRow = true;
2805
+ }
2806
+ if (!isNewRow && numCols > 1 && row[1]) {
2807
+ isNewRow = true;
2808
+ }
2809
+ if (!isNewRow) {
2810
+ const hasData = row.slice(dataColStart).some((c) => c !== "");
2811
+ const prevHasData = prev.slice(dataColStart).some((c) => c !== "");
2812
+ if (hasData && prevHasData) {
2813
+ isNewRow = true;
2814
+ }
2815
+ }
2816
+ if (isNewRow && filledCount === 1 && row[0] && row[0].length <= 2) {
2817
+ isNewRow = false;
2818
+ }
2819
+ if (isNewRow) {
2820
+ merged.push([...row]);
2821
+ } else {
2822
+ for (let c = 0; c < numCols; c++) {
2823
+ if (row[c]) {
2824
+ prev[c] = prev[c] ? prev[c] + " " + row[c] : row[c];
3061
2825
  }
3062
2826
  }
3063
2827
  }
3064
- flushKvTable();
3065
- result.push(block);
3066
2828
  }
3067
- flushKvTable();
3068
- return result;
3069
- }
3070
- function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
3071
- const ZONE_RATIO = 0.12;
3072
- const MIN_REPEAT = 3;
3073
- const topEntries = [];
3074
- const bottomEntries = [];
3075
- for (let bi = 0; bi < blocks.length; bi++) {
3076
- const b = blocks[bi];
3077
- if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
3078
- const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
3079
- if (!ph) continue;
3080
- const blockTop = ph - (b.bbox.y + b.bbox.height);
3081
- const blockBottom = ph - b.bbox.y;
3082
- const entry = { blockIdx: bi, page: b.pageNumber, text: b.text.trim() };
3083
- if (blockBottom <= ph * ZONE_RATIO) topEntries.push(entry);
3084
- else if (blockTop >= ph * (1 - ZONE_RATIO)) bottomEntries.push(entry);
2829
+ if (merged.length < 2) {
2830
+ return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
3085
2831
  }
3086
- const removeSet = /* @__PURE__ */ new Set();
3087
- for (const entries of [topEntries, bottomEntries]) {
3088
- if (entries.length === 0) continue;
3089
- const patternCount = /* @__PURE__ */ new Map();
3090
- const patternPages = /* @__PURE__ */ new Map();
3091
- for (const e of entries) {
3092
- const norm = e.text.replace(/\d+/g, "#");
3093
- patternCount.set(norm, (patternCount.get(norm) || 0) + 1);
3094
- const pages = patternPages.get(norm) || /* @__PURE__ */ new Set();
3095
- pages.add(e.page);
3096
- patternPages.set(norm, pages);
3097
- }
3098
- const repeatedPatterns = /* @__PURE__ */ new Set();
3099
- for (const [p, count] of patternCount) {
3100
- if (count >= MIN_REPEAT && (patternPages.get(p)?.size ?? 0) >= MIN_REPEAT) {
3101
- repeatedPatterns.add(p);
2832
+ let headerEnd = 0;
2833
+ for (let r = 0; r < merged.length; r++) {
2834
+ const hasDataValues = merged[r].slice(dataColStart).some((c) => c && /\d/.test(c));
2835
+ if (hasDataValues) break;
2836
+ headerEnd = r + 1;
2837
+ }
2838
+ if (headerEnd > 1) {
2839
+ const headerRow = Array(numCols).fill("");
2840
+ for (let r = 0; r < headerEnd; r++) {
2841
+ for (let c = 0; c < numCols; c++) {
2842
+ if (merged[r][c]) {
2843
+ headerRow[c] = headerRow[c] ? headerRow[c] + " " + merged[r][c] : merged[r][c];
2844
+ }
3102
2845
  }
3103
2846
  }
3104
- for (const e of entries) {
3105
- const norm = e.text.replace(/\d+/g, "#");
3106
- if (repeatedPatterns.has(norm)) {
3107
- removeSet.add(e.blockIdx);
3108
- }
2847
+ merged.splice(0, headerEnd, headerRow);
2848
+ }
2849
+ for (const row of merged) {
2850
+ for (let c = 0; c < row.length; c++) {
2851
+ if (row[c]) row[c] = collapseEvenSpacing(row[c]);
3109
2852
  }
3110
2853
  }
3111
- if (removeSet.size > 0) {
3112
- warnings.push({ message: `${removeSet.size}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
2854
+ const totalCells = merged.length * numCols;
2855
+ const filledCells = merged.reduce((s, row) => s + row.filter((c) => c).length, 0);
2856
+ if (filledCells < totalCells * 0.35 || merged.length < 2 || merged.length <= 3 && numCols >= 7) {
2857
+ return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
3113
2858
  }
3114
- return [...removeSet].sort((a, b) => a - b);
2859
+ const escCell = (c) => c.replace(/\t/g, " ").replace(/\|/g, "\\|");
2860
+ const md = [];
2861
+ md.push("| " + merged[0].map(escCell).join(" | ") + " |");
2862
+ md.push("| " + merged[0].map(() => "---").join(" | ") + " |");
2863
+ for (let r = 1; r < merged.length; r++) {
2864
+ md.push("| " + merged[r].map(escCell).join(" | ") + " |");
2865
+ }
2866
+ return md.join("\n");
3115
2867
  }
3116
2868
 
3117
2869
  // src/pdf/page-blocks.ts
3118
- function extractPageBlocksWithLines(items, pageNum, opList, pageWidth, pageHeight) {
2870
+ function extractPageBlocksWithLines(items, pageNum, opList, pageWidth, pageHeight, extraLines) {
3119
2871
  if (items.length === 0) return [];
3120
2872
  let { horizontals, verticals } = extractLines(opList.fnArray, opList.argsArray);
2873
+ if (extraLines) {
2874
+ horizontals = horizontals.concat(extraLines.horizontals);
2875
+ verticals = verticals.concat(extraLines.verticals);
2876
+ }
2877
+ ;
3121
2878
  ({ horizontals, verticals } = filterPageBorderLines(horizontals, verticals, pageWidth, pageHeight));
3122
2879
  ({ horizontals, verticals } = preprocessLines(horizontals, verticals));
3123
2880
  verticals = closeOpenTableEdges(horizontals, verticals);
@@ -3458,7 +3215,7 @@ function splitTwoColumnProse(items, cutX) {
3458
3215
  else cross.push(i);
3459
3216
  }
3460
3217
  if (cross.length === 0) {
3461
- return [left, right].filter((g2) => g2.length > 0);
3218
+ return [left, right].filter((g) => g.length > 0);
3462
3219
  }
3463
3220
  cross.sort((a, b) => b.y - a.y);
3464
3221
  const crossLines = [];
@@ -3569,452 +3326,18 @@ function extractPageBlocksFallback(items, pageNum, fullPage = false) {
3569
3326
  return detectSpecialKoreanTables(blocks);
3570
3327
  }
3571
3328
 
3572
- // src/pdf/text-clean.ts
3573
- function sanitizeBlockControlChars(blocks) {
3574
- for (const b of blocks) {
3575
- if (b.text) b.text = stripControlChars(b.text);
3576
- if (b.table) {
3577
- for (const row of b.table.cells) {
3578
- for (const cell of row) {
3579
- if (cell.text) cell.text = stripControlChars(cell.text);
3580
- }
3581
- }
3582
- }
3583
- if (b.children) sanitizeBlockControlChars(b.children);
3584
- }
3585
- }
3586
- function cleanPdfText(text) {
3587
- return mergeKoreanLines(
3588
- stripControlChars(text).replace(/^\d{1,4}\n/, "").replace(/^[\s]*[-–—]\s*[-–—]?\d+[-–—]?[\s]*[-–—]?[\s]*$/gm, "").replace(/^\s*\d+\s*\/\s*\d+\s*$/gm, "").replace(/\n\d{1,4}\n/g, "\n").replace(/\n\d{1,4}$/, "").replace(/^#{1,6}\s*\d{1,4}\s*$/gm, "")
3589
- ).replace(/^(?!\| ---).*$/gm, (line) => {
3590
- if (/^\s*\${1,2}.+\${1,2}\s*$/.test(line)) return line;
3591
- return collapseEvenSpacing(line);
3592
- }).replace(/([□■◆○●▶ㅇ])\s+([가-힣])\s+([가-힣])/g, "$1 $2$3").replace(/\\~\\~/g, "~~").replace(/~~~~/g, "").replace(/\n{3,}/g, "\n\n").trim();
3593
- }
3594
- function startsWithMarker(line) {
3595
- const t = line.trimStart();
3596
- return /^[가-힣ㄱ-ㅎ][.)]/.test(t) || /^\d+[.)]/.test(t) || /^\([가-힣ㄱ-ㅎ\d]+\)/.test(t) || /^[○●※▶▷◆◇■□★☆\-·]\s/.test(t) || /^제\d+[조항호장절]/.test(t);
3597
- }
3598
- function isStandaloneHeader(line) {
3599
- return /^제\d+[조항호장절](\([^)]*\))?(\s+\S+){0,7}$/.test(line.trim());
3600
- }
3601
- function mergeKoreanLines(text) {
3602
- if (!text) return "";
3603
- const lines = text.split("\n");
3604
- if (lines.length <= 1) return text;
3605
- const result = [lines[0]];
3606
- for (let i = 1; i < lines.length; i++) {
3607
- const prev = result[result.length - 1];
3608
- const curr = lines[i];
3609
- const currTrimmed = curr.trim();
3610
- if (/^#{1,6}\s/.test(prev) || /^#{1,6}\s/.test(curr) || /^\|/.test(currTrimmed) || /^---/.test(currTrimmed)) {
3611
- result.push(curr);
3612
- continue;
3613
- }
3614
- if (/,$/.test(prev.trim()) && currTrimmed.length > 0) {
3615
- result[result.length - 1] = prev + "\n" + curr;
3616
- continue;
3617
- }
3618
- if (/^\(※/.test(currTrimmed)) {
3619
- result[result.length - 1] = prev + " " + currTrimmed;
3620
- continue;
3621
- }
3622
- if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev) && !startsWithMarker(prev)) {
3623
- result[result.length - 1] = prev + " " + curr;
3624
- } else {
3625
- result.push(curr);
3626
- }
3627
- }
3628
- return result.join("\n");
3629
- }
3630
-
3631
- // src/pdf/formula-ocr.ts
3632
- async function applyFormulaOcr(buffer, blocks, pageFilter, effectivePageCount, warnings, _onProgress) {
3633
- const formulaMod = await import("./formula-RXVSQPXI.js");
3634
- const { FormulaPipeline, ensureFormulaModels } = formulaMod;
3635
- await ensureFormulaModels((p) => {
3636
- if (p.phase === "download" && p.total) {
3637
- const pct = Math.floor(p.downloaded / p.total * 100);
3638
- process.stderr.write(`\r[kordoc-formula] ${p.spec.name} ${pct}% (${formatMb(p.downloaded)}/${formatMb(p.total)})`);
3639
- if (p.downloaded >= p.total) process.stderr.write("\n");
3640
- } else if (p.phase === "verify") {
3641
- process.stderr.write(`[kordoc-formula] ${p.spec.name} SHA-256 \uAC80\uC99D \uC911...
3642
- `);
3643
- } else if (p.phase === "done") {
3644
- process.stderr.write(`[kordoc-formula] ${p.spec.name} \uC900\uBE44 \uC644\uB8CC
3645
- `);
3646
- } else if (p.phase === "skip") {
3647
- }
3648
- });
3649
- const pipeline = await FormulaPipeline.create();
3650
- try {
3651
- const pagesResult = await pipeline.runOnBuffer(buffer, pageFilter);
3652
- if (pagesResult.length === 0) return;
3653
- let insertedCount = 0;
3654
- let removedDupCount = 0;
3655
- for (const page of pagesResult) {
3656
- const pageNumber = page.pageNumber;
3657
- const pdfHeight = page.pdfHeight;
3658
- const scaleX = page.renderedWidth > 0 ? page.pdfWidth / page.renderedWidth : 0.5;
3659
- const scaleY = page.renderedHeight > 0 ? page.pdfHeight / page.renderedHeight : 0.5;
3660
- const candidates = [];
3661
- for (const r of page.regions) {
3662
- if (!r.latex || !r.latex.trim()) continue;
3663
- const wrapped = r.kind === "display" ? `$$${r.latex}$$` : `$${r.latex}$`;
3664
- const x1 = r.bbox.x1 * scaleX;
3665
- const x2 = r.bbox.x2 * scaleX;
3666
- const yTop = pdfHeight - r.bbox.y1 * scaleY;
3667
- const yBottom = pdfHeight - r.bbox.y2 * scaleY;
3668
- const centerY = (yTop + yBottom) / 2;
3669
- const width = x2 - x1;
3670
- const height = yTop - yBottom;
3671
- candidates.push({
3672
- block: {
3673
- type: "paragraph",
3674
- text: wrapped,
3675
- pageNumber,
3676
- bbox: { page: pageNumber, x: x1, y: yBottom, width, height }
3677
- },
3678
- pdfBbox: { x1, x2, yTop, yBottom },
3679
- centerY
3680
- });
3681
- }
3682
- if (candidates.length === 0) continue;
3683
- const OVERLAP_THRESHOLD = 0.6;
3684
- const indicesToRemove = /* @__PURE__ */ new Set();
3685
- for (let i = 0; i < blocks.length; i++) {
3686
- const b = blocks[i];
3687
- if (b.pageNumber !== pageNumber) continue;
3688
- if (b.type === "table") continue;
3689
- if (!b.bbox || b.bbox.width <= 0 || b.bbox.height <= 0) continue;
3690
- const blockArea = b.bbox.width * b.bbox.height;
3691
- if (blockArea <= 0) continue;
3692
- for (const c of candidates) {
3693
- const ox1 = Math.max(b.bbox.x, c.pdfBbox.x1);
3694
- const ox2 = Math.min(b.bbox.x + b.bbox.width, c.pdfBbox.x2);
3695
- const oy1 = Math.max(b.bbox.y, c.pdfBbox.yBottom);
3696
- const oy2 = Math.min(b.bbox.y + b.bbox.height, c.pdfBbox.yTop);
3697
- const interArea = Math.max(0, ox2 - ox1) * Math.max(0, oy2 - oy1);
3698
- if (interArea / blockArea >= OVERLAP_THRESHOLD) {
3699
- indicesToRemove.add(i);
3700
- break;
3701
- }
3702
- }
3703
- }
3704
- if (indicesToRemove.size > 0) {
3705
- const sorted = [...indicesToRemove].sort((a, b) => b - a);
3706
- for (const idx of sorted) blocks.splice(idx, 1);
3707
- removedDupCount += indicesToRemove.size;
3708
- }
3709
- candidates.sort((a, b) => b.centerY - a.centerY);
3710
- for (const c of candidates) {
3711
- let insertIdx = -1;
3712
- let pageFirstIdx = -1;
3713
- let pageLastIdx = -1;
3714
- for (let i = 0; i < blocks.length; i++) {
3715
- const b = blocks[i];
3716
- if (b.pageNumber !== pageNumber) continue;
3717
- if (pageFirstIdx === -1) pageFirstIdx = i;
3718
- pageLastIdx = i;
3719
- if (!b.bbox) continue;
3720
- const blockCenter = b.bbox.y + b.bbox.height / 2;
3721
- if (blockCenter < c.centerY) {
3722
- insertIdx = i;
3723
- break;
3724
- }
3725
- }
3726
- if (insertIdx !== -1) {
3727
- blocks.splice(insertIdx, 0, c.block);
3728
- } else if (pageLastIdx !== -1) {
3729
- blocks.splice(pageLastIdx + 1, 0, c.block);
3730
- } else {
3731
- blocks.push(c.block);
3732
- }
3733
- insertedCount++;
3734
- }
3735
- }
3736
- if (insertedCount > 0 || removedDupCount > 0) {
3737
- process.stderr.write(
3738
- `[kordoc-formula] ${insertedCount}\uAC1C \uC218\uC2DD \uC0BD\uC785, ${removedDupCount}\uAC1C \uC911\uBCF5 block \uC81C\uAC70 (${pagesResult.length}\uAC1C \uD398\uC774\uC9C0)
3739
- `
3740
- );
3741
- }
3742
- } finally {
3743
- await pipeline.destroy().catch(() => {
3744
- });
3745
- }
3746
- }
3747
- function formatMb(bytes) {
3748
- return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
3749
- }
3750
-
3751
- // src/pdf/polyfill.ts
3752
- import * as pdfjsWorker from "pdfjs-dist/legacy/build/pdf.worker.mjs";
3753
- var g = globalThis;
3754
- if (typeof g.DOMMatrix === "undefined") {
3755
- g.DOMMatrix = class DOMMatrix {
3756
- m = [1, 0, 0, 1, 0, 0];
3757
- constructor(init) {
3758
- if (init) this.m = init;
3759
- }
3760
- };
3761
- }
3762
- if (typeof g.Path2D === "undefined") {
3763
- g.Path2D = class Path2D {
3764
- };
3765
- }
3766
- g.pdfjsWorker = pdfjsWorker;
3767
-
3768
- // src/pdf/parser.ts
3769
- import { getDocument, GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mjs";
3770
- import { createRequire } from "module";
3771
- import { dirname, join } from "path";
3772
- GlobalWorkerOptions.workerSrc = "";
3773
- var MAX_PAGES = 5e3;
3774
- var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
3775
- var PDF_LOAD_TIMEOUT_MS = 3e4;
3776
- var pdfjsAssets = {};
3777
- try {
3778
- const _require = createRequire(import.meta.url);
3779
- const pkgDir = dirname(_require.resolve("pdfjs-dist/package.json"));
3780
- pdfjsAssets.cMapUrl = join(pkgDir, "cmaps") + "/";
3781
- pdfjsAssets.cMapPacked = true;
3782
- pdfjsAssets.standardFontDataUrl = join(pkgDir, "standard_fonts") + "/";
3783
- } catch {
3784
- }
3785
- async function loadPdfWithTimeout(buffer) {
3786
- const loadingTask = getDocument({
3787
- data: new Uint8Array(buffer),
3788
- useSystemFonts: true,
3789
- disableFontFace: true,
3790
- isEvalSupported: false,
3791
- ...pdfjsAssets
3792
- });
3793
- let timer;
3794
- try {
3795
- return await Promise.race([
3796
- loadingTask.promise,
3797
- new Promise((_, reject) => {
3798
- timer = setTimeout(() => {
3799
- loadingTask.destroy();
3800
- reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
3801
- }, PDF_LOAD_TIMEOUT_MS);
3802
- })
3803
- ]);
3804
- } finally {
3805
- if (timer !== void 0) clearTimeout(timer);
3806
- }
3807
- }
3808
- async function parsePdfDocument(buffer, options) {
3809
- const formulaBuffer = options?.formulaOcr ? buffer.slice(0) : null;
3810
- const doc = await loadPdfWithTimeout(buffer);
3811
- try {
3812
- const pageCount = doc.numPages;
3813
- if (pageCount === 0) throw new KordocError("PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
3814
- const metadata = { pageCount };
3815
- await extractPdfMetadata(doc, metadata);
3816
- const blocks = [];
3817
- const warnings = [];
3818
- const pageQuality = [];
3819
- let totalChars = 0;
3820
- let totalTextBytes = 0;
3821
- const effectivePageCount = Math.min(pageCount, MAX_PAGES);
3822
- const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
3823
- const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
3824
- const fontSizeFreq = /* @__PURE__ */ new Map();
3825
- const pageHeights = /* @__PURE__ */ new Map();
3826
- const pagesWithLargeImage = /* @__PURE__ */ new Set();
3827
- const skippedImagePages = /* @__PURE__ */ new Map();
3828
- const imageState = createPdfImageState();
3829
- const extractedImages = [];
3830
- const pageImageBlocks = /* @__PURE__ */ new Map();
3831
- let parsedPages = 0;
3832
- for (let i = 1; i <= effectivePageCount; i++) {
3833
- if (pageFilter && !pageFilter.has(i)) continue;
3834
- try {
3835
- const page = await doc.getPage(i);
3836
- const tc = await page.getTextContent();
3837
- const viewport = page.getViewport({ scale: 1 });
3838
- pageHeights.set(i, viewport.height);
3839
- const rawItems = tc.items;
3840
- const items = normalizeItems(rawItems);
3841
- const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
3842
- if (hiddenCount > 0) {
3843
- warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
3844
- }
3845
- for (const item of visible) {
3846
- if (item.fontSize > 0) fontSizeFreq.set(item.fontSize, (fontSizeFreq.get(item.fontSize) || 0) + 1);
3847
- }
3848
- const opList = await page.getOperatorList();
3849
- const pageArea = viewport.width * viewport.height;
3850
- if (pageArea > 0) {
3851
- const imageRegions = extractImageRegions(opList.fnArray, opList.argsArray);
3852
- let uncovered = 0;
3853
- for (const r of imageRegions) {
3854
- const area = (r.x2 - r.x1) * (r.y2 - r.y1);
3855
- if (area < pageArea * 0.05) continue;
3856
- pagesWithLargeImage.add(i);
3857
- const hasText = visible.some((it) => {
3858
- const cx = it.x + it.w / 2;
3859
- const cy = it.y + (it.h || it.fontSize) / 2;
3860
- return cx >= r.x1 && cx <= r.x2 && cy >= r.y1 && cy <= r.y2;
3861
- });
3862
- if (!hasText) uncovered++;
3863
- }
3864
- if (uncovered > 0) skippedImagePages.set(i, uncovered);
3865
- }
3866
- const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
3867
- for (const b of pageBlocks) blocks.push(b);
3868
- try {
3869
- const { blocks: imgBlocks, images: pageImages } = await extractPageImages(page, opList.fnArray, opList.argsArray, i, imageState, warnings);
3870
- if (imgBlocks.length > 0) pageImageBlocks.set(i, imgBlocks);
3871
- extractedImages.push(...pageImages);
3872
- } catch {
3873
- }
3874
- let pageText = "";
3875
- for (const b of pageBlocks) {
3876
- let t = b.text || "";
3877
- if (b.type === "table" && b.table) {
3878
- const cellText = b.table.cells.map((row) => row.map((c) => c.text).join(" ")).join("\n");
3879
- t = t ? t + "\n" + cellText : cellText;
3880
- }
3881
- totalChars += t.replace(/\s/g, "").length;
3882
- totalTextBytes += t.length * 2;
3883
- pageText += pageText ? "\n" + t : t;
3884
- }
3885
- pageQuality.push(computePageQuality(i, pageText));
3886
- if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
3887
- parsedPages++;
3888
- options?.onProgress?.(parsedPages, totalTarget);
3889
- } catch (pageErr) {
3890
- if (pageErr instanceof KordocError) throw pageErr;
3891
- warnings.push({ page: i, message: `\uD398\uC774\uC9C0 ${i} \uD30C\uC2F1 \uC2E4\uD328: ${pageErr instanceof Error ? pageErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
3892
- }
3893
- }
3894
- const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
3895
- let isImageBased = false;
3896
- if (totalChars / Math.max(parsedPageCount, 1) < 10) {
3897
- if (options?.ocr) {
3898
- try {
3899
- const { ocrPages } = await import("./provider-C6IOGIS5.js");
3900
- const ocrBlocks = await ocrPages(doc, options.ocr, pageFilter, effectivePageCount);
3901
- if (ocrBlocks.length > 0) {
3902
- const ocrMarkdown = ocrBlocks.map((b) => b.text || "").filter(Boolean).join("\n\n");
3903
- return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true, pageQuality, qualitySummary: summarizeDocumentQuality(pageQuality), images: extractedImages.length > 0 ? extractedImages : void 0 };
3904
- }
3905
- } catch {
3906
- }
3907
- }
3908
- isImageBased = true;
3909
- warnings.push({
3910
- message: `\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (${pageCount}\uD398\uC774\uC9C0, \uD14D\uC2A4\uD2B8 ${totalChars}\uC790) \u2014 \uD14D\uC2A4\uD2B8 \uB808\uC774\uC5B4\uAC00 \uC5C6\uC5B4 OCR\uC774 \uD544\uC694\uD569\uB2C8\uB2E4`,
3911
- code: "NEEDS_OCR"
3912
- });
3913
- }
3914
- if (!isImageBased) {
3915
- const OCR_REASON_MESSAGES = {
3916
- low_text: "\uD14D\uC2A4\uD2B8\uAC00 \uAC70\uC758 \uC5C6\uB294 \uD398\uC774\uC9C0 (\uC2A4\uCE94/\uC774\uBBF8\uC9C0 \uCD94\uC815)",
3917
- high_pua: "\uAE00\uAF34 \uB9E4\uD551 \uC2E4\uD328 (PUA \uBE44\uC728 \uB192\uC74C) \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
3918
- high_control: "\uC81C\uC5B4\uBB38\uC790 \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
3919
- high_replacement: "\uB300\uCCB4\uBB38\uC790(U+FFFD) \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
3920
- garbled_hangul: "\uAE00\uAF34 \uB9E4\uD551 \uC2E4\uD328 (\uD55C\uAE00 \uC790\uC18C \uBD84\uD3EC \uC774\uC0C1) \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8\uAC00 \uAE68\uC84C\uC744 \uC218 \uC788\uC74C"
3921
- };
3922
- for (const pq of pageQuality) {
3923
- if (!pq.needsOcr || !pq.ocrReason) continue;
3924
- if (pq.ocrReason === "low_text" && !pagesWithLargeImage.has(pq.page)) continue;
3925
- warnings.push({ page: pq.page, message: `${OCR_REASON_MESSAGES[pq.ocrReason]} \u2014 OCR \uAC80\uD1A0 \uD544\uC694`, code: "NEEDS_OCR" });
3926
- }
3927
- }
3928
- if (!isImageBased) {
3929
- for (const [page, count] of [...skippedImagePages.entries()].sort((a, b) => a[0] - b[0])) {
3930
- warnings.push({ page, message: `${count}\uAC1C \uC774\uBBF8\uC9C0 \uC601\uC5ED\uC5D0 \uCD94\uCD9C \uAC00\uB2A5\uD55C \uD14D\uC2A4\uD2B8 \uC5C6\uC74C (\uADF8\uB9BC/\uCC28\uD2B8/\uB3C4\uC7A5 \uB0B4\uC6A9 \uB204\uB77D \uAC00\uB2A5)`, code: "SKIPPED_IMAGE" });
3931
- }
3932
- }
3933
- if (options?.removeHeaderFooter !== false && parsedPageCount >= 3) {
3934
- const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
3935
- for (let ri = removed.length - 1; ri >= 0; ri--) {
3936
- blocks.splice(removed[ri], 1);
3937
- }
3938
- }
3939
- mergeCrossPageTables(blocks);
3940
- injectPageImageBlocks(blocks, pageImageBlocks);
3941
- if (options?.formulaOcr && formulaBuffer) {
3942
- try {
3943
- await applyFormulaOcr(formulaBuffer, blocks, pageFilter, effectivePageCount, warnings, options.onProgress);
3944
- } catch (e) {
3945
- warnings.push({
3946
- message: `\uC218\uC2DD OCR \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`,
3947
- code: "PARTIAL_PARSE"
3948
- });
3949
- }
3950
- }
3951
- const medianFontSize = computeMedianFontSizeFromFreq(fontSizeFreq);
3952
- if (medianFontSize > 0) {
3953
- detectHeadings(blocks, medianFontSize);
3954
- }
3955
- detectMarkerHeadings(blocks);
3956
- detectTableCaptions(blocks);
3957
- detectKoreanListBlocks(blocks);
3958
- const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3959
- sanitizeBlockControlChars(blocks);
3960
- let markdown = cleanPdfText(blocksToMarkdown(blocks));
3961
- return {
3962
- markdown,
3963
- blocks,
3964
- metadata,
3965
- outline: outline.length > 0 ? outline : void 0,
3966
- warnings: warnings.length > 0 ? warnings : void 0,
3967
- isImageBased: isImageBased || void 0,
3968
- pageQuality,
3969
- qualitySummary: summarizeDocumentQuality(pageQuality),
3970
- images: extractedImages.length > 0 ? extractedImages : void 0
3971
- };
3972
- } finally {
3973
- await doc.destroy().catch(() => {
3974
- });
3975
- }
3976
- }
3977
- async function extractPdfMetadata(doc, metadata) {
3978
- try {
3979
- const result = await doc.getMetadata();
3980
- if (!result?.info) return;
3981
- const info = result.info;
3982
- if (typeof info.Title === "string" && info.Title.trim()) metadata.title = info.Title.trim();
3983
- if (typeof info.Author === "string" && info.Author.trim()) metadata.author = info.Author.trim();
3984
- if (typeof info.Creator === "string" && info.Creator.trim()) metadata.creator = info.Creator.trim();
3985
- if (typeof info.Subject === "string" && info.Subject.trim()) metadata.description = info.Subject.trim();
3986
- if (typeof info.Keywords === "string" && info.Keywords.trim()) {
3987
- metadata.keywords = info.Keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
3988
- }
3989
- if (typeof info.CreationDate === "string") metadata.createdAt = parsePdfDate(info.CreationDate);
3990
- if (typeof info.ModDate === "string") metadata.modifiedAt = parsePdfDate(info.ModDate);
3991
- } catch {
3992
- }
3993
- }
3994
- function parsePdfDate(dateStr) {
3995
- const m = dateStr.match(/D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?/);
3996
- if (!m) return void 0;
3997
- const [, year, month = "01", day = "01", hour = "00", min = "00", sec = "00"] = m;
3998
- return `${year}-${month}-${day}T${hour}:${min}:${sec}`;
3999
- }
4000
- async function extractPdfMetadataOnly(buffer) {
4001
- const doc = await loadPdfWithTimeout(buffer);
4002
- try {
4003
- const metadata = { pageCount: doc.numPages };
4004
- await extractPdfMetadata(doc, metadata);
4005
- return metadata;
4006
- } finally {
4007
- await doc.destroy().catch(() => {
4008
- });
4009
- }
4010
- }
4011
3329
  export {
4012
- cleanPdfText,
4013
- detectKoreanListBlocks,
3330
+ extractImageRegions,
3331
+ filterHiddenText,
3332
+ collapseEvenSpacing,
3333
+ normalizeItems,
3334
+ computeMedianFontSizeFromFreq,
3335
+ detectHeadings,
3336
+ detectMarkerHeadings,
4014
3337
  detectTableCaptions,
4015
- extractPdfMetadataOnly,
4016
- mergeCrossPageTables,
4017
- parsePdfDocument,
4018
- removeHeaderFooterBlocks
3338
+ detectKoreanListBlocks,
3339
+ removeHeaderFooterBlocks,
3340
+ extractPageBlocksWithLines,
3341
+ mergeCrossPageTables
4019
3342
  };
4020
- //# sourceMappingURL=parser-XX4QTDFQ.js.map
3343
+ //# sourceMappingURL=chunk-EZHWPKCH.js.map