kordoc 4.1.0 → 4.2.0

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