kordoc 4.0.8 → 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.
- package/README.md +50 -16
- package/dist/{-LE4RLXVA.js → -SXQIZHYA.js} +24 -10
- package/dist/{parser-TCTCSBYZ.js → chunk-26AJJCFF.js} +1427 -2027
- package/dist/chunk-26AJJCFF.js.map +1 -0
- package/dist/{parser-KMMUS73Q.js → chunk-4JPPLIIV.js} +1425 -2025
- package/dist/chunk-4JPPLIIV.js.map +1 -0
- package/dist/chunk-4VNLZ2GZ.js +13 -0
- package/dist/chunk-4VNLZ2GZ.js.map +1 -0
- package/dist/{chunk-2DZQF6YJ.js → chunk-6SHO3BNV.js} +24 -15
- package/dist/chunk-6SHO3BNV.js.map +1 -0
- package/dist/chunk-7FCNOKOR.js +144 -0
- package/dist/chunk-7FCNOKOR.js.map +1 -0
- package/dist/{chunk-6XAAYAWL.cjs → chunk-CMN3SYZ3.js} +62 -304
- package/dist/chunk-CMN3SYZ3.js.map +1 -0
- package/dist/chunk-DZIXKL7E.js +145 -0
- package/dist/chunk-DZIXKL7E.js.map +1 -0
- package/dist/chunk-ELOV2GIJ.js +82 -0
- package/dist/chunk-ELOV2GIJ.js.map +1 -0
- package/dist/chunk-F2HYH62H.cjs +130 -0
- package/dist/chunk-F2HYH62H.cjs.map +1 -0
- package/dist/{chunk-HMUEU7D7.js → chunk-FSXLSHCW.js} +3 -3
- package/dist/{chunk-HMUEU7D7.js.map → chunk-FSXLSHCW.js.map} +1 -1
- package/dist/chunk-GEPINOL6.js +83 -0
- package/dist/chunk-GEPINOL6.js.map +1 -0
- package/dist/chunk-H7OWEYH2.cjs +243 -0
- package/dist/chunk-H7OWEYH2.cjs.map +1 -0
- package/dist/{chunk-7S3M4N4E.js → chunk-IGGRLWT5.js} +683 -226
- package/dist/chunk-IGGRLWT5.js.map +1 -0
- package/dist/chunk-J2RZ444H.js +243 -0
- package/dist/chunk-J2RZ444H.js.map +1 -0
- package/dist/chunk-JP5YH3Z4.js +130 -0
- package/dist/chunk-JP5YH3Z4.js.map +1 -0
- package/dist/{parser-ITMU2WN5.cjs → chunk-LHROM2BO.cjs} +1409 -2012
- package/dist/chunk-LHROM2BO.cjs.map +1 -0
- package/dist/chunk-NNDVFNMJ.js +246 -0
- package/dist/chunk-NNDVFNMJ.js.map +1 -0
- package/dist/{chunk-RROH5WHO.js → chunk-PLIY23LY.js} +2 -2
- package/dist/{chunk-BNU5QGIZ.js → chunk-V6U6YS5B.js} +12 -4
- package/dist/chunk-V6U6YS5B.js.map +1 -0
- package/dist/{chunk-UEIYETCQ.js → chunk-WF2E5P2G.js} +40 -35
- package/dist/chunk-WF2E5P2G.js.map +1 -0
- package/dist/{chunk-D35VBACN.js → chunk-WQLHPOMJ.js} +7 -4
- package/dist/chunk-WQLHPOMJ.js.map +1 -0
- package/dist/{chunk-OQPILS7B.js → chunk-XTCT6KQ5.cjs} +79 -183
- package/dist/chunk-XTCT6KQ5.cjs.map +1 -0
- package/dist/{chunk-MEPHGCPQ.js → chunk-YOO6ET6M.js} +7 -3
- package/dist/chunk-YOO6ET6M.js.map +1 -0
- package/dist/chunks-ZQEFAZNC.js +10 -0
- package/dist/cli.js +228 -36
- package/dist/cli.js.map +1 -1
- package/dist/{detect-RI2MQ33K.js → detect-YEZX2XCF.js} +2 -2
- package/dist/{formula-RXVSQPXI.js → formula-KSNAEBZ2.js} +23 -232
- package/dist/formula-KSNAEBZ2.js.map +1 -0
- package/dist/{formula-5NKVS2LR.cjs → formula-SB3NHDUM.cjs} +30 -240
- package/dist/formula-SB3NHDUM.cjs.map +1 -0
- package/dist/{formula-JCNF43NE.js → formula-SXC6LRRC.js} +23 -233
- package/dist/formula-SXC6LRRC.js.map +1 -0
- package/dist/index.cjs +1280 -589
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -4
- package/dist/index.d.ts +112 -4
- package/dist/index.js +929 -238
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +278 -68
- package/dist/mcp.js.map +1 -1
- package/dist/models-KA4GLQK5.js +25 -0
- package/dist/parser-ND2YGQZR.js +800 -0
- package/dist/parser-ND2YGQZR.js.map +1 -0
- package/dist/parser-RBZHA6SY.js +804 -0
- package/dist/parser-RBZHA6SY.js.map +1 -0
- package/dist/parser-WDZOMDG4.cjs +803 -0
- package/dist/parser-WDZOMDG4.cjs.map +1 -0
- package/dist/pdf-ocr-2Z7VZIFI.cjs +439 -0
- package/dist/pdf-ocr-2Z7VZIFI.cjs.map +1 -0
- package/dist/pdf-ocr-FJFXFADQ.js +438 -0
- package/dist/pdf-ocr-FJFXFADQ.js.map +1 -0
- package/dist/pdf-ocr-VYE4DHD4.js +386 -0
- package/dist/pdf-ocr-VYE4DHD4.js.map +1 -0
- package/dist/{profile-io-SLN4P76T.js → profile-io-TWHDIVPL.js} +3 -3
- package/dist/rasterize-W2NEZUSW.js +40 -0
- package/dist/rasterize-W2NEZUSW.js.map +1 -0
- package/dist/redact-7ILEAUTS.js +12 -0
- package/dist/redact-7ILEAUTS.js.map +1 -0
- package/dist/render-4MG5QUDG.js +10 -0
- package/dist/render-4MG5QUDG.js.map +1 -0
- package/dist/seal-D2QNXJVO.js +10 -0
- package/dist/seal-D2QNXJVO.js.map +1 -0
- package/dist/{setup-57FB3LSP.js → setup-Q2PRE7UA.js} +5 -3
- package/dist/setup-Q2PRE7UA.js.map +1 -0
- package/dist/{watch-OHQWSVPE.js → watch-VWXJZT2R.js} +78 -23
- package/dist/watch-VWXJZT2R.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-2DZQF6YJ.js.map +0 -1
- package/dist/chunk-6XAAYAWL.cjs.map +0 -1
- package/dist/chunk-7S3M4N4E.js.map +0 -1
- package/dist/chunk-BNU5QGIZ.js.map +0 -1
- package/dist/chunk-D35VBACN.js.map +0 -1
- package/dist/chunk-MEPHGCPQ.js.map +0 -1
- package/dist/chunk-OQPILS7B.js.map +0 -1
- package/dist/chunk-UEIYETCQ.js.map +0 -1
- package/dist/formula-5NKVS2LR.cjs.map +0 -1
- package/dist/formula-JCNF43NE.js.map +0 -1
- package/dist/formula-RXVSQPXI.js.map +0 -1
- package/dist/parser-ITMU2WN5.cjs.map +0 -1
- package/dist/parser-KMMUS73Q.js.map +0 -1
- package/dist/parser-TCTCSBYZ.js.map +0 -1
- package/dist/provider-4ZJKV3DC.js +0 -37
- package/dist/provider-4ZJKV3DC.js.map +0 -1
- package/dist/provider-AKROB7WQ.js +0 -39
- package/dist/provider-AKROB7WQ.js.map +0 -1
- package/dist/provider-G4C2V2PD.cjs +0 -39
- package/dist/provider-G4C2V2PD.cjs.map +0 -1
- package/dist/render-25BT623I.js +0 -10
- package/dist/seal-R4TETA4C.js +0 -10
- package/dist/setup-57FB3LSP.js.map +0 -1
- package/dist/watch-OHQWSVPE.js.map +0 -1
- /package/dist/{-LE4RLXVA.js.map → -SXQIZHYA.js.map} +0 -0
- /package/dist/{chunk-RROH5WHO.js.map → chunk-PLIY23LY.js.map} +0 -0
- /package/dist/{detect-RI2MQ33K.js.map → chunks-ZQEFAZNC.js.map} +0 -0
- /package/dist/{profile-io-SLN4P76T.js.map → detect-YEZX2XCF.js.map} +0 -0
- /package/dist/{render-25BT623I.js.map → models-KA4GLQK5.js.map} +0 -0
- /package/dist/{seal-R4TETA4C.js.map → profile-io-TWHDIVPL.js.map} +0 -0
|
@@ -2,21 +2,67 @@
|
|
|
2
2
|
import {
|
|
3
3
|
HEADING_RATIO_H1,
|
|
4
4
|
HEADING_RATIO_H2,
|
|
5
|
-
HEADING_RATIO_H3
|
|
6
|
-
|
|
7
|
-
encodePng
|
|
8
|
-
} from "./chunk-UEIYETCQ.js";
|
|
5
|
+
HEADING_RATIO_H3
|
|
6
|
+
} from "./chunk-4VNLZ2GZ.js";
|
|
9
7
|
import {
|
|
10
|
-
parsePageRange
|
|
11
|
-
} from "./chunk-MOL7MDBG.js";
|
|
12
|
-
import {
|
|
13
|
-
KordocError,
|
|
14
8
|
safeMax,
|
|
15
9
|
safeMin
|
|
16
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FSXLSHCW.js";
|
|
17
11
|
|
|
18
|
-
// src/pdf/
|
|
12
|
+
// src/pdf/image-regions.ts
|
|
19
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";
|
|
20
66
|
var ORIENTATION_TOL = 2;
|
|
21
67
|
var MIN_LINE_LENGTH = 15;
|
|
22
68
|
var MAX_LINE_WIDTH = 5;
|
|
@@ -65,16 +111,16 @@ function extractLines(fnArray, argsArray) {
|
|
|
65
111
|
const op = fnArray[i];
|
|
66
112
|
const args = argsArray[i];
|
|
67
113
|
switch (op) {
|
|
68
|
-
case
|
|
114
|
+
case OPS2.setLineWidth:
|
|
69
115
|
lineWidth = args[0] || 1;
|
|
70
116
|
break;
|
|
71
|
-
case
|
|
117
|
+
case OPS2.save:
|
|
72
118
|
ctmStack.push(ctm.slice());
|
|
73
119
|
break;
|
|
74
|
-
case
|
|
120
|
+
case OPS2.restore:
|
|
75
121
|
ctm = ctmStack.pop() ?? [1, 0, 0, 1, 0, 0];
|
|
76
122
|
break;
|
|
77
|
-
case
|
|
123
|
+
case OPS2.transform: {
|
|
78
124
|
const t = args;
|
|
79
125
|
ctm = [
|
|
80
126
|
ctm[0] * t[0] + ctm[2] * t[1],
|
|
@@ -86,36 +132,36 @@ function extractLines(fnArray, argsArray) {
|
|
|
86
132
|
];
|
|
87
133
|
break;
|
|
88
134
|
}
|
|
89
|
-
case
|
|
135
|
+
case OPS2.constructPath: {
|
|
90
136
|
const arg0 = args[0];
|
|
91
137
|
if (Array.isArray(arg0)) {
|
|
92
138
|
const subOps = arg0;
|
|
93
139
|
const coords = args[1];
|
|
94
140
|
let ci = 0;
|
|
95
141
|
for (const subOp of subOps) {
|
|
96
|
-
if (subOp ===
|
|
142
|
+
if (subOp === OPS2.moveTo) {
|
|
97
143
|
curX = coords[ci++];
|
|
98
144
|
curY = coords[ci++];
|
|
99
145
|
pathStartX = curX;
|
|
100
146
|
pathStartY = curY;
|
|
101
|
-
} else if (subOp ===
|
|
147
|
+
} else if (subOp === OPS2.lineTo) {
|
|
102
148
|
const x2 = coords[ci++], y2 = coords[ci++];
|
|
103
149
|
pushSeg(curX, curY, x2, y2);
|
|
104
150
|
curX = x2;
|
|
105
151
|
curY = y2;
|
|
106
|
-
} else if (subOp ===
|
|
152
|
+
} else if (subOp === OPS2.rectangle) {
|
|
107
153
|
const rx = coords[ci++], ry = coords[ci++];
|
|
108
154
|
const rw = coords[ci++], rh = coords[ci++];
|
|
109
155
|
pushRectangle(rx, ry, rw, rh);
|
|
110
|
-
} else if (subOp ===
|
|
156
|
+
} else if (subOp === OPS2.closePath) {
|
|
111
157
|
if (curX !== pathStartX || curY !== pathStartY) {
|
|
112
158
|
pushSeg(curX, curY, pathStartX, pathStartY);
|
|
113
159
|
}
|
|
114
160
|
curX = pathStartX;
|
|
115
161
|
curY = pathStartY;
|
|
116
|
-
} else if (subOp ===
|
|
162
|
+
} else if (subOp === OPS2.curveTo) {
|
|
117
163
|
ci += 6;
|
|
118
|
-
} else if (subOp ===
|
|
164
|
+
} else if (subOp === OPS2.curveTo2 || subOp === OPS2.curveTo3) {
|
|
119
165
|
ci += 4;
|
|
120
166
|
}
|
|
121
167
|
}
|
|
@@ -153,33 +199,33 @@ function extractLines(fnArray, argsArray) {
|
|
|
153
199
|
}
|
|
154
200
|
}
|
|
155
201
|
}
|
|
156
|
-
if (afterOp ===
|
|
202
|
+
if (afterOp === OPS2.stroke || afterOp === OPS2.closeStroke) {
|
|
157
203
|
flushPath(true);
|
|
158
|
-
} else if (afterOp ===
|
|
204
|
+
} else if (afterOp === OPS2.fill || afterOp === OPS2.eoFill) {
|
|
159
205
|
flushPath(true, true);
|
|
160
|
-
} else if (afterOp ===
|
|
206
|
+
} else if (afterOp === OPS2.fillStroke || afterOp === OPS2.eoFillStroke || afterOp === OPS2.closeFillStroke || afterOp === OPS2.closeEOFillStroke) {
|
|
161
207
|
flushPath(true);
|
|
162
|
-
} else if (afterOp ===
|
|
208
|
+
} else if (afterOp === OPS2.endPath) {
|
|
163
209
|
flushPath(false);
|
|
164
210
|
}
|
|
165
211
|
}
|
|
166
212
|
break;
|
|
167
213
|
}
|
|
168
|
-
case
|
|
169
|
-
case
|
|
214
|
+
case OPS2.stroke:
|
|
215
|
+
case OPS2.closeStroke:
|
|
170
216
|
flushPath(true);
|
|
171
217
|
break;
|
|
172
|
-
case
|
|
173
|
-
case
|
|
218
|
+
case OPS2.fill:
|
|
219
|
+
case OPS2.eoFill:
|
|
174
220
|
flushPath(true, true);
|
|
175
221
|
break;
|
|
176
|
-
case
|
|
177
|
-
case
|
|
178
|
-
case
|
|
179
|
-
case
|
|
222
|
+
case OPS2.fillStroke:
|
|
223
|
+
case OPS2.eoFillStroke:
|
|
224
|
+
case OPS2.closeFillStroke:
|
|
225
|
+
case OPS2.closeEOFillStroke:
|
|
180
226
|
flushPath(true);
|
|
181
227
|
break;
|
|
182
|
-
case
|
|
228
|
+
case OPS2.endPath:
|
|
183
229
|
flushPath(false);
|
|
184
230
|
break;
|
|
185
231
|
}
|
|
@@ -315,9 +361,9 @@ function closeOpenTableEdges(horizontals, verticals) {
|
|
|
315
361
|
const groups = [];
|
|
316
362
|
for (const hl of chainCollinearRules(horizontals)) {
|
|
317
363
|
let placed = false;
|
|
318
|
-
for (const
|
|
319
|
-
if (Math.abs(
|
|
320
|
-
|
|
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);
|
|
321
367
|
placed = true;
|
|
322
368
|
break;
|
|
323
369
|
}
|
|
@@ -325,8 +371,8 @@ function closeOpenTableEdges(horizontals, verticals) {
|
|
|
325
371
|
if (!placed) groups.push([hl]);
|
|
326
372
|
}
|
|
327
373
|
const splitGroups = [];
|
|
328
|
-
for (const
|
|
329
|
-
const sorted = [...
|
|
374
|
+
for (const g of groups) {
|
|
375
|
+
const sorted = [...g].sort((a, b) => a.y1 - b.y1);
|
|
330
376
|
const gaps = [];
|
|
331
377
|
for (let i = 1; i < sorted.length; i++) gaps.push(sorted[i].y1 - sorted[i - 1].y1);
|
|
332
378
|
const median = gaps.length ? [...gaps].sort((a, b) => a - b)[gaps.length >> 1] : 0;
|
|
@@ -347,21 +393,21 @@ function closeOpenTableEdges(horizontals, verticals) {
|
|
|
347
393
|
if (cur.length) splitGroups.push(cur);
|
|
348
394
|
}
|
|
349
395
|
const synthesized = [];
|
|
350
|
-
for (const
|
|
351
|
-
if (
|
|
396
|
+
for (const g of splitGroups) {
|
|
397
|
+
if (g.length < EDGE_MIN_RULES) continue;
|
|
352
398
|
let yMin = Infinity, yMax = -Infinity, x1 = 0, x2 = 0;
|
|
353
|
-
for (const hl of
|
|
399
|
+
for (const hl of g) {
|
|
354
400
|
if (hl.y1 < yMin) yMin = hl.y1;
|
|
355
401
|
if (hl.y1 > yMax) yMax = hl.y1;
|
|
356
402
|
x1 += hl.x1;
|
|
357
403
|
x2 += hl.x2;
|
|
358
404
|
}
|
|
359
|
-
x1 /=
|
|
360
|
-
x2 /=
|
|
405
|
+
x1 /= g.length;
|
|
406
|
+
x2 /= g.length;
|
|
361
407
|
if (yMax - yMin < EDGE_MIN_SPAN) continue;
|
|
362
408
|
const crossCount = (v) => {
|
|
363
409
|
let n = 0;
|
|
364
|
-
for (const hl of
|
|
410
|
+
for (const hl of g) {
|
|
365
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++;
|
|
366
412
|
}
|
|
367
413
|
return n;
|
|
@@ -498,58 +544,6 @@ function bridgeSplitColumnVerticals(horizontals, verticals) {
|
|
|
498
544
|
return synthesized.length ? [...verticals, ...synthesized] : verticals;
|
|
499
545
|
}
|
|
500
546
|
|
|
501
|
-
// src/pdf/image-regions.ts
|
|
502
|
-
import { OPS as OPS2 } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
503
|
-
function multiplyTransform(m, t) {
|
|
504
|
-
return [
|
|
505
|
-
m[0] * t[0] + m[2] * t[1],
|
|
506
|
-
m[1] * t[0] + m[3] * t[1],
|
|
507
|
-
m[0] * t[2] + m[2] * t[3],
|
|
508
|
-
m[1] * t[2] + m[3] * t[3],
|
|
509
|
-
m[0] * t[4] + m[2] * t[5] + m[4],
|
|
510
|
-
m[1] * t[4] + m[3] * t[5] + m[5]
|
|
511
|
-
];
|
|
512
|
-
}
|
|
513
|
-
function extractImageRegions(fnArray, argsArray) {
|
|
514
|
-
const regions = [];
|
|
515
|
-
let ctm = [1, 0, 0, 1, 0, 0];
|
|
516
|
-
const stack = [];
|
|
517
|
-
for (let i = 0; i < fnArray.length; i++) {
|
|
518
|
-
const op = fnArray[i];
|
|
519
|
-
switch (op) {
|
|
520
|
-
case OPS2.save:
|
|
521
|
-
stack.push(ctm);
|
|
522
|
-
break;
|
|
523
|
-
case OPS2.restore:
|
|
524
|
-
ctm = stack.pop() || [1, 0, 0, 1, 0, 0];
|
|
525
|
-
break;
|
|
526
|
-
case OPS2.transform: {
|
|
527
|
-
const t = argsArray[i];
|
|
528
|
-
if (Array.isArray(t) && t.length >= 6) ctm = multiplyTransform(ctm, t);
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
case OPS2.paintImageXObject:
|
|
532
|
-
case OPS2.paintInlineImageXObject:
|
|
533
|
-
case OPS2.paintImageMaskXObject:
|
|
534
|
-
case OPS2.paintImageXObjectRepeat: {
|
|
535
|
-
const corners = [[0, 0], [1, 0], [0, 1], [1, 1]];
|
|
536
|
-
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
|
537
|
-
for (const [u, v] of corners) {
|
|
538
|
-
const x = ctm[0] * u + ctm[2] * v + ctm[4];
|
|
539
|
-
const y = ctm[1] * u + ctm[3] * v + ctm[5];
|
|
540
|
-
if (x < x1) x1 = x;
|
|
541
|
-
if (x > x2) x2 = x;
|
|
542
|
-
if (y < y1) y1 = y;
|
|
543
|
-
if (y > y2) y2 = y;
|
|
544
|
-
}
|
|
545
|
-
if (x2 - x1 > 0 && y2 - y1 > 0) regions.push({ x1, y1, x2, y2 });
|
|
546
|
-
break;
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return regions;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
547
|
// src/pdf/line-types.ts
|
|
554
548
|
var VERTEX_MERGE_FACTOR = 4;
|
|
555
549
|
|
|
@@ -581,6 +575,18 @@ function buildVertices(horizontals, verticals) {
|
|
|
581
575
|
}
|
|
582
576
|
function mergeVertices(vertices) {
|
|
583
577
|
if (vertices.length <= 1) return vertices;
|
|
578
|
+
let maxRadiusAll = 1;
|
|
579
|
+
for (const v of vertices) {
|
|
580
|
+
if (v.radius > maxRadiusAll) maxRadiusAll = v.radius;
|
|
581
|
+
}
|
|
582
|
+
const cell = Math.max(VERTEX_MERGE_FACTOR * maxRadiusAll, 1);
|
|
583
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
584
|
+
for (let i = 0; i < vertices.length; i++) {
|
|
585
|
+
const key = Math.floor(vertices[i].x / cell) + "," + Math.floor(vertices[i].y / cell);
|
|
586
|
+
const arr = buckets.get(key);
|
|
587
|
+
if (arr) arr.push(i);
|
|
588
|
+
else buckets.set(key, [i]);
|
|
589
|
+
}
|
|
584
590
|
const merged = [];
|
|
585
591
|
const used = new Array(vertices.length).fill(false);
|
|
586
592
|
for (let i = 0; i < vertices.length; i++) {
|
|
@@ -588,7 +594,20 @@ function mergeVertices(vertices) {
|
|
|
588
594
|
let sumX = vertices[i].x, sumY = vertices[i].y;
|
|
589
595
|
let maxRadius = vertices[i].radius;
|
|
590
596
|
let count = 1;
|
|
591
|
-
|
|
597
|
+
const cx = Math.floor(vertices[i].x / cell);
|
|
598
|
+
const cy = Math.floor(vertices[i].y / cell);
|
|
599
|
+
const candidates = [];
|
|
600
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
601
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
602
|
+
const arr = buckets.get(cx + dx + "," + (cy + dy));
|
|
603
|
+
if (!arr) continue;
|
|
604
|
+
for (const j of arr) {
|
|
605
|
+
if (j > i && !used[j]) candidates.push(j);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
candidates.sort((a, b) => a - b);
|
|
610
|
+
for (const j of candidates) {
|
|
592
611
|
if (used[j]) continue;
|
|
593
612
|
const mergeTol = VERTEX_MERGE_FACTOR * Math.max(maxRadius, vertices[j].radius);
|
|
594
613
|
if (Math.abs(vertices[i].x - vertices[j].x) <= mergeTol && Math.abs(vertices[i].y - vertices[j].y) <= mergeTol) {
|
|
@@ -614,8 +633,8 @@ function buildTableGrids(horizontals, verticals) {
|
|
|
614
633
|
...verticals.map((l, i) => ({ ...l, type: "v", id: i + horizontals.length }))
|
|
615
634
|
];
|
|
616
635
|
const groups = [];
|
|
617
|
-
for (const
|
|
618
|
-
const bands = splitStackedGroup(
|
|
636
|
+
for (const g of groupConnectedLines(allLines)) {
|
|
637
|
+
const bands = splitStackedGroup(g);
|
|
619
638
|
for (const b of bands) groups.push({ lines: b, fromSplit: bands.length > 1 });
|
|
620
639
|
}
|
|
621
640
|
const grids = [];
|
|
@@ -815,6 +834,7 @@ function splitStackedGroup(group) {
|
|
|
815
834
|
}
|
|
816
835
|
return bands.filter((b) => b.length > 0);
|
|
817
836
|
}
|
|
837
|
+
var GROUP_BUCKET_CELL = 100;
|
|
818
838
|
function groupConnectedLines(lines) {
|
|
819
839
|
const parent = lines.map((_, i) => i);
|
|
820
840
|
function find(x) {
|
|
@@ -828,10 +848,34 @@ function groupConnectedLines(lines) {
|
|
|
828
848
|
const ra = find(a), rb = find(b);
|
|
829
849
|
if (ra !== rb) parent[ra] = rb;
|
|
830
850
|
}
|
|
851
|
+
const cellMap = /* @__PURE__ */ new Map();
|
|
831
852
|
for (let i = 0; i < lines.length; i++) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
853
|
+
const l = lines[i];
|
|
854
|
+
const cx1 = Math.floor((Math.min(l.x1, l.x2) - CONNECT_TOL) / GROUP_BUCKET_CELL);
|
|
855
|
+
const cx2 = Math.floor((Math.max(l.x1, l.x2) + CONNECT_TOL) / GROUP_BUCKET_CELL);
|
|
856
|
+
const cy1 = Math.floor((Math.min(l.y1, l.y2) - CONNECT_TOL) / GROUP_BUCKET_CELL);
|
|
857
|
+
const cy2 = Math.floor((Math.max(l.y1, l.y2) + CONNECT_TOL) / GROUP_BUCKET_CELL);
|
|
858
|
+
for (let cx = cx1; cx <= cx2; cx++) {
|
|
859
|
+
for (let cy = cy1; cy <= cy2; cy++) {
|
|
860
|
+
const key = cx + "," + cy;
|
|
861
|
+
const arr = cellMap.get(key);
|
|
862
|
+
if (arr) arr.push(i);
|
|
863
|
+
else cellMap.set(key, [i]);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const tested = /* @__PURE__ */ new Set();
|
|
868
|
+
for (const arr of cellMap.values()) {
|
|
869
|
+
for (let a = 0; a < arr.length; a++) {
|
|
870
|
+
for (let b = a + 1; b < arr.length; b++) {
|
|
871
|
+
const i = Math.min(arr[a], arr[b]);
|
|
872
|
+
const j = Math.max(arr[a], arr[b]);
|
|
873
|
+
const key = i * lines.length + j;
|
|
874
|
+
if (tested.has(key)) continue;
|
|
875
|
+
tested.add(key);
|
|
876
|
+
if (linesIntersect(lines[i], lines[j])) {
|
|
877
|
+
union(i, j);
|
|
878
|
+
}
|
|
835
879
|
}
|
|
836
880
|
}
|
|
837
881
|
}
|
|
@@ -1011,7 +1055,8 @@ function cellTextToString(items) {
|
|
|
1011
1055
|
}
|
|
1012
1056
|
}
|
|
1013
1057
|
lines.push(curLine);
|
|
1014
|
-
const
|
|
1058
|
+
const merged = mergeSuperscriptRows(lines);
|
|
1059
|
+
const textLines = merged.map((line) => {
|
|
1015
1060
|
const s = line.sort((a, b) => a.x - b.x);
|
|
1016
1061
|
if (s.length === 1) return s[0].text;
|
|
1017
1062
|
const evenSpaced = detectEvenSpacedItems(s);
|
|
@@ -1035,6 +1080,40 @@ function cellTextToString(items) {
|
|
|
1035
1080
|
});
|
|
1036
1081
|
return mergeCellTextLines(textLines);
|
|
1037
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
|
+
}
|
|
1038
1117
|
function detectEvenSpacedItems(items) {
|
|
1039
1118
|
const result = new Array(items.length).fill(false);
|
|
1040
1119
|
if (items.length < 3) return result;
|
|
@@ -1074,12 +1153,12 @@ function markEvenRun(items, result, start, end) {
|
|
|
1074
1153
|
for (let i = start + 1; i < end; i++) {
|
|
1075
1154
|
gaps.push(items[i].x - (items[i - 1].x + items[i - 1].w));
|
|
1076
1155
|
}
|
|
1077
|
-
const posGaps = gaps.filter((
|
|
1156
|
+
const posGaps = gaps.filter((g) => g > 0);
|
|
1078
1157
|
if (posGaps.length < 2) return;
|
|
1079
1158
|
let minGap = Infinity, maxGap = -Infinity;
|
|
1080
|
-
for (const
|
|
1081
|
-
if (
|
|
1082
|
-
if (
|
|
1159
|
+
for (const g of posGaps) {
|
|
1160
|
+
if (g < minGap) minGap = g;
|
|
1161
|
+
if (g > maxGap) maxGap = g;
|
|
1083
1162
|
}
|
|
1084
1163
|
const avgFs = items[start].fontSize;
|
|
1085
1164
|
if (minGap >= avgFs * 0.1 && maxGap <= avgFs * 3 && maxGap / Math.max(minGap, 0.1) <= 3) {
|
|
@@ -1224,301 +1303,6 @@ function normalizeUndersegmentedTable(originalCells, colXs, items) {
|
|
|
1224
1303
|
return rebuilt;
|
|
1225
1304
|
}
|
|
1226
1305
|
|
|
1227
|
-
// src/pdf/image-extract.ts
|
|
1228
|
-
import { OPS as OPS3, ImageKind } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
1229
|
-
var IMAGE_RESOLVE_TIMEOUT_MS = 5e3;
|
|
1230
|
-
function resolveImgData(page, objId) {
|
|
1231
|
-
const store = objId.startsWith("g_") ? page.commonObjs : page.objs;
|
|
1232
|
-
return new Promise((resolve) => {
|
|
1233
|
-
let done = false;
|
|
1234
|
-
const timer = setTimeout(() => {
|
|
1235
|
-
if (!done) {
|
|
1236
|
-
done = true;
|
|
1237
|
-
resolve(null);
|
|
1238
|
-
}
|
|
1239
|
-
}, IMAGE_RESOLVE_TIMEOUT_MS);
|
|
1240
|
-
try {
|
|
1241
|
-
store.get(objId, (data) => {
|
|
1242
|
-
if (!done) {
|
|
1243
|
-
done = true;
|
|
1244
|
-
clearTimeout(timer);
|
|
1245
|
-
resolve(data ?? null);
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
} catch {
|
|
1249
|
-
if (!done) {
|
|
1250
|
-
done = true;
|
|
1251
|
-
clearTimeout(timer);
|
|
1252
|
-
resolve(null);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
var MIN_DIM = 8;
|
|
1258
|
-
var MAX_PIXELS = 36e6;
|
|
1259
|
-
var MAX_IMAGES_PER_DOC = 200;
|
|
1260
|
-
var MAX_TOTAL_IMAGE_BYTES = 128 * 1024 * 1024;
|
|
1261
|
-
function createPdfImageState() {
|
|
1262
|
-
return { imageIndex: 0, totalBytes: 0, seen: /* @__PURE__ */ new Map(), capWarned: false };
|
|
1263
|
-
}
|
|
1264
|
-
function fnv1a(data) {
|
|
1265
|
-
let h = 2166136261;
|
|
1266
|
-
for (let i = 0; i < data.length; i++) {
|
|
1267
|
-
h ^= data[i];
|
|
1268
|
-
h = Math.imul(h, 16777619);
|
|
1269
|
-
}
|
|
1270
|
-
return h >>> 0;
|
|
1271
|
-
}
|
|
1272
|
-
function toRgba(img) {
|
|
1273
|
-
const { width: w, height: h, kind, data } = img;
|
|
1274
|
-
if (!data || !w || !h) return null;
|
|
1275
|
-
const rgba = new Uint8Array(w * h * 4);
|
|
1276
|
-
if (kind === ImageKind.RGBA_32BPP) {
|
|
1277
|
-
if (data.length < w * h * 4) return null;
|
|
1278
|
-
rgba.set(data.subarray(0, w * h * 4));
|
|
1279
|
-
return rgba;
|
|
1280
|
-
}
|
|
1281
|
-
if (kind === ImageKind.RGB_24BPP) {
|
|
1282
|
-
if (data.length < w * h * 3) return null;
|
|
1283
|
-
for (let i = 0, s = 0, d = 0; i < w * h; i++, s += 3, d += 4) {
|
|
1284
|
-
rgba[d] = data[s];
|
|
1285
|
-
rgba[d + 1] = data[s + 1];
|
|
1286
|
-
rgba[d + 2] = data[s + 2];
|
|
1287
|
-
rgba[d + 3] = 255;
|
|
1288
|
-
}
|
|
1289
|
-
return rgba;
|
|
1290
|
-
}
|
|
1291
|
-
if (kind === ImageKind.GRAYSCALE_1BPP) {
|
|
1292
|
-
const stride = w + 7 >> 3;
|
|
1293
|
-
if (data.length < stride * h) return null;
|
|
1294
|
-
for (let y = 0; y < h; y++) {
|
|
1295
|
-
for (let x = 0; x < w; x++) {
|
|
1296
|
-
const bit = data[y * stride + (x >> 3)] >> 7 - (x & 7) & 1;
|
|
1297
|
-
const v = bit ? 255 : 0;
|
|
1298
|
-
const d = (y * w + x) * 4;
|
|
1299
|
-
rgba[d] = v;
|
|
1300
|
-
rgba[d + 1] = v;
|
|
1301
|
-
rgba[d + 2] = v;
|
|
1302
|
-
rgba[d + 3] = 255;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return rgba;
|
|
1306
|
-
}
|
|
1307
|
-
return null;
|
|
1308
|
-
}
|
|
1309
|
-
async function extractPageImages(page, fnArray, argsArray, pageNumber, state, warnings) {
|
|
1310
|
-
const blocks = [];
|
|
1311
|
-
const images = [];
|
|
1312
|
-
const pageSeenIds = /* @__PURE__ */ new Set();
|
|
1313
|
-
for (let i = 0; i < fnArray.length; i++) {
|
|
1314
|
-
const op = fnArray[i];
|
|
1315
|
-
let imgData = null;
|
|
1316
|
-
let dedupeId = null;
|
|
1317
|
-
if (op === OPS3.paintImageXObject || op === OPS3.paintImageXObjectRepeat) {
|
|
1318
|
-
const objId = argsArray[i]?.[0];
|
|
1319
|
-
if (typeof objId !== "string" || pageSeenIds.has(objId)) continue;
|
|
1320
|
-
pageSeenIds.add(objId);
|
|
1321
|
-
dedupeId = objId;
|
|
1322
|
-
imgData = await resolveImgData(page, objId);
|
|
1323
|
-
} else if (op === OPS3.paintInlineImageXObject) {
|
|
1324
|
-
imgData = argsArray[i]?.[0];
|
|
1325
|
-
} else {
|
|
1326
|
-
continue;
|
|
1327
|
-
}
|
|
1328
|
-
if (!imgData?.data || !imgData.width || !imgData.height) continue;
|
|
1329
|
-
const { width: w, height: h } = imgData;
|
|
1330
|
-
if (w < MIN_DIM || h < MIN_DIM) continue;
|
|
1331
|
-
if (w * h > MAX_PIXELS) {
|
|
1332
|
-
warnings.push({ page: pageNumber, message: `\uC774\uBBF8\uC9C0 \uD06C\uAE30 \uCD08\uACFC\uB85C \uCD94\uCD9C \uC0DD\uB7B5 (${w}\xD7${h})`, code: "SKIPPED_IMAGE" });
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
const hash = `${w}x${h}k${imgData.kind ?? "?"}h${fnv1a(imgData.data)}${dedupeId ? "" : "inline"}`;
|
|
1336
|
-
if (state.seen.has(hash)) continue;
|
|
1337
|
-
if (state.imageIndex >= MAX_IMAGES_PER_DOC || state.totalBytes >= MAX_TOTAL_IMAGE_BYTES) {
|
|
1338
|
-
if (!state.capWarned) {
|
|
1339
|
-
state.capWarned = true;
|
|
1340
|
-
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" });
|
|
1341
|
-
}
|
|
1342
|
-
continue;
|
|
1343
|
-
}
|
|
1344
|
-
const rgba = toRgba(imgData);
|
|
1345
|
-
if (!rgba) continue;
|
|
1346
|
-
const png = encodePng(w, h, rgba);
|
|
1347
|
-
state.imageIndex++;
|
|
1348
|
-
state.totalBytes += png.length;
|
|
1349
|
-
const filename = `image_${String(state.imageIndex).padStart(3, "0")}.png`;
|
|
1350
|
-
state.seen.set(hash, filename);
|
|
1351
|
-
images.push({ filename, data: png, mimeType: "image/png" });
|
|
1352
|
-
blocks.push({ type: "image", text: filename, pageNumber });
|
|
1353
|
-
}
|
|
1354
|
-
return { blocks, images };
|
|
1355
|
-
}
|
|
1356
|
-
function injectPageImageBlocks(blocks, pageImages) {
|
|
1357
|
-
if (pageImages.size === 0) return;
|
|
1358
|
-
const lastIndexForPage = /* @__PURE__ */ new Map();
|
|
1359
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1360
|
-
const p = blocks[i].pageNumber;
|
|
1361
|
-
if (p !== void 0) lastIndexForPage.set(p, i);
|
|
1362
|
-
}
|
|
1363
|
-
const pageAtIndex = /* @__PURE__ */ new Map();
|
|
1364
|
-
for (const [p, last] of lastIndexForPage) pageAtIndex.set(last, p);
|
|
1365
|
-
const result = [];
|
|
1366
|
-
const injected = /* @__PURE__ */ new Set();
|
|
1367
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
1368
|
-
result.push(blocks[i]);
|
|
1369
|
-
const p = pageAtIndex.get(i);
|
|
1370
|
-
if (p !== void 0 && pageImages.has(p)) {
|
|
1371
|
-
result.push(...pageImages.get(p));
|
|
1372
|
-
injected.add(p);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
for (const p of [...pageImages.keys()].sort((a, b) => a - b)) {
|
|
1376
|
-
if (injected.has(p)) continue;
|
|
1377
|
-
let at = result.length;
|
|
1378
|
-
for (let i = 0; i < result.length; i++) {
|
|
1379
|
-
const bp = result[i].pageNumber;
|
|
1380
|
-
if (bp !== void 0 && bp > p) {
|
|
1381
|
-
at = i;
|
|
1382
|
-
break;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
result.splice(at, 0, ...pageImages.get(p));
|
|
1386
|
-
}
|
|
1387
|
-
blocks.length = 0;
|
|
1388
|
-
blocks.push(...result);
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
// src/pdf/quality.ts
|
|
1392
|
-
var RARE_JONG = /* @__PURE__ */ new Set([3, 5, 6, 9, 10, 11, 12, 13, 14, 15, 18, 24, 25, 26]);
|
|
1393
|
-
var MOJIBAKE_MIN_HANGUL = 30;
|
|
1394
|
-
var MOJIBAKE_MAX_NOBATCHIM = 0.15;
|
|
1395
|
-
var MOJIBAKE_MIN_RAREBATCHIM = 0.25;
|
|
1396
|
-
function computePageQuality(page, text) {
|
|
1397
|
-
let total = 0;
|
|
1398
|
-
let hangul = 0;
|
|
1399
|
-
let hangulNoBatchim = 0;
|
|
1400
|
-
let hangulRareBatchim = 0;
|
|
1401
|
-
let control = 0;
|
|
1402
|
-
let replacement = 0;
|
|
1403
|
-
let pua = 0;
|
|
1404
|
-
for (let i = 0; i < text.length; i++) {
|
|
1405
|
-
const code = text.charCodeAt(i);
|
|
1406
|
-
if (code === 32 || code === 9 || code === 10 || code === 13) continue;
|
|
1407
|
-
total++;
|
|
1408
|
-
if (code < 32 || code === 127 || code >= 128 && code <= 159) {
|
|
1409
|
-
control++;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
if (code === 65533) {
|
|
1413
|
-
replacement++;
|
|
1414
|
-
continue;
|
|
1415
|
-
}
|
|
1416
|
-
if (code >= 44032 && code <= 55203) {
|
|
1417
|
-
hangul++;
|
|
1418
|
-
const jong = (code - 44032) % 28;
|
|
1419
|
-
if (jong === 0) hangulNoBatchim++;
|
|
1420
|
-
else if (RARE_JONG.has(jong)) hangulRareBatchim++;
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
if (code >= 57344 && code <= 63743 || code >= 56192 && code <= 56319) {
|
|
1424
|
-
pua++;
|
|
1425
|
-
continue;
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
const denom = total || 1;
|
|
1429
|
-
const puaRatio = pua / denom;
|
|
1430
|
-
const controlCharRatio = control / denom;
|
|
1431
|
-
const replacementCharRatio = replacement / denom;
|
|
1432
|
-
const hangulNoBatchimRatio = hangul > 0 ? hangulNoBatchim / hangul : 0;
|
|
1433
|
-
const hangulRareBatchimRatio = hangul > 0 ? hangulRareBatchim / hangul : 0;
|
|
1434
|
-
const garbledHangul = hangul >= MOJIBAKE_MIN_HANGUL && hangulNoBatchimRatio < MOJIBAKE_MAX_NOBATCHIM && hangulRareBatchimRatio >= MOJIBAKE_MIN_RAREBATCHIM;
|
|
1435
|
-
let needsOcr = false;
|
|
1436
|
-
let ocrReason;
|
|
1437
|
-
if (total < LOW_TEXT_THRESHOLD) {
|
|
1438
|
-
needsOcr = true;
|
|
1439
|
-
ocrReason = "low_text";
|
|
1440
|
-
} else if (puaRatio >= HIGH_PUA_THRESHOLD) {
|
|
1441
|
-
needsOcr = true;
|
|
1442
|
-
ocrReason = "high_pua";
|
|
1443
|
-
} else if (controlCharRatio >= HIGH_CONTROL_THRESHOLD) {
|
|
1444
|
-
needsOcr = true;
|
|
1445
|
-
ocrReason = "high_control";
|
|
1446
|
-
} else if (replacementCharRatio >= HIGH_REPLACEMENT_THRESHOLD) {
|
|
1447
|
-
needsOcr = true;
|
|
1448
|
-
ocrReason = "high_replacement";
|
|
1449
|
-
} else if (garbledHangul) {
|
|
1450
|
-
needsOcr = true;
|
|
1451
|
-
ocrReason = "garbled_hangul";
|
|
1452
|
-
}
|
|
1453
|
-
return {
|
|
1454
|
-
page,
|
|
1455
|
-
textChars: total,
|
|
1456
|
-
hangulRatio: hangul / denom,
|
|
1457
|
-
controlCharRatio,
|
|
1458
|
-
replacementCharRatio,
|
|
1459
|
-
puaRatio,
|
|
1460
|
-
hangulNoBatchimRatio,
|
|
1461
|
-
hangulRareBatchimRatio,
|
|
1462
|
-
needsOcr,
|
|
1463
|
-
ocrReason
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
var LOW_TEXT_THRESHOLD = 20;
|
|
1467
|
-
var HIGH_PUA_THRESHOLD = 0.2;
|
|
1468
|
-
var HIGH_CONTROL_THRESHOLD = 0.05;
|
|
1469
|
-
var HIGH_REPLACEMENT_THRESHOLD = 0.05;
|
|
1470
|
-
var DOC_NEEDS_OCR_PAGE_RATIO = 0.3;
|
|
1471
|
-
function stripControlChars(text) {
|
|
1472
|
-
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
1473
|
-
}
|
|
1474
|
-
function summarizeDocumentQuality(pages) {
|
|
1475
|
-
if (pages.length === 0) {
|
|
1476
|
-
return {
|
|
1477
|
-
totalPages: 0,
|
|
1478
|
-
totalTextChars: 0,
|
|
1479
|
-
avgHangulRatio: 0,
|
|
1480
|
-
avgControlCharRatio: 0,
|
|
1481
|
-
avgReplacementCharRatio: 0,
|
|
1482
|
-
avgPuaRatio: 0,
|
|
1483
|
-
lowTextPageCount: 0,
|
|
1484
|
-
highPuaPageCount: 0,
|
|
1485
|
-
needsOcr: false,
|
|
1486
|
-
ocrCandidatePages: []
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
|
-
let textChars = 0;
|
|
1490
|
-
let hangul = 0;
|
|
1491
|
-
let control = 0;
|
|
1492
|
-
let replacement = 0;
|
|
1493
|
-
let pua = 0;
|
|
1494
|
-
let lowText = 0;
|
|
1495
|
-
let highPua = 0;
|
|
1496
|
-
const ocrCandidatePages = [];
|
|
1497
|
-
for (const p of pages) {
|
|
1498
|
-
textChars += p.textChars;
|
|
1499
|
-
hangul += p.hangulRatio;
|
|
1500
|
-
control += p.controlCharRatio;
|
|
1501
|
-
replacement += p.replacementCharRatio;
|
|
1502
|
-
pua += p.puaRatio;
|
|
1503
|
-
if (p.textChars < LOW_TEXT_THRESHOLD) lowText++;
|
|
1504
|
-
if (p.puaRatio >= HIGH_PUA_THRESHOLD) highPua++;
|
|
1505
|
-
if (p.needsOcr) ocrCandidatePages.push(p.page);
|
|
1506
|
-
}
|
|
1507
|
-
const n = pages.length;
|
|
1508
|
-
return {
|
|
1509
|
-
totalPages: n,
|
|
1510
|
-
totalTextChars: textChars,
|
|
1511
|
-
avgHangulRatio: hangul / n,
|
|
1512
|
-
avgControlCharRatio: control / n,
|
|
1513
|
-
avgReplacementCharRatio: replacement / n,
|
|
1514
|
-
avgPuaRatio: pua / n,
|
|
1515
|
-
lowTextPageCount: lowText,
|
|
1516
|
-
highPuaPageCount: highPua,
|
|
1517
|
-
needsOcr: ocrCandidatePages.length / n >= DOC_NEEDS_OCR_PAGE_RATIO,
|
|
1518
|
-
ocrCandidatePages
|
|
1519
|
-
};
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1306
|
// src/pdf/text-line.ts
|
|
1523
1307
|
function filterHiddenText(items, pageWidth, pageHeight) {
|
|
1524
1308
|
let hiddenCount = 0;
|
|
@@ -1613,7 +1397,8 @@ function normalizeItems(rawItems) {
|
|
|
1613
1397
|
for (let j = deduped.length - 1; j >= 0; j--) {
|
|
1614
1398
|
const prev = deduped[j];
|
|
1615
1399
|
if (prev.y - sorted[i].y > 3) break;
|
|
1616
|
-
|
|
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) {
|
|
1617
1402
|
isDup = true;
|
|
1618
1403
|
break;
|
|
1619
1404
|
}
|
|
@@ -1675,7 +1460,12 @@ function mergeSuperscriptLines(lines) {
|
|
|
1675
1460
|
}
|
|
1676
1461
|
return { bottom, top, height: top - bottom };
|
|
1677
1462
|
};
|
|
1678
|
-
const isFrag = (line) =>
|
|
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
|
+
};
|
|
1679
1469
|
const result = [lines[0]];
|
|
1680
1470
|
for (let i = 1; i < lines.length; i++) {
|
|
1681
1471
|
const prev = result[result.length - 1];
|
|
@@ -1727,1339 +1517,1356 @@ function mergeLineSimple(items) {
|
|
|
1727
1517
|
return result;
|
|
1728
1518
|
}
|
|
1729
1519
|
|
|
1730
|
-
// src/pdf/
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
const rows = mergeOverlappingRows(groupByBaseline(merged));
|
|
1742
|
-
if (rows.length < MIN_ROWS) return [];
|
|
1743
|
-
const results = [];
|
|
1744
|
-
const headerResult = detectHeaderRow(rows);
|
|
1745
|
-
if (headerResult) {
|
|
1746
|
-
const { columns, headerIdx } = headerResult;
|
|
1747
|
-
const headerRow = rows[headerIdx];
|
|
1748
|
-
const headerItems = [...headerRow.items].sort((a, b) => a.x - b.x);
|
|
1749
|
-
const headerAndBelow = rows.slice(headerIdx);
|
|
1750
|
-
const mergedRows = mergeMultiLineRows(headerAndBelow, columns);
|
|
1751
|
-
const tableRegions = findTableRegionsByHeader(mergedRows, columns, headerItems);
|
|
1752
|
-
for (const region of tableRegions) {
|
|
1753
|
-
const table = buildClusterTable(region.rows, columns, pageNum);
|
|
1754
|
-
if (table) {
|
|
1755
|
-
expandUsedItems(table.usedItems, originMap);
|
|
1756
|
-
results.push(table);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
if (results.length === 0) {
|
|
1761
|
-
const suspiciousRows = rows.filter((row) => hasSuspiciousGaps(row));
|
|
1762
|
-
if (suspiciousRows.length >= MIN_ROWS) {
|
|
1763
|
-
const columns = extractColumnClusters(suspiciousRows);
|
|
1764
|
-
if (columns.length >= MIN_COLS) {
|
|
1765
|
-
const tableRegions = findTableRegions(rows, columns);
|
|
1766
|
-
for (const region of tableRegions) {
|
|
1767
|
-
const mergedRows = mergeMultiLineRows(region.rows, columns);
|
|
1768
|
-
const table = buildClusterTable(mergedRows, columns, pageNum);
|
|
1769
|
-
if (table) {
|
|
1770
|
-
expandUsedItems(table.usedItems, originMap);
|
|
1771
|
-
results.push(table);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
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;
|
|
1776
1531
|
}
|
|
1777
|
-
return
|
|
1532
|
+
return sorted[sorted.length - 1][0];
|
|
1778
1533
|
}
|
|
1779
|
-
function
|
|
1780
|
-
const
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
}
|
|
1795
|
-
function findTwoColumnProseCutX(items) {
|
|
1796
|
-
const lines = groupByBaseline(items);
|
|
1797
|
-
if (lines.length < 8) return null;
|
|
1798
|
-
let minX = Infinity;
|
|
1799
|
-
let maxX = -Infinity;
|
|
1800
|
-
for (const i of items) {
|
|
1801
|
-
if (i.x < minX) minX = i.x;
|
|
1802
|
-
if (i.x + i.w > maxX) maxX = i.x + i.w;
|
|
1803
|
-
}
|
|
1804
|
-
if (!Number.isFinite(maxX - minX)) return null;
|
|
1805
|
-
if (maxX - minX < 100) return null;
|
|
1806
|
-
const lo = minX + (maxX - minX) * 0.3;
|
|
1807
|
-
const hi = minX + (maxX - minX) * 0.7;
|
|
1808
|
-
const step = Math.max(2, (hi - lo) / 400);
|
|
1809
|
-
let cutX = 0;
|
|
1810
|
-
let bestCover = Infinity;
|
|
1811
|
-
for (let x = lo; x <= hi; x += step) {
|
|
1812
|
-
let cover = 0;
|
|
1813
|
-
for (const line of lines) {
|
|
1814
|
-
if (line.items.some((i) => i.x < x && i.x + i.w > x)) cover++;
|
|
1815
|
-
}
|
|
1816
|
-
if (cover < bestCover) {
|
|
1817
|
-
bestCover = cover;
|
|
1818
|
-
cutX = x;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
if (bestCover / lines.length > 0.15) return null;
|
|
1822
|
-
const left = [];
|
|
1823
|
-
const right = [];
|
|
1824
|
-
let twoSide = 0;
|
|
1825
|
-
for (const line of lines) {
|
|
1826
|
-
let hasL = false;
|
|
1827
|
-
let hasR = false;
|
|
1828
|
-
for (const i of line.items) {
|
|
1829
|
-
if (i.x + i.w <= cutX) {
|
|
1830
|
-
left.push(i);
|
|
1831
|
-
hasL = true;
|
|
1832
|
-
} else if (i.x >= cutX) {
|
|
1833
|
-
right.push(i);
|
|
1834
|
-
hasR = true;
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
if (hasL && hasR) twoSide++;
|
|
1838
|
-
}
|
|
1839
|
-
if (twoSide / lines.length < 0.55) return null;
|
|
1840
|
-
if (left.length < 5 || right.length < 5) return null;
|
|
1841
|
-
const allText = items.map((i) => i.text).join("").replace(/\s+/g, "");
|
|
1842
|
-
const digits = (allText.match(/[\d,.%△—-]/g) || []).length;
|
|
1843
|
-
if (allText.length === 0 || digits / allText.length > 0.15) return null;
|
|
1844
|
-
const stats = [left, right].map((side) => {
|
|
1845
|
-
let sMinX = Infinity;
|
|
1846
|
-
let sMaxR = -Infinity;
|
|
1847
|
-
let fsSum = 0;
|
|
1848
|
-
const rowEnds = /* @__PURE__ */ new Map();
|
|
1849
|
-
const rowLens = /* @__PURE__ */ new Map();
|
|
1850
|
-
const rowFirst = /* @__PURE__ */ new Map();
|
|
1851
|
-
for (const i of side) {
|
|
1852
|
-
if (i.x < sMinX) sMinX = i.x;
|
|
1853
|
-
const rgt = i.x + i.w;
|
|
1854
|
-
if (rgt > sMaxR) sMaxR = rgt;
|
|
1855
|
-
fsSum += i.fontSize;
|
|
1856
|
-
const key = Math.round(i.y / 4);
|
|
1857
|
-
rowEnds.set(key, Math.max(rowEnds.get(key) ?? -Infinity, rgt));
|
|
1858
|
-
rowLens.set(key, (rowLens.get(key) ?? 0) + i.text.replace(/\s+/g, "").length);
|
|
1859
|
-
const f = rowFirst.get(key);
|
|
1860
|
-
if (!f || i.x < f.x) rowFirst.set(key, i);
|
|
1861
|
-
}
|
|
1862
|
-
const ends = [...rowEnds.values()].sort((a, b) => a - b);
|
|
1863
|
-
const p85 = ends[Math.floor(ends.length * 0.85)] ?? sMaxR;
|
|
1864
|
-
const fs = fsSum / side.length;
|
|
1865
|
-
const justified = ends.filter((e) => Math.abs(e - p85) <= fs).length / ends.length;
|
|
1866
|
-
const lens = [...rowLens.values()];
|
|
1867
|
-
const avgLen = lens.reduce((s, v) => s + v, 0) / (lens.length || 1);
|
|
1868
|
-
const markers = [...rowFirst.values()].filter((i) => /^[•▪◦‣∙·\-–—□■◇◆▶※*]/.test(i.text)).length;
|
|
1869
|
-
return { width: sMaxR - sMinX, justified, avgLen, markerRatio: markers / (rowFirst.size || 1) };
|
|
1870
|
-
});
|
|
1871
|
-
if (stats.some((s) => s.avgLen < 12)) return null;
|
|
1872
|
-
if (stats.some((s) => s.markerRatio > 0.1)) return null;
|
|
1873
|
-
const widthSym = Math.min(stats[0].width, stats[1].width) / Math.max(stats[0].width, stats[1].width);
|
|
1874
|
-
if (widthSym < 0.6) return null;
|
|
1875
|
-
if (Math.min(stats[0].justified, stats[1].justified) < 0.55) return null;
|
|
1876
|
-
return cutX;
|
|
1877
|
-
}
|
|
1878
|
-
function mergeEvenSpacedClusters(items) {
|
|
1879
|
-
const originMap = /* @__PURE__ */ new Map();
|
|
1880
|
-
const rows = groupByBaseline(items);
|
|
1881
|
-
const merged = [];
|
|
1882
|
-
for (const row of rows) {
|
|
1883
|
-
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
1884
|
-
let i = 0;
|
|
1885
|
-
while (i < sorted.length) {
|
|
1886
|
-
if (/^[가-힣\d]$/.test(sorted[i].text)) {
|
|
1887
|
-
let runEnd = i + 1;
|
|
1888
|
-
while (runEnd < sorted.length && /^[가-힣\d]$/.test(sorted[runEnd].text)) {
|
|
1889
|
-
if (sorted[runEnd].hasSpaceBefore) break;
|
|
1890
|
-
const gap = sorted[runEnd].x - (sorted[runEnd - 1].x + sorted[runEnd - 1].w);
|
|
1891
|
-
const fs = sorted[runEnd].fontSize;
|
|
1892
|
-
if (gap < fs * 0.1 || gap > fs * 3) break;
|
|
1893
|
-
runEnd++;
|
|
1894
|
-
}
|
|
1895
|
-
if (runEnd - i >= 3) {
|
|
1896
|
-
const gaps = [];
|
|
1897
|
-
for (let g2 = i + 1; g2 < runEnd; g2++) {
|
|
1898
|
-
gaps.push(sorted[g2].x - (sorted[g2 - 1].x + sorted[g2 - 1].w));
|
|
1899
|
-
}
|
|
1900
|
-
let minG = Infinity, maxG = -Infinity;
|
|
1901
|
-
for (const g2 of gaps) {
|
|
1902
|
-
if (g2 < minG) minG = g2;
|
|
1903
|
-
if (g2 > maxG) maxG = g2;
|
|
1904
|
-
}
|
|
1905
|
-
if (minG > 0 && maxG / minG <= 3) {
|
|
1906
|
-
const run = sorted.slice(i, runEnd);
|
|
1907
|
-
const text = run.map((r) => r.text).join("");
|
|
1908
|
-
const first = run[0], last = run[runEnd - i - 1];
|
|
1909
|
-
const item = {
|
|
1910
|
-
text,
|
|
1911
|
-
x: first.x,
|
|
1912
|
-
y: first.y,
|
|
1913
|
-
w: last.x + last.w - first.x,
|
|
1914
|
-
h: first.h,
|
|
1915
|
-
fontSize: first.fontSize,
|
|
1916
|
-
fontName: first.fontName
|
|
1917
|
-
};
|
|
1918
|
-
originMap.set(item, run);
|
|
1919
|
-
merged.push(item);
|
|
1920
|
-
i = runEnd;
|
|
1921
|
-
continue;
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
1925
|
-
merged.push(sorted[i]);
|
|
1926
|
-
i++;
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
return { merged, originMap };
|
|
1930
|
-
}
|
|
1931
|
-
function expandUsedItems(usedItems, originMap) {
|
|
1932
|
-
const toAdd = [];
|
|
1933
|
-
for (const item of usedItems) {
|
|
1934
|
-
const origins = originMap.get(item);
|
|
1935
|
-
if (origins) for (const o of origins) toAdd.push(o);
|
|
1936
|
-
}
|
|
1937
|
-
for (const a of toAdd) usedItems.add(a);
|
|
1938
|
-
}
|
|
1939
|
-
function detectHeaderRow(rows) {
|
|
1940
|
-
const allItems = rows.flatMap((r) => r.items);
|
|
1941
|
-
if (allItems.length === 0) return null;
|
|
1942
|
-
let allMinX = Infinity, allMaxX = -Infinity;
|
|
1943
|
-
for (const i of allItems) {
|
|
1944
|
-
if (i.x < allMinX) allMinX = i.x;
|
|
1945
|
-
const r = i.x + i.w;
|
|
1946
|
-
if (r > allMaxX) allMaxX = r;
|
|
1947
|
-
}
|
|
1948
|
-
const pageSpan = allMaxX - allMinX;
|
|
1949
|
-
if (pageSpan <= 0) return null;
|
|
1950
|
-
for (let ri = 0; ri < rows.length; ri++) {
|
|
1951
|
-
const row = rows[ri];
|
|
1952
|
-
if (row.items.length < MIN_COLS || row.items.length > 6) continue;
|
|
1953
|
-
if (row.items.some((i) => i.text.length > 8)) continue;
|
|
1954
|
-
if (!row.items.some((i) => /[가-힣]/.test(i.text))) continue;
|
|
1955
|
-
if (row.items.some((i) => /^[□■○●·※▶▷◆◇\-]/.test(i.text))) continue;
|
|
1956
|
-
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
1957
|
-
const xSpan = sorted[sorted.length - 1].x + sorted[sorted.length - 1].w - sorted[0].x;
|
|
1958
|
-
if (xSpan / pageSpan < 0.4) continue;
|
|
1959
|
-
const avgFs = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
|
|
1960
|
-
let hasLargeGap = false;
|
|
1961
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
1962
|
-
const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
|
|
1963
|
-
if (gap >= avgFs * 2.5) {
|
|
1964
|
-
hasLargeGap = true;
|
|
1965
|
-
break;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
if (!hasLargeGap) continue;
|
|
1969
|
-
const columns = sorted.map((item) => ({ x: item.x, count: 0 }));
|
|
1970
|
-
let matchCount = 0;
|
|
1971
|
-
for (let j = ri + 1; j < rows.length && matchCount < MIN_ROWS + 2; j++) {
|
|
1972
|
-
const matched = countMatchedColumnsRange(rows[j], columns, sorted);
|
|
1973
|
-
if (matched >= MIN_COLS) matchCount++;
|
|
1974
|
-
}
|
|
1975
|
-
if (matchCount < MIN_ROWS) continue;
|
|
1976
|
-
return { columns, headerIdx: ri };
|
|
1977
|
-
}
|
|
1978
|
-
return null;
|
|
1979
|
-
}
|
|
1980
|
-
function mergeOverlappingRows(rows) {
|
|
1981
|
-
if (rows.length <= 1) return rows;
|
|
1982
|
-
const result = [rows[0]];
|
|
1983
|
-
for (let i = 1; i < rows.length; i++) {
|
|
1984
|
-
const prev = result[result.length - 1];
|
|
1985
|
-
const curr = rows[i];
|
|
1986
|
-
const a = rowBand(prev);
|
|
1987
|
-
const b = rowBand(curr);
|
|
1988
|
-
const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
|
|
1989
|
-
const prevIsFrag = isFragmentRow(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
|
|
1990
|
-
const currIsFrag = isFragmentRow(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
|
|
1991
|
-
if (prevIsFrag || currIsFrag) {
|
|
1992
|
-
const baseY = prevIsFrag ? curr.y : prev.y;
|
|
1993
|
-
result[result.length - 1] = { y: baseY, items: [...prev.items, ...curr.items] };
|
|
1994
|
-
} else {
|
|
1995
|
-
result.push(curr);
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
return result;
|
|
1999
|
-
}
|
|
2000
|
-
function isFragmentRow(row) {
|
|
2001
|
-
return row.items.length <= 3 && row.items.every((i) => i.text.length <= 8);
|
|
2002
|
-
}
|
|
2003
|
-
function rowBand(row) {
|
|
2004
|
-
let bottom = Infinity, top = -Infinity;
|
|
2005
|
-
for (const i of row.items) {
|
|
2006
|
-
const h = i.h > 0 ? i.h : i.fontSize;
|
|
2007
|
-
if (i.y < bottom) bottom = i.y;
|
|
2008
|
-
if (i.y + h > top) top = i.y + h;
|
|
2009
|
-
}
|
|
2010
|
-
return { bottom, top, height: top - bottom };
|
|
2011
|
-
}
|
|
2012
|
-
function mergeMultiLineRows(rows, columns) {
|
|
2013
|
-
if (rows.length <= 1) return rows;
|
|
2014
|
-
const result = [rows[0]];
|
|
2015
|
-
const allFontSizes = rows.flatMap((r) => r.items).map((i) => i.fontSize);
|
|
2016
|
-
const avgFontSize = allFontSizes.length > 0 ? allFontSizes.reduce((s, v) => s + v, 0) / allFontSizes.length : 12;
|
|
2017
|
-
for (let i = 1; i < rows.length; i++) {
|
|
2018
|
-
const prev = result[result.length - 1];
|
|
2019
|
-
const curr = rows[i];
|
|
2020
|
-
const yGap = Math.abs(prev.y - curr.y);
|
|
2021
|
-
const matchedCols = countMatchedColumns(curr, columns);
|
|
2022
|
-
if (yGap < avgFontSize * 1.8 && curr.items.length <= 2 && (matchedCols < MIN_COLS || curr.items.length === 1)) {
|
|
2023
|
-
result[result.length - 1] = {
|
|
2024
|
-
y: prev.y,
|
|
2025
|
-
items: [...prev.items, ...curr.items]
|
|
2026
|
-
};
|
|
2027
|
-
} else {
|
|
2028
|
-
result.push(curr);
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
return result;
|
|
2032
|
-
}
|
|
2033
|
-
function groupByBaseline(items) {
|
|
2034
|
-
if (items.length === 0) return [];
|
|
2035
|
-
const sorted = [...items].sort((a, b) => b.y - a.y || a.x - b.x);
|
|
2036
|
-
const rows = [];
|
|
2037
|
-
let curItems = [sorted[0]];
|
|
2038
|
-
let curY = sorted[0].y;
|
|
2039
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
2040
|
-
if (Math.abs(sorted[i].y - curY) <= Y_TOL) {
|
|
2041
|
-
curItems.push(sorted[i]);
|
|
2042
|
-
} else {
|
|
2043
|
-
rows.push({ y: curY, items: curItems });
|
|
2044
|
-
curItems = [sorted[i]];
|
|
2045
|
-
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);
|
|
2046
1549
|
}
|
|
2047
1550
|
}
|
|
2048
|
-
if (curItems.length > 0) rows.push({ y: curY, items: curItems });
|
|
2049
|
-
return rows;
|
|
2050
1551
|
}
|
|
2051
|
-
function
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
if (
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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;
|
|
2060
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;
|
|
2061
1572
|
return false;
|
|
2062
1573
|
}
|
|
2063
|
-
function
|
|
2064
|
-
const
|
|
2065
|
-
for (
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
for (let i = 1; i <= allX.length; i++) {
|
|
2073
|
-
if (i === allX.length || allX[i] - allX[i - 1] > COL_CLUSTER_TOL) {
|
|
2074
|
-
const slice = allX.slice(clusterStart, i);
|
|
2075
|
-
const avg = Math.round(slice.reduce((s, v) => s + v, 0) / slice.length);
|
|
2076
|
-
clusters.push({ x: avg, count: slice.length });
|
|
2077
|
-
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(" "));
|
|
2078
1583
|
}
|
|
2079
1584
|
}
|
|
2080
|
-
|
|
2081
|
-
return clusters.filter((c) => c.count >= minCount).sort((a, b) => a.x - b.x);
|
|
1585
|
+
return lines.join("\n");
|
|
2082
1586
|
}
|
|
2083
|
-
function
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
if (currentRegion.length >= MIN_ROWS) {
|
|
2102
|
-
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;
|
|
2103
1605
|
}
|
|
2104
|
-
currentRegion = [];
|
|
2105
|
-
missStreak = 0;
|
|
2106
1606
|
}
|
|
2107
1607
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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
|
+
}
|
|
2112
1642
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
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 };
|
|
2115
1652
|
}
|
|
2116
|
-
|
|
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;
|
|
2117
1663
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
for (
|
|
2122
|
-
const
|
|
2123
|
-
if (
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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];
|
|
2128
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;
|
|
2129
1716
|
} else {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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;
|
|
2134
1731
|
}
|
|
1732
|
+
prevListIdx = i;
|
|
2135
1733
|
}
|
|
2136
|
-
if (
|
|
2137
|
-
|
|
1734
|
+
if (toRemove.size > 0) {
|
|
1735
|
+
const sorted = [...toRemove].sort((a, b) => b - a);
|
|
1736
|
+
for (const idx of sorted) blocks.splice(idx, 1);
|
|
2138
1737
|
}
|
|
2139
|
-
return regions;
|
|
2140
1738
|
}
|
|
2141
|
-
function
|
|
2142
|
-
const
|
|
2143
|
-
for (
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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;
|
|
2148
1748
|
}
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
}
|
|
2153
|
-
function countMatchedColumnsRange(row, columns, headerItems) {
|
|
2154
|
-
const boundaries = [];
|
|
2155
|
-
for (let ci = 0; ci < headerItems.length; ci++) {
|
|
2156
|
-
const left = ci === 0 ? 0 : (headerItems[ci - 1].x + headerItems[ci - 1].w + headerItems[ci].x) / 2;
|
|
2157
|
-
const right = ci === headerItems.length - 1 ? Infinity : (headerItems[ci].x + headerItems[ci].w + headerItems[ci + 1].x) / 2;
|
|
2158
|
-
boundaries.push({ left, right });
|
|
2159
|
-
}
|
|
2160
|
-
const matched = /* @__PURE__ */ new Set();
|
|
2161
|
-
for (const item of row.items) {
|
|
2162
|
-
for (let ci = 0; ci < boundaries.length; ci++) {
|
|
2163
|
-
if (item.x >= boundaries[ci].left && item.x < boundaries[ci].right) {
|
|
2164
|
-
matched.add(ci);
|
|
2165
|
-
break;
|
|
1749
|
+
if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
|
|
1750
|
+
result.push({ ...block, type: "list", listType: "unordered", text: block.text });
|
|
1751
|
+
continue;
|
|
2166
1752
|
}
|
|
2167
1753
|
}
|
|
1754
|
+
result.push(block);
|
|
2168
1755
|
}
|
|
2169
|
-
return
|
|
1756
|
+
return result;
|
|
2170
1757
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
const colCenters = columns.map((c) => c.x);
|
|
2175
|
-
const gaps = [];
|
|
2176
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
2177
|
-
gaps.push({ idx: i, size: sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w) });
|
|
2178
|
-
}
|
|
2179
|
-
const gapSizes = gaps.map((g2) => g2.size).sort((a, b) => a - b);
|
|
2180
|
-
const medianGap = gapSizes.length > 0 ? gapSizes[Math.floor(gapSizes.length / 2)] : 0;
|
|
2181
|
-
const gapThreshold = sorted.length <= numCols + 1 ? 12 : Math.max(medianGap * 2.5, 12);
|
|
2182
|
-
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);
|
|
2183
|
-
const groups = [];
|
|
2184
|
-
let start = 0;
|
|
2185
|
-
for (const gap of significantGaps) {
|
|
2186
|
-
groups.push(sorted.slice(start, gap.idx));
|
|
2187
|
-
start = gap.idx;
|
|
2188
|
-
}
|
|
2189
|
-
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) {
|
|
2190
1761
|
const result = [];
|
|
2191
|
-
|
|
2192
|
-
const
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
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;
|
|
2198
1768
|
}
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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;
|
|
2205
1801
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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
|
+
}
|
|
2223
1822
|
}
|
|
1823
|
+
continue;
|
|
2224
1824
|
}
|
|
2225
|
-
|
|
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
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
flushKvTable();
|
|
1840
|
+
result.push(block);
|
|
2226
1841
|
}
|
|
1842
|
+
flushKvTable();
|
|
2227
1843
|
return result;
|
|
2228
1844
|
}
|
|
2229
|
-
function
|
|
2230
|
-
const
|
|
2231
|
-
const
|
|
2232
|
-
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
const
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
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);
|
|
2244
1872
|
}
|
|
2245
|
-
const
|
|
2246
|
-
for (const
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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);
|
|
2251
1898
|
}
|
|
2252
1899
|
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
if (row.every((c) => c.text === "")) emptyRows++;
|
|
2256
|
-
}
|
|
2257
|
-
if (emptyRows > numRows * 0.5) return null;
|
|
2258
|
-
for (let c = 0; c < numCols; c++) {
|
|
2259
|
-
const hasValue = cells.some((row) => row[c].text !== "");
|
|
2260
|
-
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" });
|
|
2261
1902
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
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);
|
|
2277
1933
|
}
|
|
2278
1934
|
}
|
|
2279
1935
|
}
|
|
2280
|
-
|
|
2281
|
-
const
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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
|
+
}
|
|
2291
1949
|
}
|
|
2292
|
-
for (let c = 0; c < numCols; c++) next[c].text = "";
|
|
2293
1950
|
}
|
|
2294
1951
|
}
|
|
2295
1952
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
for (const
|
|
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) {
|
|
2308
1977
|
if (i.x < minX) minX = i.x;
|
|
2309
|
-
if (i.y < minY) minY = i.y;
|
|
2310
1978
|
if (i.x + i.w > maxX) maxX = i.x + i.w;
|
|
2311
|
-
const h = i.h > 0 ? i.h : i.fontSize;
|
|
2312
|
-
if (i.y + h > maxY) maxY = i.y + h;
|
|
2313
1979
|
}
|
|
2314
|
-
return
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
if (
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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;
|
|
2339
2011
|
}
|
|
2340
2012
|
}
|
|
2013
|
+
if (hasL && hasR) twoSide++;
|
|
2341
2014
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
const
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
const
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
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);
|
|
2357
2037
|
}
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
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++;
|
|
2363
2103
|
}
|
|
2364
2104
|
}
|
|
2365
|
-
return
|
|
2105
|
+
return { merged, originMap };
|
|
2366
2106
|
}
|
|
2367
|
-
function
|
|
2368
|
-
const
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
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);
|
|
2373
2112
|
}
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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;
|
|
2388
2142
|
}
|
|
2389
2143
|
}
|
|
2390
|
-
if (
|
|
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 };
|
|
2391
2153
|
}
|
|
2392
|
-
return
|
|
2154
|
+
return null;
|
|
2393
2155
|
}
|
|
2394
|
-
function
|
|
2395
|
-
if (
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
result.push(
|
|
2410
|
-
continue;
|
|
2411
|
-
}
|
|
2412
|
-
if (gi >= groups.length) {
|
|
2413
|
-
result.push([sortedCross[ci++]]);
|
|
2414
|
-
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);
|
|
2415
2172
|
}
|
|
2416
|
-
const crossTop = sortedCross[ci].y + sortedCross[ci].h;
|
|
2417
|
-
if (crossTop >= groupTop(groups[gi])) result.push([sortedCross[ci++]]);
|
|
2418
|
-
else result.push(groups[gi++]);
|
|
2419
2173
|
}
|
|
2420
2174
|
return result;
|
|
2421
2175
|
}
|
|
2422
|
-
function
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
let
|
|
2427
|
-
for (
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
if (gap > largestGap) {
|
|
2432
|
-
largestGap = gap;
|
|
2433
|
-
position = (prevBottom + currTop) / 2;
|
|
2434
|
-
}
|
|
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;
|
|
2435
2185
|
}
|
|
2436
|
-
return {
|
|
2186
|
+
return { bottom, top, height: top - bottom };
|
|
2437
2187
|
}
|
|
2438
|
-
function
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
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);
|
|
2455
2205
|
}
|
|
2456
2206
|
}
|
|
2457
|
-
return
|
|
2207
|
+
return result;
|
|
2458
2208
|
}
|
|
2459
|
-
function
|
|
2460
|
-
if (items.length
|
|
2461
|
-
const sorted = [...items].sort((a, b) =>
|
|
2462
|
-
|
|
2463
|
-
let
|
|
2464
|
-
let
|
|
2465
|
-
for (
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
position = (prevRight + left) / 2;
|
|
2473
|
-
}
|
|
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;
|
|
2474
2222
|
}
|
|
2475
|
-
prevRight = prevRight === null ? right : Math.max(prevRight, right);
|
|
2476
2223
|
}
|
|
2477
|
-
|
|
2224
|
+
if (curItems.length > 0) rows.push({ y: curY, items: curItems });
|
|
2225
|
+
return rows;
|
|
2478
2226
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
if (
|
|
2483
|
-
const
|
|
2484
|
-
const
|
|
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);
|
|
2485
2233
|
for (let i = 1; i < sorted.length; i++) {
|
|
2486
|
-
|
|
2234
|
+
const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
|
|
2235
|
+
if (gap >= minGap) return true;
|
|
2487
2236
|
}
|
|
2488
|
-
|
|
2489
|
-
const avgLen = items.reduce((s, i) => s + i.text.length, 0) / items.length;
|
|
2490
|
-
return maxGap < 40 && avgLen < 5;
|
|
2237
|
+
return false;
|
|
2491
2238
|
}
|
|
2492
|
-
function
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
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;
|
|
2502
2254
|
}
|
|
2503
2255
|
}
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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();
|
|
2276
|
+
}
|
|
2277
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
2278
|
+
regions.push({ rows: [...currentRegion] });
|
|
2279
|
+
}
|
|
2280
|
+
currentRegion = [];
|
|
2281
|
+
missStreak = 0;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
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);
|
|
2519
2304
|
}
|
|
2520
|
-
|
|
2521
|
-
|
|
2305
|
+
} else {
|
|
2306
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
2307
|
+
regions.push({ rows: [...currentRegion] });
|
|
2522
2308
|
}
|
|
2309
|
+
currentRegion = [];
|
|
2523
2310
|
}
|
|
2524
2311
|
}
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2312
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
2313
|
+
regions.push({ rows: currentRegion });
|
|
2314
|
+
}
|
|
2315
|
+
return regions;
|
|
2316
|
+
}
|
|
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;
|
|
2534
2324
|
}
|
|
2535
|
-
prev.count += peaks[i].count;
|
|
2536
|
-
prev.minX = Math.min(prev.minX, peaks[i].minX);
|
|
2537
|
-
} else {
|
|
2538
|
-
merged.push({ ...peaks[i] });
|
|
2539
2325
|
}
|
|
2540
2326
|
}
|
|
2541
|
-
|
|
2542
|
-
if (rawColumns.length < 3) return null;
|
|
2543
|
-
const MIN_DETECT_COL_WIDTH = 30;
|
|
2544
|
-
const columns = [rawColumns[0]];
|
|
2545
|
-
for (let i = 1; i < rawColumns.length; i++) {
|
|
2546
|
-
if (rawColumns[i] - columns[columns.length - 1] < MIN_DETECT_COL_WIDTH) continue;
|
|
2547
|
-
columns.push(rawColumns[i]);
|
|
2548
|
-
}
|
|
2549
|
-
return columns.length >= 3 ? columns : null;
|
|
2327
|
+
return matched.size;
|
|
2550
2328
|
}
|
|
2551
|
-
function
|
|
2552
|
-
|
|
2553
|
-
|
|
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 });
|
|
2554
2335
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
for (let i = 0; i < yLines.length; i++) {
|
|
2563
|
-
if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
|
|
2564
|
-
bigoIdx = i;
|
|
2565
|
-
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
|
+
}
|
|
2566
2343
|
}
|
|
2567
2344
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
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) });
|
|
2575
2354
|
}
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
|
|
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;
|
|
2579
2364
|
}
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
);
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
} else {
|
|
2590
|
-
if (gridLines.length > 0) {
|
|
2591
|
-
result.push(buildGridTable(gridLines.splice(0), columns));
|
|
2592
|
-
}
|
|
2593
|
-
result.push(mergeLineSimple(line));
|
|
2594
|
-
}
|
|
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;
|
|
2595
2374
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
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]) });
|
|
2598
2381
|
}
|
|
2599
2382
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
+
}
|
|
2604
2400
|
}
|
|
2401
|
+
result.push({ col: bestCol, items: groups[gi] });
|
|
2605
2402
|
}
|
|
2606
|
-
return result
|
|
2403
|
+
return result;
|
|
2607
2404
|
}
|
|
2608
|
-
function
|
|
2405
|
+
function buildClusterTable(rows, columns, pageNum) {
|
|
2609
2406
|
const numCols = columns.length;
|
|
2610
|
-
const
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
if (merged.length === 0) {
|
|
2623
|
-
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]);
|
|
2624
2419
|
continue;
|
|
2625
2420
|
}
|
|
2626
|
-
const
|
|
2627
|
-
const
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
}
|
|
2633
|
-
if (!isNewRow && numCols > 1 && row[1]) {
|
|
2634
|
-
isNewRow = true;
|
|
2635
|
-
}
|
|
2636
|
-
if (!isNewRow) {
|
|
2637
|
-
const hasData = row.slice(dataColStart).some((c) => c !== "");
|
|
2638
|
-
const prevHasData = prev.slice(dataColStart).some((c) => c !== "");
|
|
2639
|
-
if (hasData && prevHasData) {
|
|
2640
|
-
isNewRow = true;
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
if (isNewRow && filledCount === 1 && row[0] && row[0].length <= 2) {
|
|
2644
|
-
isNewRow = false;
|
|
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);
|
|
2645
2427
|
}
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2428
|
+
}
|
|
2429
|
+
let emptyRows = 0;
|
|
2430
|
+
for (const row of cells) {
|
|
2431
|
+
if (row.every((c) => c.text === "")) emptyRows++;
|
|
2432
|
+
}
|
|
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;
|
|
2437
|
+
}
|
|
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;
|
|
2652
2450
|
}
|
|
2451
|
+
for (let c = 0; c < numCols; c++) cells[r][c].text = "";
|
|
2452
|
+
break;
|
|
2653
2453
|
}
|
|
2654
2454
|
}
|
|
2655
2455
|
}
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
for (let r = 0; r < headerEnd; r++) {
|
|
2668
|
-
for (let c = 0; c < numCols; c++) {
|
|
2669
|
-
if (merged[r][c]) {
|
|
2670
|
-
headerRow[c] = headerRow[c] ? headerRow[c] + " " + merged[r][c] : merged[r][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;
|
|
2671
2467
|
}
|
|
2468
|
+
for (let c = 0; c < numCols; c++) next[c].text = "";
|
|
2672
2469
|
}
|
|
2673
2470
|
}
|
|
2674
|
-
merged.splice(0, headerEnd, headerRow);
|
|
2675
|
-
}
|
|
2676
|
-
for (const row of merged) {
|
|
2677
|
-
for (let c = 0; c < row.length; c++) {
|
|
2678
|
-
if (row[c]) row[c] = collapseEvenSpacing(row[c]);
|
|
2679
|
-
}
|
|
2680
2471
|
}
|
|
2681
|
-
const
|
|
2682
|
-
const
|
|
2683
|
-
if (
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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;
|
|
2691
2489
|
}
|
|
2692
|
-
return
|
|
2490
|
+
return {
|
|
2491
|
+
table: irTable,
|
|
2492
|
+
bbox: { page: pageNum, x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
2493
|
+
usedItems
|
|
2494
|
+
};
|
|
2693
2495
|
}
|
|
2694
2496
|
|
|
2695
|
-
// src/pdf/
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|
+
}
|
|
2706
2517
|
}
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
if (
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
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)];
|
|
2724
2539
|
}
|
|
2725
2540
|
}
|
|
2541
|
+
return [items];
|
|
2726
2542
|
}
|
|
2727
|
-
function
|
|
2728
|
-
const
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
}) && table.cells.slice(1).some((row) => row.some((c) => c.text.trim() !== ""))) return false;
|
|
2734
|
-
if (table.rows <= 3 && table.cols <= 3 && allText.length <= 200) {
|
|
2735
|
-
const totalCells2 = table.rows * table.cols;
|
|
2736
|
-
const emptyCells2 = totalCells2 - allCells.length;
|
|
2737
|
-
if (emptyCells2 >= totalCells2 * 0.3) return true;
|
|
2738
|
-
if (/[□■◆○●▶ㅇ]/.test(allText)) return true;
|
|
2739
|
-
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;
|
|
2740
2549
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
} else {
|
|
2757
|
-
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
|
+
}
|
|
2758
2565
|
}
|
|
2566
|
+
if (overlaps >= CROSS_MIN_OVERLAPS) cross.add(item);
|
|
2759
2567
|
}
|
|
2760
|
-
return
|
|
2568
|
+
return cross;
|
|
2761
2569
|
}
|
|
2762
|
-
function
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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++]);
|
|
2770
2586
|
continue;
|
|
2771
2587
|
}
|
|
2772
|
-
if (
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
const prevIsStructural = !prev || prev.type === "table" || prev.type === "heading" || prev.type === "separator";
|
|
2776
|
-
const nextIsStructural = !next || next.type === "table" || next.type === "heading" || next.type === "paragraph" && next.text && /^[□■◆○●]/.test(next.text.trim());
|
|
2777
|
-
if (prevIsStructural || nextIsStructural) {
|
|
2778
|
-
block.type = "heading";
|
|
2779
|
-
block.level = 3;
|
|
2780
|
-
}
|
|
2588
|
+
if (gi >= groups.length) {
|
|
2589
|
+
result.push([sortedCross[ci++]]);
|
|
2590
|
+
continue;
|
|
2781
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++]);
|
|
2782
2595
|
}
|
|
2596
|
+
return result;
|
|
2783
2597
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
const
|
|
2792
|
-
|
|
2793
|
-
if (
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const capBottom = b.bbox.y;
|
|
2797
|
-
const tblTop = table.bbox.y + table.bbox.height;
|
|
2798
|
-
const tblBottom = table.bbox.y;
|
|
2799
|
-
const gap = capBottom >= tblTop ? capBottom - tblTop : tblBottom - capTop;
|
|
2800
|
-
if (gap > CAPTION_MAX_GAP) return false;
|
|
2801
|
-
const overlap = Math.min(b.bbox.x + b.bbox.width, table.bbox.x + table.bbox.width) - Math.max(b.bbox.x, table.bbox.x);
|
|
2802
|
-
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;
|
|
2803
2610
|
}
|
|
2804
|
-
|
|
2805
|
-
};
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
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
|
+
}
|
|
2816
2631
|
}
|
|
2817
2632
|
}
|
|
2633
|
+
return edgeCut;
|
|
2818
2634
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
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);
|
|
2834
2652
|
}
|
|
2835
|
-
|
|
2836
|
-
if (m) return { family: "circled", ord: m[1].charCodeAt(0) - 9312 + 1 };
|
|
2837
|
-
return null;
|
|
2653
|
+
return { position, gap: largestGap };
|
|
2838
2654
|
}
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
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));
|
|
2847
2663
|
}
|
|
2848
|
-
const
|
|
2849
|
-
const
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
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
|
+
}
|
|
2854
2679
|
}
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
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 });
|
|
2864
2698
|
}
|
|
2865
2699
|
}
|
|
2866
|
-
if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
|
|
2867
2700
|
}
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
const
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
if (b.type === "paragraph" && ATTACHMENT_RE.test(text)) {
|
|
2881
|
-
blocks[i] = { ...b, type: "list", listType: "unordered" };
|
|
2882
|
-
continue;
|
|
2883
|
-
}
|
|
2884
|
-
if (!validated.has(i)) continue;
|
|
2885
|
-
const label = parseListLabel(text);
|
|
2886
|
-
let depth = familyStack.indexOf(label.family);
|
|
2887
|
-
if (depth < 0) {
|
|
2888
|
-
familyStack.push(label.family);
|
|
2889
|
-
depth = familyStack.length - 1;
|
|
2890
|
-
} else {
|
|
2891
|
-
familyStack = familyStack.slice(0, depth + 1);
|
|
2892
|
-
}
|
|
2893
|
-
const listType = label.family === "arabicDot" ? "ordered" : "unordered";
|
|
2894
|
-
const listBlock = { ...b, type: "list", listType };
|
|
2895
|
-
if (depth === 0) {
|
|
2896
|
-
blocks[i] = listBlock;
|
|
2897
|
-
lastTopLevelList = listBlock;
|
|
2898
|
-
} else if (lastTopLevelList) {
|
|
2899
|
-
if (!lastTopLevelList.children) lastTopLevelList.children = [];
|
|
2900
|
-
lastTopLevelList.children.push(listBlock);
|
|
2901
|
-
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);
|
|
2902
2713
|
} else {
|
|
2903
|
-
|
|
2904
|
-
lastTopLevelList = listBlock;
|
|
2714
|
+
merged.push({ ...peaks[i] });
|
|
2905
2715
|
}
|
|
2906
2716
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
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]);
|
|
2910
2724
|
}
|
|
2725
|
+
return columns.length >= 3 ? columns : null;
|
|
2911
2726
|
}
|
|
2912
|
-
function
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
const block = blocks[i];
|
|
2916
|
-
if (block.type === "paragraph" && block.text) {
|
|
2917
|
-
const text = block.text.trim();
|
|
2918
|
-
if (/^\d+\.\s/.test(text)) {
|
|
2919
|
-
result.push({ ...block, type: "list", listType: "ordered", text: block.text });
|
|
2920
|
-
continue;
|
|
2921
|
-
}
|
|
2922
|
-
if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
|
|
2923
|
-
result.push({ ...block, type: "list", listType: "unordered", text: block.text });
|
|
2924
|
-
continue;
|
|
2925
|
-
}
|
|
2926
|
-
}
|
|
2927
|
-
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;
|
|
2928
2730
|
}
|
|
2929
|
-
return
|
|
2731
|
+
return 0;
|
|
2930
2732
|
}
|
|
2931
|
-
|
|
2932
|
-
var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
|
|
2933
|
-
function detectSpecialKoreanTables(blocks) {
|
|
2733
|
+
function extractWithColumns(yLines, columns) {
|
|
2934
2734
|
const result = [];
|
|
2935
|
-
|
|
2936
|
-
const
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
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;
|
|
2941
2742
|
}
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
return [
|
|
2950
|
-
{ text: kv.key, colSpan: 2, rowSpan: 1 },
|
|
2951
|
-
{ text: "", colSpan: 1, rowSpan: 1 }
|
|
2952
|
-
];
|
|
2953
|
-
});
|
|
2954
|
-
const irTable = {
|
|
2955
|
-
rows: cells.length,
|
|
2956
|
-
cols: 2,
|
|
2957
|
-
cells,
|
|
2958
|
-
hasHeader: true
|
|
2959
|
-
};
|
|
2960
|
-
const firstBlock = kvLines[0].block;
|
|
2961
|
-
result.push({
|
|
2962
|
-
type: "table",
|
|
2963
|
-
table: irTable,
|
|
2964
|
-
pageNumber: firstBlock.pageNumber,
|
|
2965
|
-
bbox: firstBlock.bbox
|
|
2966
|
-
});
|
|
2967
|
-
kvLines = [];
|
|
2968
|
-
};
|
|
2969
|
-
for (const block of blocks) {
|
|
2970
|
-
if (block.type !== "paragraph" || !block.text) {
|
|
2971
|
-
flushKvTable();
|
|
2972
|
-
result.push(block);
|
|
2973
|
-
continue;
|
|
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;
|
|
2974
2750
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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);
|
|
2984
2765
|
} else {
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
kvLines.push({
|
|
2988
|
-
key: text.slice(0, spaceIdx).trim(),
|
|
2989
|
-
value: text.slice(spaceIdx + 1).trim(),
|
|
2990
|
-
block
|
|
2991
|
-
});
|
|
2992
|
-
} else {
|
|
2993
|
-
kvLines.push({ key: text, value: "", block });
|
|
2766
|
+
if (gridLines.length > 0) {
|
|
2767
|
+
result.push(buildGridTable(gridLines.splice(0), columns));
|
|
2994
2768
|
}
|
|
2769
|
+
result.push(mergeLineSimple(line));
|
|
2995
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]);
|
|
2996
2800
|
continue;
|
|
2997
2801
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
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];
|
|
3009
2828
|
}
|
|
3010
2829
|
}
|
|
3011
2830
|
}
|
|
3012
|
-
flushKvTable();
|
|
3013
|
-
result.push(block);
|
|
3014
2831
|
}
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
}
|
|
3018
|
-
function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
|
|
3019
|
-
const ZONE_RATIO = 0.12;
|
|
3020
|
-
const MIN_REPEAT = 3;
|
|
3021
|
-
const topEntries = [];
|
|
3022
|
-
const bottomEntries = [];
|
|
3023
|
-
for (let bi = 0; bi < blocks.length; bi++) {
|
|
3024
|
-
const b = blocks[bi];
|
|
3025
|
-
if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
|
|
3026
|
-
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
3027
|
-
if (!ph) continue;
|
|
3028
|
-
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
3029
|
-
const blockBottom = ph - b.bbox.y;
|
|
3030
|
-
const entry = { blockIdx: bi, page: b.pageNumber, text: b.text.trim() };
|
|
3031
|
-
if (blockBottom <= ph * ZONE_RATIO) bottomEntries.push(entry);
|
|
3032
|
-
else if (blockTop >= ph * (1 - ZONE_RATIO)) topEntries.push(entry);
|
|
2832
|
+
if (merged.length < 2) {
|
|
2833
|
+
return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
|
|
3033
2834
|
}
|
|
3034
|
-
|
|
3035
|
-
for (
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
for (const [p, count] of patternCount) {
|
|
3048
|
-
if (count >= MIN_REPEAT && (patternPages.get(p)?.size ?? 0) >= MIN_REPEAT) {
|
|
3049
|
-
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
|
+
}
|
|
3050
2848
|
}
|
|
3051
2849
|
}
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
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]);
|
|
3057
2855
|
}
|
|
3058
2856
|
}
|
|
3059
|
-
|
|
3060
|
-
|
|
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");
|
|
3061
2861
|
}
|
|
3062
|
-
|
|
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");
|
|
3063
2870
|
}
|
|
3064
2871
|
|
|
3065
2872
|
// src/pdf/page-blocks.ts
|
|
@@ -3202,6 +3009,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
3202
3009
|
hasSpaceBefore: i.hasSpaceBefore
|
|
3203
3010
|
}));
|
|
3204
3011
|
const cellTextMap = mapTextToCells(textItems, cells);
|
|
3012
|
+
const assignedItems = /* @__PURE__ */ new Set();
|
|
3013
|
+
for (const arr of cellTextMap.values()) for (const it of arr) assignedItems.add(it);
|
|
3205
3014
|
const numRows = grid.rowYs.length - 1;
|
|
3206
3015
|
const numCols = grid.colXs.length - 1;
|
|
3207
3016
|
const irGrid = Array.from(
|
|
@@ -3221,9 +3030,11 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
3221
3030
|
}
|
|
3222
3031
|
let finalGrid = irGrid;
|
|
3223
3032
|
let finalRows = numRows;
|
|
3033
|
+
let rebuiltUsed = false;
|
|
3224
3034
|
if (numRows <= 2 && numCols >= 3) {
|
|
3225
3035
|
const rebuilt = normalizeUndersegmentedTable(irGrid, grid.colXs, textItems);
|
|
3226
3036
|
if (rebuilt) {
|
|
3037
|
+
rebuiltUsed = true;
|
|
3227
3038
|
finalGrid = rebuilt.map((row) => row.map((rawText) => {
|
|
3228
3039
|
const cleaned = rawText.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
|
|
3229
3040
|
return {
|
|
@@ -3235,6 +3046,11 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
3235
3046
|
finalRows = finalGrid.length;
|
|
3236
3047
|
}
|
|
3237
3048
|
}
|
|
3049
|
+
if (!rebuiltUsed) {
|
|
3050
|
+
for (let ti = 0; ti < textItems.length; ti++) {
|
|
3051
|
+
if (!assignedItems.has(textItems[ti])) usedItems.delete(tableItems[ti]);
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3238
3054
|
const irTable = {
|
|
3239
3055
|
rows: finalRows,
|
|
3240
3056
|
cols: numCols,
|
|
@@ -3291,6 +3107,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
3291
3107
|
}
|
|
3292
3108
|
remaining = remaining.filter((_, idx) => !usedClusterIndices.has(idx));
|
|
3293
3109
|
}
|
|
3110
|
+
const groupSizes = [];
|
|
3111
|
+
let finalTextBlocks = [];
|
|
3294
3112
|
if (remaining.length > 0) {
|
|
3295
3113
|
const allY = remaining.map((i) => i.y);
|
|
3296
3114
|
const pageH = safeMax(allY) - safeMin(allY);
|
|
@@ -3300,16 +3118,28 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
|
3300
3118
|
if (group.length === 0) continue;
|
|
3301
3119
|
const groupBlocks = extractPageBlocksFallback(group, pageNum);
|
|
3302
3120
|
for (const b of groupBlocks) textBlocks.push(b);
|
|
3121
|
+
groupSizes.push(groupBlocks.length);
|
|
3303
3122
|
}
|
|
3304
|
-
|
|
3305
|
-
for (const b of finalTextBlocks) blocks.push(b);
|
|
3123
|
+
finalTextBlocks = detectListBlocks(textBlocks);
|
|
3306
3124
|
}
|
|
3307
|
-
blocks.
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3125
|
+
const units = blocks.map((b) => [b]);
|
|
3126
|
+
let off = 0;
|
|
3127
|
+
for (const size of groupSizes) {
|
|
3128
|
+
const unit = finalTextBlocks.slice(off, off + size);
|
|
3129
|
+
off += size;
|
|
3130
|
+
if (unit.length > 0) units.push(unit);
|
|
3131
|
+
}
|
|
3132
|
+
const unitTopY = (u) => {
|
|
3133
|
+
let top = 0;
|
|
3134
|
+
for (const b of u) {
|
|
3135
|
+
if (b.bbox && b.bbox.y + b.bbox.height > top) top = b.bbox.y + b.bbox.height;
|
|
3136
|
+
}
|
|
3137
|
+
return top;
|
|
3138
|
+
};
|
|
3139
|
+
units.sort((a, b) => unitTopY(b) - unitTopY(a));
|
|
3140
|
+
const ordered = [];
|
|
3141
|
+
for (const u of units) for (const b of u) ordered.push(b);
|
|
3142
|
+
return mergeAdjacentTableBlocks(ordered);
|
|
3313
3143
|
}
|
|
3314
3144
|
return mergeAdjacentTableBlocks(blocks);
|
|
3315
3145
|
}
|
|
@@ -3383,7 +3213,7 @@ function splitTwoColumnProse(items, cutX) {
|
|
|
3383
3213
|
else cross.push(i);
|
|
3384
3214
|
}
|
|
3385
3215
|
if (cross.length === 0) {
|
|
3386
|
-
return [left, right].filter((
|
|
3216
|
+
return [left, right].filter((g) => g.length > 0);
|
|
3387
3217
|
}
|
|
3388
3218
|
cross.sort((a, b) => b.y - a.y);
|
|
3389
3219
|
const crossLines = [];
|
|
@@ -3494,448 +3324,18 @@ function extractPageBlocksFallback(items, pageNum, fullPage = false) {
|
|
|
3494
3324
|
return detectSpecialKoreanTables(blocks);
|
|
3495
3325
|
}
|
|
3496
3326
|
|
|
3497
|
-
// src/pdf/text-clean.ts
|
|
3498
|
-
function sanitizeBlockControlChars(blocks) {
|
|
3499
|
-
for (const b of blocks) {
|
|
3500
|
-
if (b.text) b.text = stripControlChars(b.text);
|
|
3501
|
-
if (b.table) {
|
|
3502
|
-
for (const row of b.table.cells) {
|
|
3503
|
-
for (const cell of row) {
|
|
3504
|
-
if (cell.text) cell.text = stripControlChars(cell.text);
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
}
|
|
3508
|
-
if (b.children) sanitizeBlockControlChars(b.children);
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
function cleanPdfText(text) {
|
|
3512
|
-
return mergeKoreanLines(
|
|
3513
|
-
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, "")
|
|
3514
|
-
).replace(/^(?!\| ---).*$/gm, (line) => {
|
|
3515
|
-
if (/^\s*\${1,2}.+\${1,2}\s*$/.test(line)) return line;
|
|
3516
|
-
return collapseEvenSpacing(line);
|
|
3517
|
-
}).replace(/([□■◆○●▶ㅇ])\s+([가-힣])\s+([가-힣])/g, "$1 $2$3").replace(/\\~\\~/g, "~~").replace(/~~~~/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
3518
|
-
}
|
|
3519
|
-
function startsWithMarker(line) {
|
|
3520
|
-
const t = line.trimStart();
|
|
3521
|
-
return /^[가-힣ㄱ-ㅎ][.)]/.test(t) || /^\d+[.)]/.test(t) || /^\([가-힣ㄱ-ㅎ\d]+\)/.test(t) || /^[○●※▶▷◆◇■□★☆\-·]\s/.test(t) || /^제\d+[조항호장절]/.test(t);
|
|
3522
|
-
}
|
|
3523
|
-
function isStandaloneHeader(line) {
|
|
3524
|
-
return /^제\d+[조항호장절](\([^)]*\))?(\s+\S+){0,7}$/.test(line.trim());
|
|
3525
|
-
}
|
|
3526
|
-
function mergeKoreanLines(text) {
|
|
3527
|
-
if (!text) return "";
|
|
3528
|
-
const lines = text.split("\n");
|
|
3529
|
-
if (lines.length <= 1) return text;
|
|
3530
|
-
const result = [lines[0]];
|
|
3531
|
-
for (let i = 1; i < lines.length; i++) {
|
|
3532
|
-
const prev = result[result.length - 1];
|
|
3533
|
-
const curr = lines[i];
|
|
3534
|
-
const currTrimmed = curr.trim();
|
|
3535
|
-
if (/^#{1,6}\s/.test(prev) || /^#{1,6}\s/.test(curr) || /^\|/.test(currTrimmed) || /^---/.test(currTrimmed)) {
|
|
3536
|
-
result.push(curr);
|
|
3537
|
-
continue;
|
|
3538
|
-
}
|
|
3539
|
-
if (/,$/.test(prev.trim()) && currTrimmed.length > 0) {
|
|
3540
|
-
result[result.length - 1] = prev + "\n" + curr;
|
|
3541
|
-
continue;
|
|
3542
|
-
}
|
|
3543
|
-
if (/^\(※/.test(currTrimmed)) {
|
|
3544
|
-
result[result.length - 1] = prev + " " + currTrimmed;
|
|
3545
|
-
continue;
|
|
3546
|
-
}
|
|
3547
|
-
if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev) && !startsWithMarker(prev)) {
|
|
3548
|
-
result[result.length - 1] = prev + " " + curr;
|
|
3549
|
-
} else {
|
|
3550
|
-
result.push(curr);
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
return result.join("\n");
|
|
3554
|
-
}
|
|
3555
|
-
|
|
3556
|
-
// src/pdf/formula-ocr.ts
|
|
3557
|
-
async function applyFormulaOcr(buffer, blocks, pageFilter, effectivePageCount, warnings, _onProgress) {
|
|
3558
|
-
const formulaMod = await import("./formula-JCNF43NE.js");
|
|
3559
|
-
const { FormulaPipeline, ensureFormulaModels } = formulaMod;
|
|
3560
|
-
await ensureFormulaModels((p) => {
|
|
3561
|
-
if (p.phase === "download" && p.total) {
|
|
3562
|
-
const pct = Math.floor(p.downloaded / p.total * 100);
|
|
3563
|
-
process.stderr.write(`\r[kordoc-formula] ${p.spec.name} ${pct}% (${formatMb(p.downloaded)}/${formatMb(p.total)})`);
|
|
3564
|
-
if (p.downloaded >= p.total) process.stderr.write("\n");
|
|
3565
|
-
} else if (p.phase === "verify") {
|
|
3566
|
-
process.stderr.write(`[kordoc-formula] ${p.spec.name} SHA-256 \uAC80\uC99D \uC911...
|
|
3567
|
-
`);
|
|
3568
|
-
} else if (p.phase === "done") {
|
|
3569
|
-
process.stderr.write(`[kordoc-formula] ${p.spec.name} \uC900\uBE44 \uC644\uB8CC
|
|
3570
|
-
`);
|
|
3571
|
-
} else if (p.phase === "skip") {
|
|
3572
|
-
}
|
|
3573
|
-
});
|
|
3574
|
-
const pipeline = await FormulaPipeline.create();
|
|
3575
|
-
try {
|
|
3576
|
-
const pagesResult = await pipeline.runOnBuffer(buffer, pageFilter);
|
|
3577
|
-
if (pagesResult.length === 0) return;
|
|
3578
|
-
let insertedCount = 0;
|
|
3579
|
-
let removedDupCount = 0;
|
|
3580
|
-
for (const page of pagesResult) {
|
|
3581
|
-
const pageNumber = page.pageNumber;
|
|
3582
|
-
const pdfHeight = page.pdfHeight;
|
|
3583
|
-
const scaleX = page.renderedWidth > 0 ? page.pdfWidth / page.renderedWidth : 0.5;
|
|
3584
|
-
const scaleY = page.renderedHeight > 0 ? page.pdfHeight / page.renderedHeight : 0.5;
|
|
3585
|
-
const candidates = [];
|
|
3586
|
-
for (const r of page.regions) {
|
|
3587
|
-
if (!r.latex || !r.latex.trim()) continue;
|
|
3588
|
-
const wrapped = r.kind === "display" ? `$$${r.latex}$$` : `$${r.latex}$`;
|
|
3589
|
-
const x1 = r.bbox.x1 * scaleX;
|
|
3590
|
-
const x2 = r.bbox.x2 * scaleX;
|
|
3591
|
-
const yTop = pdfHeight - r.bbox.y1 * scaleY;
|
|
3592
|
-
const yBottom = pdfHeight - r.bbox.y2 * scaleY;
|
|
3593
|
-
const centerY = (yTop + yBottom) / 2;
|
|
3594
|
-
const width = x2 - x1;
|
|
3595
|
-
const height = yTop - yBottom;
|
|
3596
|
-
candidates.push({
|
|
3597
|
-
block: {
|
|
3598
|
-
type: "paragraph",
|
|
3599
|
-
text: wrapped,
|
|
3600
|
-
pageNumber,
|
|
3601
|
-
bbox: { page: pageNumber, x: x1, y: yBottom, width, height }
|
|
3602
|
-
},
|
|
3603
|
-
pdfBbox: { x1, x2, yTop, yBottom },
|
|
3604
|
-
centerY
|
|
3605
|
-
});
|
|
3606
|
-
}
|
|
3607
|
-
if (candidates.length === 0) continue;
|
|
3608
|
-
const OVERLAP_THRESHOLD = 0.6;
|
|
3609
|
-
const indicesToRemove = /* @__PURE__ */ new Set();
|
|
3610
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
3611
|
-
const b = blocks[i];
|
|
3612
|
-
if (b.pageNumber !== pageNumber) continue;
|
|
3613
|
-
if (b.type === "table") continue;
|
|
3614
|
-
if (!b.bbox || b.bbox.width <= 0 || b.bbox.height <= 0) continue;
|
|
3615
|
-
const blockArea = b.bbox.width * b.bbox.height;
|
|
3616
|
-
if (blockArea <= 0) continue;
|
|
3617
|
-
for (const c of candidates) {
|
|
3618
|
-
const ox1 = Math.max(b.bbox.x, c.pdfBbox.x1);
|
|
3619
|
-
const ox2 = Math.min(b.bbox.x + b.bbox.width, c.pdfBbox.x2);
|
|
3620
|
-
const oy1 = Math.max(b.bbox.y, c.pdfBbox.yBottom);
|
|
3621
|
-
const oy2 = Math.min(b.bbox.y + b.bbox.height, c.pdfBbox.yTop);
|
|
3622
|
-
const interArea = Math.max(0, ox2 - ox1) * Math.max(0, oy2 - oy1);
|
|
3623
|
-
if (interArea / blockArea >= OVERLAP_THRESHOLD) {
|
|
3624
|
-
indicesToRemove.add(i);
|
|
3625
|
-
break;
|
|
3626
|
-
}
|
|
3627
|
-
}
|
|
3628
|
-
}
|
|
3629
|
-
if (indicesToRemove.size > 0) {
|
|
3630
|
-
const sorted = [...indicesToRemove].sort((a, b) => b - a);
|
|
3631
|
-
for (const idx of sorted) blocks.splice(idx, 1);
|
|
3632
|
-
removedDupCount += indicesToRemove.size;
|
|
3633
|
-
}
|
|
3634
|
-
candidates.sort((a, b) => b.centerY - a.centerY);
|
|
3635
|
-
for (const c of candidates) {
|
|
3636
|
-
let insertIdx = -1;
|
|
3637
|
-
let pageFirstIdx = -1;
|
|
3638
|
-
let pageLastIdx = -1;
|
|
3639
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
3640
|
-
const b = blocks[i];
|
|
3641
|
-
if (b.pageNumber !== pageNumber) continue;
|
|
3642
|
-
if (pageFirstIdx === -1) pageFirstIdx = i;
|
|
3643
|
-
pageLastIdx = i;
|
|
3644
|
-
if (!b.bbox) continue;
|
|
3645
|
-
const blockCenter = b.bbox.y + b.bbox.height / 2;
|
|
3646
|
-
if (blockCenter < c.centerY) {
|
|
3647
|
-
insertIdx = i;
|
|
3648
|
-
break;
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
if (insertIdx !== -1) {
|
|
3652
|
-
blocks.splice(insertIdx, 0, c.block);
|
|
3653
|
-
} else if (pageLastIdx !== -1) {
|
|
3654
|
-
blocks.splice(pageLastIdx + 1, 0, c.block);
|
|
3655
|
-
} else {
|
|
3656
|
-
blocks.push(c.block);
|
|
3657
|
-
}
|
|
3658
|
-
insertedCount++;
|
|
3659
|
-
}
|
|
3660
|
-
}
|
|
3661
|
-
if (insertedCount > 0 || removedDupCount > 0) {
|
|
3662
|
-
process.stderr.write(
|
|
3663
|
-
`[kordoc-formula] ${insertedCount}\uAC1C \uC218\uC2DD \uC0BD\uC785, ${removedDupCount}\uAC1C \uC911\uBCF5 block \uC81C\uAC70 (${pagesResult.length}\uAC1C \uD398\uC774\uC9C0)
|
|
3664
|
-
`
|
|
3665
|
-
);
|
|
3666
|
-
}
|
|
3667
|
-
} finally {
|
|
3668
|
-
await pipeline.destroy().catch(() => {
|
|
3669
|
-
});
|
|
3670
|
-
}
|
|
3671
|
-
}
|
|
3672
|
-
function formatMb(bytes) {
|
|
3673
|
-
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
3674
|
-
}
|
|
3675
|
-
|
|
3676
|
-
// src/pdf/polyfill.ts
|
|
3677
|
-
import * as pdfjsWorker from "pdfjs-dist/legacy/build/pdf.worker.mjs";
|
|
3678
|
-
var g = globalThis;
|
|
3679
|
-
if (typeof g.DOMMatrix === "undefined") {
|
|
3680
|
-
g.DOMMatrix = class DOMMatrix {
|
|
3681
|
-
m = [1, 0, 0, 1, 0, 0];
|
|
3682
|
-
constructor(init) {
|
|
3683
|
-
if (init) this.m = init;
|
|
3684
|
-
}
|
|
3685
|
-
};
|
|
3686
|
-
}
|
|
3687
|
-
if (typeof g.Path2D === "undefined") {
|
|
3688
|
-
g.Path2D = class Path2D {
|
|
3689
|
-
};
|
|
3690
|
-
}
|
|
3691
|
-
g.pdfjsWorker = pdfjsWorker;
|
|
3692
|
-
|
|
3693
|
-
// src/pdf/parser.ts
|
|
3694
|
-
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
3695
|
-
import { createRequire } from "module";
|
|
3696
|
-
import { dirname, join } from "path";
|
|
3697
|
-
GlobalWorkerOptions.workerSrc = "";
|
|
3698
|
-
var MAX_PAGES = 5e3;
|
|
3699
|
-
var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
|
|
3700
|
-
var PDF_LOAD_TIMEOUT_MS = 3e4;
|
|
3701
|
-
var pdfjsAssets = {};
|
|
3702
|
-
try {
|
|
3703
|
-
const _require = createRequire(import.meta.url);
|
|
3704
|
-
const pkgDir = dirname(_require.resolve("pdfjs-dist/package.json"));
|
|
3705
|
-
pdfjsAssets.cMapUrl = join(pkgDir, "cmaps") + "/";
|
|
3706
|
-
pdfjsAssets.cMapPacked = true;
|
|
3707
|
-
pdfjsAssets.standardFontDataUrl = join(pkgDir, "standard_fonts") + "/";
|
|
3708
|
-
} catch {
|
|
3709
|
-
}
|
|
3710
|
-
async function loadPdfWithTimeout(buffer) {
|
|
3711
|
-
const loadingTask = getDocument({
|
|
3712
|
-
data: new Uint8Array(buffer),
|
|
3713
|
-
useSystemFonts: true,
|
|
3714
|
-
disableFontFace: true,
|
|
3715
|
-
isEvalSupported: false,
|
|
3716
|
-
...pdfjsAssets
|
|
3717
|
-
});
|
|
3718
|
-
let timer;
|
|
3719
|
-
try {
|
|
3720
|
-
return await Promise.race([
|
|
3721
|
-
loadingTask.promise,
|
|
3722
|
-
new Promise((_, reject) => {
|
|
3723
|
-
timer = setTimeout(() => {
|
|
3724
|
-
loadingTask.destroy();
|
|
3725
|
-
reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
|
|
3726
|
-
}, PDF_LOAD_TIMEOUT_MS);
|
|
3727
|
-
})
|
|
3728
|
-
]);
|
|
3729
|
-
} finally {
|
|
3730
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
3731
|
-
}
|
|
3732
|
-
}
|
|
3733
|
-
async function parsePdfDocument(buffer, options) {
|
|
3734
|
-
const formulaBuffer = options?.formulaOcr ? buffer.slice(0) : null;
|
|
3735
|
-
const doc = await loadPdfWithTimeout(buffer);
|
|
3736
|
-
try {
|
|
3737
|
-
const pageCount = doc.numPages;
|
|
3738
|
-
if (pageCount === 0) throw new KordocError("PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3739
|
-
const metadata = { pageCount };
|
|
3740
|
-
await extractPdfMetadata(doc, metadata);
|
|
3741
|
-
const blocks = [];
|
|
3742
|
-
const warnings = [];
|
|
3743
|
-
const pageQuality = [];
|
|
3744
|
-
let totalChars = 0;
|
|
3745
|
-
let totalTextBytes = 0;
|
|
3746
|
-
const effectivePageCount = Math.min(pageCount, MAX_PAGES);
|
|
3747
|
-
const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
|
|
3748
|
-
const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
|
|
3749
|
-
const fontSizeFreq = /* @__PURE__ */ new Map();
|
|
3750
|
-
const pageHeights = /* @__PURE__ */ new Map();
|
|
3751
|
-
const pagesWithLargeImage = /* @__PURE__ */ new Set();
|
|
3752
|
-
const skippedImagePages = /* @__PURE__ */ new Map();
|
|
3753
|
-
const imageState = createPdfImageState();
|
|
3754
|
-
const extractedImages = [];
|
|
3755
|
-
const pageImageBlocks = /* @__PURE__ */ new Map();
|
|
3756
|
-
let parsedPages = 0;
|
|
3757
|
-
for (let i = 1; i <= effectivePageCount; i++) {
|
|
3758
|
-
if (pageFilter && !pageFilter.has(i)) continue;
|
|
3759
|
-
try {
|
|
3760
|
-
const page = await doc.getPage(i);
|
|
3761
|
-
const tc = await page.getTextContent();
|
|
3762
|
-
const viewport = page.getViewport({ scale: 1 });
|
|
3763
|
-
pageHeights.set(i, viewport.height);
|
|
3764
|
-
const rawItems = tc.items;
|
|
3765
|
-
const items = normalizeItems(rawItems);
|
|
3766
|
-
const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
|
|
3767
|
-
if (hiddenCount > 0) {
|
|
3768
|
-
warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
3769
|
-
}
|
|
3770
|
-
for (const item of visible) {
|
|
3771
|
-
if (item.fontSize > 0) fontSizeFreq.set(item.fontSize, (fontSizeFreq.get(item.fontSize) || 0) + 1);
|
|
3772
|
-
}
|
|
3773
|
-
const opList = await page.getOperatorList();
|
|
3774
|
-
const pageArea = viewport.width * viewport.height;
|
|
3775
|
-
if (pageArea > 0) {
|
|
3776
|
-
const imageRegions = extractImageRegions(opList.fnArray, opList.argsArray);
|
|
3777
|
-
let uncovered = 0;
|
|
3778
|
-
for (const r of imageRegions) {
|
|
3779
|
-
const area = (r.x2 - r.x1) * (r.y2 - r.y1);
|
|
3780
|
-
if (area < pageArea * 0.05) continue;
|
|
3781
|
-
pagesWithLargeImage.add(i);
|
|
3782
|
-
const hasText = visible.some((it) => {
|
|
3783
|
-
const cx = it.x + it.w / 2;
|
|
3784
|
-
const cy = it.y + (it.h || it.fontSize) / 2;
|
|
3785
|
-
return cx >= r.x1 && cx <= r.x2 && cy >= r.y1 && cy <= r.y2;
|
|
3786
|
-
});
|
|
3787
|
-
if (!hasText) uncovered++;
|
|
3788
|
-
}
|
|
3789
|
-
if (uncovered > 0) skippedImagePages.set(i, uncovered);
|
|
3790
|
-
}
|
|
3791
|
-
const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
|
|
3792
|
-
for (const b of pageBlocks) blocks.push(b);
|
|
3793
|
-
try {
|
|
3794
|
-
const { blocks: imgBlocks, images: pageImages } = await extractPageImages(page, opList.fnArray, opList.argsArray, i, imageState, warnings);
|
|
3795
|
-
if (imgBlocks.length > 0) pageImageBlocks.set(i, imgBlocks);
|
|
3796
|
-
extractedImages.push(...pageImages);
|
|
3797
|
-
} catch {
|
|
3798
|
-
}
|
|
3799
|
-
let pageText = "";
|
|
3800
|
-
for (const b of pageBlocks) {
|
|
3801
|
-
const t = b.text || "";
|
|
3802
|
-
totalChars += t.replace(/\s/g, "").length;
|
|
3803
|
-
totalTextBytes += t.length * 2;
|
|
3804
|
-
pageText += pageText ? "\n" + t : t;
|
|
3805
|
-
}
|
|
3806
|
-
pageQuality.push(computePageQuality(i, pageText));
|
|
3807
|
-
if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
|
|
3808
|
-
parsedPages++;
|
|
3809
|
-
options?.onProgress?.(parsedPages, totalTarget);
|
|
3810
|
-
} catch (pageErr) {
|
|
3811
|
-
if (pageErr instanceof KordocError) throw pageErr;
|
|
3812
|
-
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" });
|
|
3813
|
-
}
|
|
3814
|
-
}
|
|
3815
|
-
const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
|
|
3816
|
-
let isImageBased = false;
|
|
3817
|
-
if (totalChars / Math.max(parsedPageCount, 1) < 10) {
|
|
3818
|
-
if (options?.ocr) {
|
|
3819
|
-
try {
|
|
3820
|
-
const { ocrPages } = await import("./provider-AKROB7WQ.js");
|
|
3821
|
-
const ocrBlocks = await ocrPages(doc, options.ocr, pageFilter, effectivePageCount);
|
|
3822
|
-
if (ocrBlocks.length > 0) {
|
|
3823
|
-
const ocrMarkdown = ocrBlocks.map((b) => b.text || "").filter(Boolean).join("\n\n");
|
|
3824
|
-
return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true, pageQuality, qualitySummary: summarizeDocumentQuality(pageQuality), images: extractedImages.length > 0 ? extractedImages : void 0 };
|
|
3825
|
-
}
|
|
3826
|
-
} catch {
|
|
3827
|
-
}
|
|
3828
|
-
}
|
|
3829
|
-
isImageBased = true;
|
|
3830
|
-
warnings.push({
|
|
3831
|
-
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`,
|
|
3832
|
-
code: "NEEDS_OCR"
|
|
3833
|
-
});
|
|
3834
|
-
}
|
|
3835
|
-
if (!isImageBased) {
|
|
3836
|
-
const OCR_REASON_MESSAGES = {
|
|
3837
|
-
low_text: "\uD14D\uC2A4\uD2B8\uAC00 \uAC70\uC758 \uC5C6\uB294 \uD398\uC774\uC9C0 (\uC2A4\uCE94/\uC774\uBBF8\uC9C0 \uCD94\uC815)",
|
|
3838
|
-
high_pua: "\uAE00\uAF34 \uB9E4\uD551 \uC2E4\uD328 (PUA \uBE44\uC728 \uB192\uC74C) \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
3839
|
-
high_control: "\uC81C\uC5B4\uBB38\uC790 \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
3840
|
-
high_replacement: "\uB300\uCCB4\uBB38\uC790(U+FFFD) \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
3841
|
-
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"
|
|
3842
|
-
};
|
|
3843
|
-
for (const pq of pageQuality) {
|
|
3844
|
-
if (!pq.needsOcr || !pq.ocrReason) continue;
|
|
3845
|
-
if (pq.ocrReason === "low_text" && !pagesWithLargeImage.has(pq.page)) continue;
|
|
3846
|
-
warnings.push({ page: pq.page, message: `${OCR_REASON_MESSAGES[pq.ocrReason]} \u2014 OCR \uAC80\uD1A0 \uD544\uC694`, code: "NEEDS_OCR" });
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
|
-
if (!isImageBased) {
|
|
3850
|
-
for (const [page, count] of [...skippedImagePages.entries()].sort((a, b) => a[0] - b[0])) {
|
|
3851
|
-
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" });
|
|
3852
|
-
}
|
|
3853
|
-
}
|
|
3854
|
-
if (options?.removeHeaderFooter !== false && parsedPageCount >= 3) {
|
|
3855
|
-
const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
|
|
3856
|
-
for (let ri = removed.length - 1; ri >= 0; ri--) {
|
|
3857
|
-
blocks.splice(removed[ri], 1);
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
mergeCrossPageTables(blocks);
|
|
3861
|
-
injectPageImageBlocks(blocks, pageImageBlocks);
|
|
3862
|
-
if (options?.formulaOcr && formulaBuffer) {
|
|
3863
|
-
try {
|
|
3864
|
-
await applyFormulaOcr(formulaBuffer, blocks, pageFilter, effectivePageCount, warnings, options.onProgress);
|
|
3865
|
-
} catch (e) {
|
|
3866
|
-
warnings.push({
|
|
3867
|
-
message: `\uC218\uC2DD OCR \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`,
|
|
3868
|
-
code: "PARTIAL_PARSE"
|
|
3869
|
-
});
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
const medianFontSize = computeMedianFontSizeFromFreq(fontSizeFreq);
|
|
3873
|
-
if (medianFontSize > 0) {
|
|
3874
|
-
detectHeadings(blocks, medianFontSize);
|
|
3875
|
-
}
|
|
3876
|
-
detectMarkerHeadings(blocks);
|
|
3877
|
-
detectTableCaptions(blocks);
|
|
3878
|
-
detectKoreanListBlocks(blocks);
|
|
3879
|
-
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
3880
|
-
sanitizeBlockControlChars(blocks);
|
|
3881
|
-
let markdown = cleanPdfText(blocksToMarkdown(blocks));
|
|
3882
|
-
return {
|
|
3883
|
-
markdown,
|
|
3884
|
-
blocks,
|
|
3885
|
-
metadata,
|
|
3886
|
-
outline: outline.length > 0 ? outline : void 0,
|
|
3887
|
-
warnings: warnings.length > 0 ? warnings : void 0,
|
|
3888
|
-
isImageBased: isImageBased || void 0,
|
|
3889
|
-
pageQuality,
|
|
3890
|
-
qualitySummary: summarizeDocumentQuality(pageQuality),
|
|
3891
|
-
images: extractedImages.length > 0 ? extractedImages : void 0
|
|
3892
|
-
};
|
|
3893
|
-
} finally {
|
|
3894
|
-
await doc.destroy().catch(() => {
|
|
3895
|
-
});
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
async function extractPdfMetadata(doc, metadata) {
|
|
3899
|
-
try {
|
|
3900
|
-
const result = await doc.getMetadata();
|
|
3901
|
-
if (!result?.info) return;
|
|
3902
|
-
const info = result.info;
|
|
3903
|
-
if (typeof info.Title === "string" && info.Title.trim()) metadata.title = info.Title.trim();
|
|
3904
|
-
if (typeof info.Author === "string" && info.Author.trim()) metadata.author = info.Author.trim();
|
|
3905
|
-
if (typeof info.Creator === "string" && info.Creator.trim()) metadata.creator = info.Creator.trim();
|
|
3906
|
-
if (typeof info.Subject === "string" && info.Subject.trim()) metadata.description = info.Subject.trim();
|
|
3907
|
-
if (typeof info.Keywords === "string" && info.Keywords.trim()) {
|
|
3908
|
-
metadata.keywords = info.Keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
|
|
3909
|
-
}
|
|
3910
|
-
if (typeof info.CreationDate === "string") metadata.createdAt = parsePdfDate(info.CreationDate);
|
|
3911
|
-
if (typeof info.ModDate === "string") metadata.modifiedAt = parsePdfDate(info.ModDate);
|
|
3912
|
-
} catch {
|
|
3913
|
-
}
|
|
3914
|
-
}
|
|
3915
|
-
function parsePdfDate(dateStr) {
|
|
3916
|
-
const m = dateStr.match(/D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?/);
|
|
3917
|
-
if (!m) return void 0;
|
|
3918
|
-
const [, year, month = "01", day = "01", hour = "00", min = "00", sec = "00"] = m;
|
|
3919
|
-
return `${year}-${month}-${day}T${hour}:${min}:${sec}`;
|
|
3920
|
-
}
|
|
3921
|
-
async function extractPdfMetadataOnly(buffer) {
|
|
3922
|
-
const doc = await loadPdfWithTimeout(buffer);
|
|
3923
|
-
try {
|
|
3924
|
-
const metadata = { pageCount: doc.numPages };
|
|
3925
|
-
await extractPdfMetadata(doc, metadata);
|
|
3926
|
-
return metadata;
|
|
3927
|
-
} finally {
|
|
3928
|
-
await doc.destroy().catch(() => {
|
|
3929
|
-
});
|
|
3930
|
-
}
|
|
3931
|
-
}
|
|
3932
3327
|
export {
|
|
3933
|
-
|
|
3934
|
-
|
|
3328
|
+
extractImageRegions,
|
|
3329
|
+
filterHiddenText,
|
|
3330
|
+
collapseEvenSpacing,
|
|
3331
|
+
normalizeItems,
|
|
3332
|
+
computeMedianFontSizeFromFreq,
|
|
3333
|
+
detectHeadings,
|
|
3334
|
+
detectMarkerHeadings,
|
|
3935
3335
|
detectTableCaptions,
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3336
|
+
detectKoreanListBlocks,
|
|
3337
|
+
removeHeaderFooterBlocks,
|
|
3338
|
+
extractPageBlocksWithLines,
|
|
3339
|
+
mergeCrossPageTables
|
|
3940
3340
|
};
|
|
3941
|
-
//# sourceMappingURL=
|
|
3341
|
+
//# sourceMappingURL=chunk-26AJJCFF.js.map
|