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