kordoc 3.8.0 → 3.8.2
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 +11 -1
- package/dist/{-ATVQYFSW.js → -FEHSMPVO.js} +3 -3
- package/dist/{chunk-QV25HMU7.js → chunk-553VTUVP.js} +2 -2
- package/dist/{chunk-SLKF72QF.js → chunk-DP37KF2X.js} +1985 -1954
- package/dist/chunk-DP37KF2X.js.map +1 -0
- package/dist/{chunk-3R3YK7EM.js → chunk-JHZUFBUV.js} +2 -2
- package/dist/{chunk-ITJIALN5.cjs → chunk-YBPNKFJW.cjs} +2 -2
- package/dist/{chunk-ITJIALN5.cjs.map → chunk-YBPNKFJW.cjs.map} +1 -1
- package/dist/cli.js +4 -4
- package/dist/index.cjs +2059 -2028
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -8
- package/dist/index.d.ts +17 -8
- package/dist/index.js +1974 -1943
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +3 -3
- package/dist/{parser-7G5F7PT2.js → parser-KNQDRLZQ.js} +1930 -1863
- package/dist/parser-KNQDRLZQ.js.map +1 -0
- package/dist/{parser-DCK42RMA.js → parser-NR2TYGO3.js} +1930 -1863
- package/dist/parser-NR2TYGO3.js.map +1 -0
- package/dist/{parser-GUSJH44K.cjs → parser-NS4ZPD7B.cjs} +1933 -1864
- package/dist/parser-NS4ZPD7B.cjs.map +1 -0
- package/dist/{watch-GVZESOCE.js → watch-XCWADLPU.js} +3 -3
- package/package.json +2 -2
- package/dist/chunk-SLKF72QF.js.map +0 -1
- package/dist/parser-7G5F7PT2.js.map +0 -1
- package/dist/parser-DCK42RMA.js.map +0 -1
- package/dist/parser-GUSJH44K.cjs.map +0 -1
- /package/dist/{-ATVQYFSW.js.map → -FEHSMPVO.js.map} +0 -0
- /package/dist/{chunk-QV25HMU7.js.map → chunk-553VTUVP.js.map} +0 -0
- /package/dist/{chunk-3R3YK7EM.js.map → chunk-JHZUFBUV.js.map} +0 -0
- /package/dist/{watch-GVZESOCE.js.map → watch-XCWADLPU.js.map} +0 -0
|
@@ -6,42 +6,47 @@
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
var
|
|
9
|
+
var _chunkYBPNKFJWcjs = require('./chunk-YBPNKFJW.cjs');
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
var _chunkDCZVOIEOcjs = require('./chunk-DCZVOIEO.cjs');
|
|
13
|
-
require('./chunk-GS7T56RP.cjs');
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
var _chunkGS7T56RPcjs = require('./chunk-GS7T56RP.cjs');
|
|
16
|
+
|
|
17
|
+
// src/pdf/line-extract.ts
|
|
16
18
|
var _pdfmjs = require('pdfjs-dist/legacy/build/pdf.mjs');
|
|
17
19
|
var ORIENTATION_TOL = 2;
|
|
18
20
|
var MIN_LINE_LENGTH = 15;
|
|
19
21
|
var MAX_LINE_WIDTH = 5;
|
|
20
|
-
var CONNECT_TOL = 5;
|
|
21
|
-
var CELL_PADDING = 2;
|
|
22
|
-
var MIN_COL_WIDTH = 15;
|
|
23
|
-
var MIN_ROW_HEIGHT = 6;
|
|
24
|
-
var VERTEX_MERGE_FACTOR = 4;
|
|
25
|
-
var MIN_COORD_MERGE_TOL = 8;
|
|
26
22
|
function extractLines(fnArray, argsArray) {
|
|
27
23
|
const horizontals = [];
|
|
28
24
|
const verticals = [];
|
|
29
25
|
let lineWidth = 1;
|
|
26
|
+
let ctm = [1, 0, 0, 1, 0, 0];
|
|
27
|
+
const ctmStack = [];
|
|
28
|
+
const applyCtm = (x, y) => [ctm[0] * x + ctm[2] * y + ctm[4], ctm[1] * x + ctm[3] * y + ctm[5]];
|
|
29
|
+
const ctmScale = () => (Math.hypot(ctm[0], ctm[1]) + Math.hypot(ctm[2], ctm[3])) / 2;
|
|
30
30
|
let currentPath = [];
|
|
31
31
|
let pathStartX = 0, pathStartY = 0;
|
|
32
32
|
let curX = 0, curY = 0;
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
function pushSeg(x1, y1, x2, y2) {
|
|
34
|
+
const [tx1, ty1] = applyCtm(x1, y1);
|
|
35
|
+
const [tx2, ty2] = applyCtm(x2, y2);
|
|
36
|
+
currentPath.push({ x1: tx1, y1: ty1, x2: tx2, y2: ty2 });
|
|
37
|
+
}
|
|
38
|
+
function pushRectangle(rx, ry, rw, rh) {
|
|
39
|
+
const effH = Math.abs(rh) * Math.hypot(ctm[2], ctm[3]);
|
|
40
|
+
const effW = Math.abs(rw) * Math.hypot(ctm[0], ctm[1]);
|
|
41
|
+
if (effH < ORIENTATION_TOL * 2) {
|
|
42
|
+
pushSeg(rx, ry + rh / 2, rx + rw, ry + rh / 2);
|
|
43
|
+
} else if (effW < ORIENTATION_TOL * 2) {
|
|
44
|
+
pushSeg(rx + rw / 2, ry, rx + rw / 2, ry + rh);
|
|
38
45
|
} else {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ x1: rx, y1: ry + rh, x2: rx, y2: ry }
|
|
44
|
-
);
|
|
46
|
+
pushSeg(rx, ry, rx + rw, ry);
|
|
47
|
+
pushSeg(rx + rw, ry, rx + rw, ry + rh);
|
|
48
|
+
pushSeg(rx + rw, ry + rh, rx, ry + rh);
|
|
49
|
+
pushSeg(rx, ry + rh, rx, ry);
|
|
45
50
|
}
|
|
46
51
|
}
|
|
47
52
|
function flushPath(isStroke) {
|
|
@@ -49,8 +54,9 @@ function extractLines(fnArray, argsArray) {
|
|
|
49
54
|
currentPath = [];
|
|
50
55
|
return;
|
|
51
56
|
}
|
|
57
|
+
const effWidth = lineWidth * ctmScale();
|
|
52
58
|
for (const seg of currentPath) {
|
|
53
|
-
classifyAndAdd(seg,
|
|
59
|
+
classifyAndAdd(seg, effWidth, horizontals, verticals);
|
|
54
60
|
}
|
|
55
61
|
currentPath = [];
|
|
56
62
|
}
|
|
@@ -61,6 +67,24 @@ function extractLines(fnArray, argsArray) {
|
|
|
61
67
|
case _pdfmjs.OPS.setLineWidth:
|
|
62
68
|
lineWidth = args[0] || 1;
|
|
63
69
|
break;
|
|
70
|
+
case _pdfmjs.OPS.save:
|
|
71
|
+
ctmStack.push(ctm.slice());
|
|
72
|
+
break;
|
|
73
|
+
case _pdfmjs.OPS.restore:
|
|
74
|
+
ctm = _nullishCoalesce(ctmStack.pop(), () => ( [1, 0, 0, 1, 0, 0]));
|
|
75
|
+
break;
|
|
76
|
+
case _pdfmjs.OPS.transform: {
|
|
77
|
+
const t = args;
|
|
78
|
+
ctm = [
|
|
79
|
+
ctm[0] * t[0] + ctm[2] * t[1],
|
|
80
|
+
ctm[1] * t[0] + ctm[3] * t[1],
|
|
81
|
+
ctm[0] * t[2] + ctm[2] * t[3],
|
|
82
|
+
ctm[1] * t[2] + ctm[3] * t[3],
|
|
83
|
+
ctm[0] * t[4] + ctm[2] * t[5] + ctm[4],
|
|
84
|
+
ctm[1] * t[4] + ctm[3] * t[5] + ctm[5]
|
|
85
|
+
];
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
64
88
|
case _pdfmjs.OPS.constructPath: {
|
|
65
89
|
const arg0 = args[0];
|
|
66
90
|
if (Array.isArray(arg0)) {
|
|
@@ -75,16 +99,16 @@ function extractLines(fnArray, argsArray) {
|
|
|
75
99
|
pathStartY = curY;
|
|
76
100
|
} else if (subOp === _pdfmjs.OPS.lineTo) {
|
|
77
101
|
const x2 = coords[ci++], y2 = coords[ci++];
|
|
78
|
-
|
|
102
|
+
pushSeg(curX, curY, x2, y2);
|
|
79
103
|
curX = x2;
|
|
80
104
|
curY = y2;
|
|
81
105
|
} else if (subOp === _pdfmjs.OPS.rectangle) {
|
|
82
106
|
const rx = coords[ci++], ry = coords[ci++];
|
|
83
107
|
const rw = coords[ci++], rh = coords[ci++];
|
|
84
|
-
pushRectangle(
|
|
108
|
+
pushRectangle(rx, ry, rw, rh);
|
|
85
109
|
} else if (subOp === _pdfmjs.OPS.closePath) {
|
|
86
110
|
if (curX !== pathStartX || curY !== pathStartY) {
|
|
87
|
-
|
|
111
|
+
pushSeg(curX, curY, pathStartX, pathStartY);
|
|
88
112
|
}
|
|
89
113
|
curX = pathStartX;
|
|
90
114
|
curY = pathStartY;
|
|
@@ -110,7 +134,7 @@ function extractLines(fnArray, argsArray) {
|
|
|
110
134
|
pathStartY = curY;
|
|
111
135
|
} else if (drawOp === 1 /* lineTo */) {
|
|
112
136
|
const x2 = pathData[di++], y2 = pathData[di++];
|
|
113
|
-
|
|
137
|
+
pushSeg(curX, curY, x2, y2);
|
|
114
138
|
curX = x2;
|
|
115
139
|
curY = y2;
|
|
116
140
|
} else if (drawOp === 2 /* curveTo */) {
|
|
@@ -119,7 +143,7 @@ function extractLines(fnArray, argsArray) {
|
|
|
119
143
|
di += 4;
|
|
120
144
|
} else if (drawOp === 4 /* closePath */) {
|
|
121
145
|
if (curX !== pathStartX || curY !== pathStartY) {
|
|
122
|
-
|
|
146
|
+
pushSeg(curX, curY, pathStartX, pathStartY);
|
|
123
147
|
}
|
|
124
148
|
curX = pathStartX;
|
|
125
149
|
curY = pathStartY;
|
|
@@ -157,55 +181,6 @@ function extractLines(fnArray, argsArray) {
|
|
|
157
181
|
}
|
|
158
182
|
return { horizontals, verticals };
|
|
159
183
|
}
|
|
160
|
-
function multiplyTransform(m, t) {
|
|
161
|
-
return [
|
|
162
|
-
m[0] * t[0] + m[2] * t[1],
|
|
163
|
-
m[1] * t[0] + m[3] * t[1],
|
|
164
|
-
m[0] * t[2] + m[2] * t[3],
|
|
165
|
-
m[1] * t[2] + m[3] * t[3],
|
|
166
|
-
m[0] * t[4] + m[2] * t[5] + m[4],
|
|
167
|
-
m[1] * t[4] + m[3] * t[5] + m[5]
|
|
168
|
-
];
|
|
169
|
-
}
|
|
170
|
-
function extractImageRegions(fnArray, argsArray) {
|
|
171
|
-
const regions = [];
|
|
172
|
-
let ctm = [1, 0, 0, 1, 0, 0];
|
|
173
|
-
const stack = [];
|
|
174
|
-
for (let i = 0; i < fnArray.length; i++) {
|
|
175
|
-
const op = fnArray[i];
|
|
176
|
-
switch (op) {
|
|
177
|
-
case _pdfmjs.OPS.save:
|
|
178
|
-
stack.push(ctm);
|
|
179
|
-
break;
|
|
180
|
-
case _pdfmjs.OPS.restore:
|
|
181
|
-
ctm = stack.pop() || [1, 0, 0, 1, 0, 0];
|
|
182
|
-
break;
|
|
183
|
-
case _pdfmjs.OPS.transform: {
|
|
184
|
-
const t = argsArray[i];
|
|
185
|
-
if (Array.isArray(t) && t.length >= 6) ctm = multiplyTransform(ctm, t);
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
case _pdfmjs.OPS.paintImageXObject:
|
|
189
|
-
case _pdfmjs.OPS.paintInlineImageXObject:
|
|
190
|
-
case _pdfmjs.OPS.paintImageMaskXObject:
|
|
191
|
-
case _pdfmjs.OPS.paintImageXObjectRepeat: {
|
|
192
|
-
const corners = [[0, 0], [1, 0], [0, 1], [1, 1]];
|
|
193
|
-
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
|
194
|
-
for (const [u, v] of corners) {
|
|
195
|
-
const x = ctm[0] * u + ctm[2] * v + ctm[4];
|
|
196
|
-
const y = ctm[1] * u + ctm[3] * v + ctm[5];
|
|
197
|
-
if (x < x1) x1 = x;
|
|
198
|
-
if (x > x2) x2 = x;
|
|
199
|
-
if (y < y1) y1 = y;
|
|
200
|
-
if (y > y2) y2 = y;
|
|
201
|
-
}
|
|
202
|
-
if (x2 - x1 > 0 && y2 - y1 > 0) regions.push({ x1, y1, x2, y2 });
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return regions;
|
|
208
|
-
}
|
|
209
184
|
function classifyAndAdd(seg, lineWidth, horizontals, verticals) {
|
|
210
185
|
const dx = Math.abs(seg.x2 - seg.x1);
|
|
211
186
|
const dy = Math.abs(seg.y2 - seg.y1);
|
|
@@ -283,6 +258,67 @@ function filterPageBorderLines(horizontals, verticals, pageWidth, pageHeight) {
|
|
|
283
258
|
)
|
|
284
259
|
};
|
|
285
260
|
}
|
|
261
|
+
|
|
262
|
+
// src/pdf/image-regions.ts
|
|
263
|
+
|
|
264
|
+
function multiplyTransform(m, t) {
|
|
265
|
+
return [
|
|
266
|
+
m[0] * t[0] + m[2] * t[1],
|
|
267
|
+
m[1] * t[0] + m[3] * t[1],
|
|
268
|
+
m[0] * t[2] + m[2] * t[3],
|
|
269
|
+
m[1] * t[2] + m[3] * t[3],
|
|
270
|
+
m[0] * t[4] + m[2] * t[5] + m[4],
|
|
271
|
+
m[1] * t[4] + m[3] * t[5] + m[5]
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
function extractImageRegions(fnArray, argsArray) {
|
|
275
|
+
const regions = [];
|
|
276
|
+
let ctm = [1, 0, 0, 1, 0, 0];
|
|
277
|
+
const stack = [];
|
|
278
|
+
for (let i = 0; i < fnArray.length; i++) {
|
|
279
|
+
const op = fnArray[i];
|
|
280
|
+
switch (op) {
|
|
281
|
+
case _pdfmjs.OPS.save:
|
|
282
|
+
stack.push(ctm);
|
|
283
|
+
break;
|
|
284
|
+
case _pdfmjs.OPS.restore:
|
|
285
|
+
ctm = stack.pop() || [1, 0, 0, 1, 0, 0];
|
|
286
|
+
break;
|
|
287
|
+
case _pdfmjs.OPS.transform: {
|
|
288
|
+
const t = argsArray[i];
|
|
289
|
+
if (Array.isArray(t) && t.length >= 6) ctm = multiplyTransform(ctm, t);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case _pdfmjs.OPS.paintImageXObject:
|
|
293
|
+
case _pdfmjs.OPS.paintInlineImageXObject:
|
|
294
|
+
case _pdfmjs.OPS.paintImageMaskXObject:
|
|
295
|
+
case _pdfmjs.OPS.paintImageXObjectRepeat: {
|
|
296
|
+
const corners = [[0, 0], [1, 0], [0, 1], [1, 1]];
|
|
297
|
+
let x1 = Infinity, y1 = Infinity, x2 = -Infinity, y2 = -Infinity;
|
|
298
|
+
for (const [u, v] of corners) {
|
|
299
|
+
const x = ctm[0] * u + ctm[2] * v + ctm[4];
|
|
300
|
+
const y = ctm[1] * u + ctm[3] * v + ctm[5];
|
|
301
|
+
if (x < x1) x1 = x;
|
|
302
|
+
if (x > x2) x2 = x;
|
|
303
|
+
if (y < y1) y1 = y;
|
|
304
|
+
if (y > y2) y2 = y;
|
|
305
|
+
}
|
|
306
|
+
if (x2 - x1 > 0 && y2 - y1 > 0) regions.push({ x1, y1, x2, y2 });
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return regions;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/pdf/line-types.ts
|
|
315
|
+
var VERTEX_MERGE_FACTOR = 4;
|
|
316
|
+
|
|
317
|
+
// src/pdf/table-grid.ts
|
|
318
|
+
var CONNECT_TOL = 5;
|
|
319
|
+
var MIN_COL_WIDTH = 15;
|
|
320
|
+
var MIN_ROW_HEIGHT = 6;
|
|
321
|
+
var MIN_COORD_MERGE_TOL = 8;
|
|
286
322
|
function buildVertices(horizontals, verticals) {
|
|
287
323
|
const vertices = [];
|
|
288
324
|
const tol = CONNECT_TOL;
|
|
@@ -494,6 +530,8 @@ function linesIntersect(a, b) {
|
|
|
494
530
|
const tol = CONNECT_TOL;
|
|
495
531
|
return v.x1 >= h.x1 - tol && v.x1 <= h.x2 + tol && h.y1 >= v.y1 - tol && h.y1 <= v.y2 + tol;
|
|
496
532
|
}
|
|
533
|
+
|
|
534
|
+
// src/pdf/cell-extract.ts
|
|
497
535
|
function extractCells(grid, horizontals, verticals) {
|
|
498
536
|
const { rowYs, colXs } = grid;
|
|
499
537
|
const numRows = rowYs.length - 1;
|
|
@@ -591,6 +629,9 @@ function hasHorizontalLine(horizontals, y, leftX, rightX, vertexRadius) {
|
|
|
591
629
|
}
|
|
592
630
|
return false;
|
|
593
631
|
}
|
|
632
|
+
|
|
633
|
+
// src/pdf/cell-text.ts
|
|
634
|
+
var CELL_PADDING = 2;
|
|
594
635
|
var SPACE_GAP_RATIO = 0.17;
|
|
595
636
|
function spaceGapThreshold(fontSize) {
|
|
596
637
|
return Math.max(fontSize * SPACE_GAP_RATIO, 1);
|
|
@@ -719,6 +760,28 @@ function markEvenRun(items, result, start, end) {
|
|
|
719
760
|
}
|
|
720
761
|
}
|
|
721
762
|
}
|
|
763
|
+
function mergeCellTextLines(textLines) {
|
|
764
|
+
if (textLines.length <= 1) return textLines[0] || "";
|
|
765
|
+
const merged = [textLines[0]];
|
|
766
|
+
for (let i = 1; i < textLines.length; i++) {
|
|
767
|
+
const prev = merged[merged.length - 1];
|
|
768
|
+
const curr = textLines[i];
|
|
769
|
+
if (/[가-힣]$/.test(prev) && /^[가-힣]+$/.test(curr) && curr.length <= 8 && !curr.includes(" ")) {
|
|
770
|
+
merged[merged.length - 1] = prev + curr;
|
|
771
|
+
} else if (curr.trim().length <= 3 && /^[)\]%}]/.test(curr.trim())) {
|
|
772
|
+
merged[merged.length - 1] = prev + curr.trim();
|
|
773
|
+
} else if (/[,(]$/.test(prev.trim()) && curr.trim().length <= 15) {
|
|
774
|
+
merged[merged.length - 1] = prev + curr.trim();
|
|
775
|
+
} else if (/[\d,]$/.test(prev) && /^[\d,]+[)\]]?$/.test(curr.trim()) && curr.trim().length <= 10) {
|
|
776
|
+
merged[merged.length - 1] = prev + curr.trim();
|
|
777
|
+
} else {
|
|
778
|
+
merged.push(curr);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return merged.join("\n");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/pdf/undersegmented.ts
|
|
722
785
|
var MAX_UNDERSEGMENTED_ROWS = 2;
|
|
723
786
|
var MIN_UNDERSEGMENTED_COLUMNS = 3;
|
|
724
787
|
var MIN_UNDERSEGMENTED_TEXT_LINES = 8;
|
|
@@ -832,977 +895,822 @@ function normalizeUndersegmentedTable(originalCells, colXs, items) {
|
|
|
832
895
|
if (countNonEmptyCols(rebuilt, numCols) < countNonEmptyCols(originalCells, numCols)) return null;
|
|
833
896
|
return rebuilt;
|
|
834
897
|
}
|
|
835
|
-
function mergeCellTextLines(textLines) {
|
|
836
|
-
if (textLines.length <= 1) return textLines[0] || "";
|
|
837
|
-
const merged = [textLines[0]];
|
|
838
|
-
for (let i = 1; i < textLines.length; i++) {
|
|
839
|
-
const prev = merged[merged.length - 1];
|
|
840
|
-
const curr = textLines[i];
|
|
841
|
-
if (/[가-힣]$/.test(prev) && /^[가-힣]+$/.test(curr) && curr.length <= 8 && !curr.includes(" ")) {
|
|
842
|
-
merged[merged.length - 1] = prev + curr;
|
|
843
|
-
} else if (curr.trim().length <= 3 && /^[)\]%}]/.test(curr.trim())) {
|
|
844
|
-
merged[merged.length - 1] = prev + curr.trim();
|
|
845
|
-
} else if (/[,(]$/.test(prev.trim()) && curr.trim().length <= 15) {
|
|
846
|
-
merged[merged.length - 1] = prev + curr.trim();
|
|
847
|
-
} else if (/[\d,]$/.test(prev) && /^[\d,]+[)\]]?$/.test(curr.trim()) && curr.trim().length <= 10) {
|
|
848
|
-
merged[merged.length - 1] = prev + curr.trim();
|
|
849
|
-
} else {
|
|
850
|
-
merged.push(curr);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return merged.join("\n");
|
|
854
|
-
}
|
|
855
898
|
|
|
856
|
-
// src/pdf/
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const headerResult = detectHeaderRow(rows);
|
|
871
|
-
if (headerResult) {
|
|
872
|
-
const { columns, headerIdx } = headerResult;
|
|
873
|
-
const headerRow = rows[headerIdx];
|
|
874
|
-
const headerItems = [...headerRow.items].sort((a, b) => a.x - b.x);
|
|
875
|
-
const headerAndBelow = rows.slice(headerIdx);
|
|
876
|
-
const mergedRows = mergeMultiLineRows(headerAndBelow, columns);
|
|
877
|
-
const tableRegions = findTableRegionsByHeader(mergedRows, columns, headerItems);
|
|
878
|
-
for (const region of tableRegions) {
|
|
879
|
-
const table = buildClusterTable(region.rows, columns, pageNum);
|
|
880
|
-
if (table) {
|
|
881
|
-
expandUsedItems(table.usedItems, originMap);
|
|
882
|
-
results.push(table);
|
|
883
|
-
}
|
|
899
|
+
// src/pdf/quality.ts
|
|
900
|
+
function computePageQuality(page, text) {
|
|
901
|
+
let total = 0;
|
|
902
|
+
let hangul = 0;
|
|
903
|
+
let control = 0;
|
|
904
|
+
let replacement = 0;
|
|
905
|
+
let pua = 0;
|
|
906
|
+
for (let i = 0; i < text.length; i++) {
|
|
907
|
+
const code = text.charCodeAt(i);
|
|
908
|
+
if (code === 32 || code === 9 || code === 10 || code === 13) continue;
|
|
909
|
+
total++;
|
|
910
|
+
if (code < 32 || code === 127 || code >= 128 && code <= 159) {
|
|
911
|
+
control++;
|
|
912
|
+
continue;
|
|
884
913
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
expandUsedItems(table.usedItems, originMap);
|
|
897
|
-
results.push(table);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
914
|
+
if (code === 65533) {
|
|
915
|
+
replacement++;
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (code >= 44032 && code <= 55203) {
|
|
919
|
+
hangul++;
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (code >= 57344 && code <= 63743 || code >= 56192 && code <= 56319) {
|
|
923
|
+
pua++;
|
|
924
|
+
continue;
|
|
901
925
|
}
|
|
902
926
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
if (runEnd - i >= 3) {
|
|
923
|
-
const gaps = [];
|
|
924
|
-
for (let g2 = i + 1; g2 < runEnd; g2++) {
|
|
925
|
-
gaps.push(sorted[g2].x - (sorted[g2 - 1].x + sorted[g2 - 1].w));
|
|
926
|
-
}
|
|
927
|
-
let minG = Infinity, maxG = -Infinity;
|
|
928
|
-
for (const g2 of gaps) {
|
|
929
|
-
if (g2 < minG) minG = g2;
|
|
930
|
-
if (g2 > maxG) maxG = g2;
|
|
931
|
-
}
|
|
932
|
-
if (minG > 0 && maxG / minG <= 3) {
|
|
933
|
-
const run = sorted.slice(i, runEnd);
|
|
934
|
-
const text = run.map((r) => r.text).join("");
|
|
935
|
-
const first = run[0], last = run[runEnd - i - 1];
|
|
936
|
-
const item = {
|
|
937
|
-
text,
|
|
938
|
-
x: first.x,
|
|
939
|
-
y: first.y,
|
|
940
|
-
w: last.x + last.w - first.x,
|
|
941
|
-
h: first.h,
|
|
942
|
-
fontSize: first.fontSize,
|
|
943
|
-
fontName: first.fontName
|
|
944
|
-
};
|
|
945
|
-
originMap.set(item, run);
|
|
946
|
-
merged.push(item);
|
|
947
|
-
i = runEnd;
|
|
948
|
-
continue;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
merged.push(sorted[i]);
|
|
953
|
-
i++;
|
|
954
|
-
}
|
|
927
|
+
const denom = total || 1;
|
|
928
|
+
const puaRatio = pua / denom;
|
|
929
|
+
const controlCharRatio = control / denom;
|
|
930
|
+
const replacementCharRatio = replacement / denom;
|
|
931
|
+
let needsOcr = false;
|
|
932
|
+
let ocrReason;
|
|
933
|
+
if (total < LOW_TEXT_THRESHOLD) {
|
|
934
|
+
needsOcr = true;
|
|
935
|
+
ocrReason = "low_text";
|
|
936
|
+
} else if (puaRatio >= HIGH_PUA_THRESHOLD) {
|
|
937
|
+
needsOcr = true;
|
|
938
|
+
ocrReason = "high_pua";
|
|
939
|
+
} else if (controlCharRatio >= HIGH_CONTROL_THRESHOLD) {
|
|
940
|
+
needsOcr = true;
|
|
941
|
+
ocrReason = "high_control";
|
|
942
|
+
} else if (replacementCharRatio >= HIGH_REPLACEMENT_THRESHOLD) {
|
|
943
|
+
needsOcr = true;
|
|
944
|
+
ocrReason = "high_replacement";
|
|
955
945
|
}
|
|
956
|
-
return {
|
|
946
|
+
return {
|
|
947
|
+
page,
|
|
948
|
+
textChars: total,
|
|
949
|
+
hangulRatio: hangul / denom,
|
|
950
|
+
controlCharRatio,
|
|
951
|
+
replacementCharRatio,
|
|
952
|
+
puaRatio,
|
|
953
|
+
needsOcr,
|
|
954
|
+
ocrReason
|
|
955
|
+
};
|
|
957
956
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
957
|
+
var LOW_TEXT_THRESHOLD = 20;
|
|
958
|
+
var HIGH_PUA_THRESHOLD = 0.2;
|
|
959
|
+
var HIGH_CONTROL_THRESHOLD = 0.05;
|
|
960
|
+
var HIGH_REPLACEMENT_THRESHOLD = 0.05;
|
|
961
|
+
var DOC_NEEDS_OCR_PAGE_RATIO = 0.3;
|
|
962
|
+
function stripControlChars(text) {
|
|
963
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
965
964
|
}
|
|
966
|
-
function
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
965
|
+
function summarizeDocumentQuality(pages) {
|
|
966
|
+
if (pages.length === 0) {
|
|
967
|
+
return {
|
|
968
|
+
totalPages: 0,
|
|
969
|
+
totalTextChars: 0,
|
|
970
|
+
avgHangulRatio: 0,
|
|
971
|
+
avgControlCharRatio: 0,
|
|
972
|
+
avgReplacementCharRatio: 0,
|
|
973
|
+
avgPuaRatio: 0,
|
|
974
|
+
lowTextPageCount: 0,
|
|
975
|
+
highPuaPageCount: 0,
|
|
976
|
+
needsOcr: false,
|
|
977
|
+
ocrCandidatePages: []
|
|
978
|
+
};
|
|
974
979
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
if (!hasLargeGap) continue;
|
|
996
|
-
const columns = sorted.map((item) => ({ x: item.x, count: 0 }));
|
|
997
|
-
let matchCount = 0;
|
|
998
|
-
for (let j = ri + 1; j < rows.length && matchCount < MIN_ROWS + 2; j++) {
|
|
999
|
-
const matched = countMatchedColumnsRange(rows[j], columns, sorted);
|
|
1000
|
-
if (matched >= MIN_COLS) matchCount++;
|
|
1001
|
-
}
|
|
1002
|
-
if (matchCount < MIN_ROWS) continue;
|
|
1003
|
-
return { columns, headerIdx: ri };
|
|
980
|
+
let textChars = 0;
|
|
981
|
+
let hangul = 0;
|
|
982
|
+
let control = 0;
|
|
983
|
+
let replacement = 0;
|
|
984
|
+
let pua = 0;
|
|
985
|
+
let lowText = 0;
|
|
986
|
+
let highPua = 0;
|
|
987
|
+
const ocrCandidatePages = [];
|
|
988
|
+
for (const p of pages) {
|
|
989
|
+
textChars += p.textChars;
|
|
990
|
+
hangul += p.hangulRatio;
|
|
991
|
+
control += p.controlCharRatio;
|
|
992
|
+
replacement += p.replacementCharRatio;
|
|
993
|
+
pua += p.puaRatio;
|
|
994
|
+
if (p.textChars < LOW_TEXT_THRESHOLD) lowText++;
|
|
995
|
+
if (p.puaRatio >= HIGH_PUA_THRESHOLD) highPua++;
|
|
996
|
+
if (p.needsOcr) ocrCandidatePages.push(p.page);
|
|
1004
997
|
}
|
|
1005
|
-
|
|
998
|
+
const n = pages.length;
|
|
999
|
+
return {
|
|
1000
|
+
totalPages: n,
|
|
1001
|
+
totalTextChars: textChars,
|
|
1002
|
+
avgHangulRatio: hangul / n,
|
|
1003
|
+
avgControlCharRatio: control / n,
|
|
1004
|
+
avgReplacementCharRatio: replacement / n,
|
|
1005
|
+
avgPuaRatio: pua / n,
|
|
1006
|
+
lowTextPageCount: lowText,
|
|
1007
|
+
highPuaPageCount: highPua,
|
|
1008
|
+
needsOcr: ocrCandidatePages.length / n >= DOC_NEEDS_OCR_PAGE_RATIO,
|
|
1009
|
+
ocrCandidatePages
|
|
1010
|
+
};
|
|
1006
1011
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
} else {
|
|
1022
|
-
result.push(curr);
|
|
1012
|
+
|
|
1013
|
+
// src/pdf/text-line.ts
|
|
1014
|
+
function filterHiddenText(items, pageWidth, pageHeight) {
|
|
1015
|
+
let hiddenCount = 0;
|
|
1016
|
+
const visible = [];
|
|
1017
|
+
for (const item of items) {
|
|
1018
|
+
if (item.isHidden) {
|
|
1019
|
+
hiddenCount++;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
const margin = Math.max(pageWidth, pageHeight) * 0.1;
|
|
1023
|
+
if (item.x < -margin || item.x > pageWidth + margin || item.y < -margin || item.y > pageHeight + margin) {
|
|
1024
|
+
hiddenCount++;
|
|
1025
|
+
continue;
|
|
1023
1026
|
}
|
|
1027
|
+
visible.push(item);
|
|
1024
1028
|
}
|
|
1025
|
-
return
|
|
1029
|
+
return { visible, hiddenCount };
|
|
1026
1030
|
}
|
|
1027
|
-
function
|
|
1028
|
-
|
|
1031
|
+
function collapseEvenSpacing(text) {
|
|
1032
|
+
const tokens = text.split(" ");
|
|
1033
|
+
const singleCharCount = tokens.filter((t) => t.length === 1).length;
|
|
1034
|
+
if (tokens.length >= 3 && singleCharCount / tokens.length >= 0.7) {
|
|
1035
|
+
return tokens.join("");
|
|
1036
|
+
}
|
|
1037
|
+
return text.replace(
|
|
1038
|
+
/(?<![가-힣])[가-힣](?: [가-힣\d]){2,}(?![가-힣])/g,
|
|
1039
|
+
(match) => match.replace(/ /g, "")
|
|
1040
|
+
);
|
|
1029
1041
|
}
|
|
1030
|
-
function
|
|
1031
|
-
let
|
|
1032
|
-
for (const i of
|
|
1033
|
-
|
|
1034
|
-
if (i.y <
|
|
1035
|
-
if (i.
|
|
1042
|
+
function computeBBox(items, pageNum) {
|
|
1043
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1044
|
+
for (const i of items) {
|
|
1045
|
+
if (i.x < minX) minX = i.x;
|
|
1046
|
+
if (i.y < minY) minY = i.y;
|
|
1047
|
+
if (i.x + i.w > maxX) maxX = i.x + i.w;
|
|
1048
|
+
const effectiveH = i.h > 0 ? i.h : i.fontSize;
|
|
1049
|
+
if (i.y + effectiveH > maxY) maxY = i.y + effectiveH;
|
|
1036
1050
|
}
|
|
1037
|
-
return {
|
|
1051
|
+
return { page: pageNum, x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
1038
1052
|
}
|
|
1039
|
-
function
|
|
1040
|
-
if (
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
result[result.length - 1] = {
|
|
1051
|
-
y: prev.y,
|
|
1052
|
-
items: [...prev.items, ...curr.items]
|
|
1053
|
-
};
|
|
1054
|
-
} else {
|
|
1055
|
-
result.push(curr);
|
|
1053
|
+
function dominantStyle(items) {
|
|
1054
|
+
if (items.length === 0) return void 0;
|
|
1055
|
+
const freq = /* @__PURE__ */ new Map();
|
|
1056
|
+
let maxCount = 0, dominantSize = 0;
|
|
1057
|
+
for (const i of items) {
|
|
1058
|
+
if (i.fontSize <= 0) continue;
|
|
1059
|
+
const count = (freq.get(i.fontSize) || 0) + 1;
|
|
1060
|
+
freq.set(i.fontSize, count);
|
|
1061
|
+
if (count > maxCount) {
|
|
1062
|
+
maxCount = count;
|
|
1063
|
+
dominantSize = i.fontSize;
|
|
1056
1064
|
}
|
|
1057
1065
|
}
|
|
1058
|
-
return
|
|
1066
|
+
if (dominantSize === 0) return void 0;
|
|
1067
|
+
const fontName = _optionalChain([items, 'access', _4 => _4.find, 'call', _5 => _5((i) => i.fontSize === dominantSize), 'optionalAccess', _6 => _6.fontName]) || void 0;
|
|
1068
|
+
return { fontSize: dominantSize, fontName };
|
|
1059
1069
|
}
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
rows.push({ y: curY, items: curItems });
|
|
1071
|
-
curItems = [sorted[i]];
|
|
1072
|
-
curY = sorted[i].y;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
if (curItems.length > 0) rows.push({ y: curY, items: curItems });
|
|
1076
|
-
return rows;
|
|
1077
|
-
}
|
|
1078
|
-
function hasSuspiciousGaps(row) {
|
|
1079
|
-
if (row.items.length < 2) return false;
|
|
1080
|
-
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
1081
|
-
if (sorted.length === 2 && sorted[1].text.length > 20) return false;
|
|
1082
|
-
const avgFontSize = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
|
|
1083
|
-
const minGap = Math.max(avgFontSize * MIN_GAP_FACTOR, MIN_GAP_ABSOLUTE);
|
|
1084
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
1085
|
-
const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
|
|
1086
|
-
if (gap >= minGap) return true;
|
|
1087
|
-
}
|
|
1088
|
-
return false;
|
|
1089
|
-
}
|
|
1090
|
-
function extractColumnClusters(rows) {
|
|
1091
|
-
const allX = [];
|
|
1092
|
-
for (const row of rows) {
|
|
1093
|
-
for (const item of row.items) allX.push(item.x);
|
|
1094
|
-
}
|
|
1095
|
-
if (allX.length === 0) return [];
|
|
1096
|
-
allX.sort((a, b) => a - b);
|
|
1097
|
-
const clusters = [];
|
|
1098
|
-
let clusterStart = 0;
|
|
1099
|
-
for (let i = 1; i <= allX.length; i++) {
|
|
1100
|
-
if (i === allX.length || allX[i] - allX[i - 1] > COL_CLUSTER_TOL) {
|
|
1101
|
-
const slice = allX.slice(clusterStart, i);
|
|
1102
|
-
const avg = Math.round(slice.reduce((s, v) => s + v, 0) / slice.length);
|
|
1103
|
-
clusters.push({ x: avg, count: slice.length });
|
|
1104
|
-
clusterStart = i;
|
|
1070
|
+
function normalizeItems(rawItems) {
|
|
1071
|
+
const items = [];
|
|
1072
|
+
const spacePositions = [];
|
|
1073
|
+
for (const i of rawItems) {
|
|
1074
|
+
if (typeof i.str !== "string") continue;
|
|
1075
|
+
const x = Math.round(i.transform[4]);
|
|
1076
|
+
const y = Math.round(i.transform[5]);
|
|
1077
|
+
if (!i.str.trim()) {
|
|
1078
|
+
spacePositions.push({ x, y });
|
|
1079
|
+
continue;
|
|
1105
1080
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
const matchedCols = countMatchedColumnsRange(row, columns, headerItems);
|
|
1116
|
-
if (matchedCols >= MIN_COLS) {
|
|
1117
|
-
currentRegion.push(row);
|
|
1118
|
-
missStreak = 0;
|
|
1119
|
-
} else if (currentRegion.length > 0 && (row.items.length <= 2 || missStreak === 0)) {
|
|
1120
|
-
currentRegion.push(row);
|
|
1121
|
-
missStreak++;
|
|
1122
|
-
} else {
|
|
1123
|
-
while (currentRegion.length > 0) {
|
|
1124
|
-
const last = currentRegion[currentRegion.length - 1];
|
|
1125
|
-
if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
|
|
1126
|
-
currentRegion.pop();
|
|
1127
|
-
}
|
|
1128
|
-
if (currentRegion.length >= MIN_ROWS) {
|
|
1129
|
-
regions.push({ rows: [...currentRegion] });
|
|
1130
|
-
}
|
|
1131
|
-
currentRegion = [];
|
|
1132
|
-
missStreak = 0;
|
|
1081
|
+
const scaleX = Math.hypot(i.transform[0], i.transform[1]);
|
|
1082
|
+
const scaleY = Math.hypot(i.transform[2], i.transform[3]);
|
|
1083
|
+
const fontSize = Math.round(Math.max(scaleY, scaleX));
|
|
1084
|
+
const w = Math.round(i.width);
|
|
1085
|
+
const h = Math.round(i.height);
|
|
1086
|
+
const isHidden = fontSize === 0 || i.width === 0 && i.str.trim().length > 0;
|
|
1087
|
+
let text = i.str.trim();
|
|
1088
|
+
if (/^[\d\s\-().·,☎]+$/.test(text) && /\d/.test(text) && / /.test(text)) {
|
|
1089
|
+
text = text.replace(/ /g, "");
|
|
1133
1090
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
currentRegion.pop();
|
|
1139
|
-
}
|
|
1140
|
-
if (currentRegion.length >= MIN_ROWS) {
|
|
1141
|
-
regions.push({ rows: currentRegion });
|
|
1142
|
-
}
|
|
1143
|
-
return regions;
|
|
1144
|
-
}
|
|
1145
|
-
function findTableRegions(allRows, columns) {
|
|
1146
|
-
const regions = [];
|
|
1147
|
-
let currentRegion = [];
|
|
1148
|
-
for (const row of allRows) {
|
|
1149
|
-
const matchedCols = countMatchedColumns(row, columns);
|
|
1150
|
-
if (matchedCols >= MIN_COLS) {
|
|
1151
|
-
currentRegion.push(row);
|
|
1152
|
-
} else if (row.items.length === 1) {
|
|
1153
|
-
if (currentRegion.length > 0) {
|
|
1154
|
-
currentRegion.push(row);
|
|
1091
|
+
const split = splitEvenSpacedItem(text, x, w, fontSize);
|
|
1092
|
+
if (split) {
|
|
1093
|
+
for (const s of split) {
|
|
1094
|
+
items.push({ text: s.text, x: s.x, y, w: s.w, h, fontSize, fontName: i.fontName || "", isHidden });
|
|
1155
1095
|
}
|
|
1156
1096
|
} else {
|
|
1157
|
-
|
|
1158
|
-
regions.push({ rows: [...currentRegion] });
|
|
1159
|
-
}
|
|
1160
|
-
currentRegion = [];
|
|
1097
|
+
items.push({ text, x, y, w, h, fontSize, fontName: i.fontName || "", isHidden });
|
|
1161
1098
|
}
|
|
1162
1099
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
if (Math.abs(item.x - columns[ci].x) <= COL_CLUSTER_TOL * 2) {
|
|
1173
|
-
matched.add(ci);
|
|
1100
|
+
const sorted = items.sort((a, b) => b.y - a.y || a.x - b.x);
|
|
1101
|
+
const deduped = [];
|
|
1102
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1103
|
+
let isDup = false;
|
|
1104
|
+
for (let j = deduped.length - 1; j >= 0; j--) {
|
|
1105
|
+
const prev = deduped[j];
|
|
1106
|
+
if (prev.y - sorted[i].y > 3) break;
|
|
1107
|
+
if (Math.abs(prev.y - sorted[i].y) <= 3 && prev.text === sorted[i].text && Math.abs(prev.x - sorted[i].x) <= 3) {
|
|
1108
|
+
isDup = true;
|
|
1174
1109
|
break;
|
|
1175
1110
|
}
|
|
1176
1111
|
}
|
|
1112
|
+
if (!isDup) deduped.push(sorted[i]);
|
|
1177
1113
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const matched = /* @__PURE__ */ new Set();
|
|
1188
|
-
for (const item of row.items) {
|
|
1189
|
-
for (let ci = 0; ci < boundaries.length; ci++) {
|
|
1190
|
-
if (item.x >= boundaries[ci].left && item.x < boundaries[ci].right) {
|
|
1191
|
-
matched.add(ci);
|
|
1192
|
-
break;
|
|
1114
|
+
if (spacePositions.length > 0) {
|
|
1115
|
+
for (const sp of spacePositions) {
|
|
1116
|
+
let nearest = null;
|
|
1117
|
+
for (const item of deduped) {
|
|
1118
|
+
if (Math.abs(sp.y - item.y) > 3) continue;
|
|
1119
|
+
const dist = item.x - sp.x;
|
|
1120
|
+
if (dist >= -1 && dist <= 20 && (!nearest || item.x < nearest.x)) {
|
|
1121
|
+
nearest = item;
|
|
1122
|
+
}
|
|
1193
1123
|
}
|
|
1124
|
+
if (nearest) nearest.hasSpaceBefore = true;
|
|
1194
1125
|
}
|
|
1195
1126
|
}
|
|
1196
|
-
return
|
|
1127
|
+
return deduped;
|
|
1197
1128
|
}
|
|
1198
|
-
function
|
|
1129
|
+
function splitEvenSpacedItem(text, itemX, itemW, fontSize) {
|
|
1130
|
+
if (!/^[가-힣\d](?: [가-힣\d]){2,}$/.test(text)) return null;
|
|
1131
|
+
const chars = text.split(" ");
|
|
1132
|
+
if (chars.length < 3) return null;
|
|
1133
|
+
const charW = itemW / chars.length;
|
|
1134
|
+
if (charW > fontSize * 2) return null;
|
|
1135
|
+
return chars.map((ch, idx) => ({
|
|
1136
|
+
text: ch,
|
|
1137
|
+
x: Math.round(itemX + idx * charW),
|
|
1138
|
+
w: Math.round(charW * 0.8)
|
|
1139
|
+
// 실제 글자 폭은 간격보다 좁음
|
|
1140
|
+
}));
|
|
1141
|
+
}
|
|
1142
|
+
function groupByY(items) {
|
|
1199
1143
|
if (items.length === 0) return [];
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
for (let i = 1; i <
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const groups = [];
|
|
1211
|
-
let start = 0;
|
|
1212
|
-
for (const gap of significantGaps) {
|
|
1213
|
-
groups.push(sorted.slice(start, gap.idx));
|
|
1214
|
-
start = gap.idx;
|
|
1144
|
+
const lines = [];
|
|
1145
|
+
let curY = items[0].y;
|
|
1146
|
+
let curLine = [items[0]];
|
|
1147
|
+
for (let i = 1; i < items.length; i++) {
|
|
1148
|
+
if (Math.abs(items[i].y - curY) > 3) {
|
|
1149
|
+
lines.push(curLine);
|
|
1150
|
+
curLine = [];
|
|
1151
|
+
curY = items[i].y;
|
|
1152
|
+
}
|
|
1153
|
+
curLine.push(items[i]);
|
|
1215
1154
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1155
|
+
if (curLine.length > 0) lines.push(curLine);
|
|
1156
|
+
return lines;
|
|
1157
|
+
}
|
|
1158
|
+
function mergeSuperscriptLines(lines) {
|
|
1159
|
+
if (lines.length <= 1) return lines;
|
|
1160
|
+
const band = (line) => {
|
|
1161
|
+
let bottom = Infinity, top = -Infinity;
|
|
1162
|
+
for (const i of line) {
|
|
1163
|
+
const h = i.h > 0 ? i.h : i.fontSize;
|
|
1164
|
+
if (i.y < bottom) bottom = i.y;
|
|
1165
|
+
if (i.y + h > top) top = i.y + h;
|
|
1225
1166
|
}
|
|
1226
|
-
return
|
|
1227
|
-
}
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1167
|
+
return { bottom, top, height: top - bottom };
|
|
1168
|
+
};
|
|
1169
|
+
const isFrag = (line) => line.length <= 3 && line.every((i) => i.text.trim().length <= 8);
|
|
1170
|
+
const result = [lines[0]];
|
|
1171
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1172
|
+
const prev = result[result.length - 1];
|
|
1173
|
+
const curr = lines[i];
|
|
1174
|
+
const a = band(prev);
|
|
1175
|
+
const b = band(curr);
|
|
1176
|
+
const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
|
|
1177
|
+
const prevIsFrag = isFrag(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
|
|
1178
|
+
const currIsFrag = isFrag(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
|
|
1179
|
+
if (prevIsFrag || currIsFrag) {
|
|
1180
|
+
result[result.length - 1] = [...prev, ...curr];
|
|
1181
|
+
} else {
|
|
1182
|
+
result.push(curr);
|
|
1232
1183
|
}
|
|
1233
1184
|
}
|
|
1234
|
-
assignments.sort((a, b) => a.dist - b.dist);
|
|
1235
|
-
const assignedGroups = /* @__PURE__ */ new Set();
|
|
1236
|
-
for (const { gi, ci } of assignments) {
|
|
1237
|
-
if (assignedGroups.has(gi) || usedCols.has(ci)) continue;
|
|
1238
|
-
result.push({ col: ci, items: groups[gi] });
|
|
1239
|
-
assignedGroups.add(gi);
|
|
1240
|
-
usedCols.add(ci);
|
|
1241
|
-
}
|
|
1242
|
-
for (let gi = 0; gi < groups.length; gi++) {
|
|
1243
|
-
if (assignedGroups.has(gi)) continue;
|
|
1244
|
-
let bestCol = 0, bestDist = Infinity;
|
|
1245
|
-
for (let ci = 0; ci < numCols; ci++) {
|
|
1246
|
-
const d = Math.abs(groupCenters[gi] - colCenters[ci]);
|
|
1247
|
-
if (d < bestDist) {
|
|
1248
|
-
bestDist = d;
|
|
1249
|
-
bestCol = ci;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
result.push({ col: bestCol, items: groups[gi] });
|
|
1253
|
-
}
|
|
1254
1185
|
return result;
|
|
1255
1186
|
}
|
|
1256
|
-
function
|
|
1257
|
-
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
cells[r][0] = { text: row.items[0].text, colSpan: numCols, rowSpan: 1 };
|
|
1269
|
-
usedItems.add(row.items[0]);
|
|
1187
|
+
function mergeLineSimple(items) {
|
|
1188
|
+
if (items.length <= 1) return _optionalChain([items, 'access', _7 => _7[0], 'optionalAccess', _8 => _8.text]) || "";
|
|
1189
|
+
const sorted = [...items].sort((a, b) => a.x - b.x);
|
|
1190
|
+
const isEvenSpaced = detectEvenSpacedItems(sorted);
|
|
1191
|
+
let result = sorted[0].text;
|
|
1192
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1193
|
+
const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
|
|
1194
|
+
const avgFs = (sorted[i].fontSize + sorted[i - 1].fontSize) / 2;
|
|
1195
|
+
const tabThreshold = Math.max(avgFs * 2, 30);
|
|
1196
|
+
if (gap > tabThreshold) {
|
|
1197
|
+
result += " ";
|
|
1198
|
+
result += sorted[i].text;
|
|
1270
1199
|
continue;
|
|
1271
1200
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
const existing = cells[r][col].text;
|
|
1276
|
-
cells[r][col].text = existing ? existing + " " + text : text;
|
|
1277
|
-
for (const item of items) usedItems.add(item);
|
|
1201
|
+
if (isEvenSpaced[i]) {
|
|
1202
|
+
result += sorted[i].text;
|
|
1203
|
+
continue;
|
|
1278
1204
|
}
|
|
1205
|
+
if (sorted[i].hasSpaceBefore && gap >= avgFs * 0.05) {
|
|
1206
|
+
result += " ";
|
|
1207
|
+
result += sorted[i].text;
|
|
1208
|
+
continue;
|
|
1209
|
+
}
|
|
1210
|
+
if (/[□■○●▶◆◇ㅇ]$/.test(sorted[i - 1].text) && /^[가-힣]/.test(sorted[i].text) && gap > 1) {
|
|
1211
|
+
result += " ";
|
|
1212
|
+
result += sorted[i].text;
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (gap > spaceGapThreshold(avgFs)) result += " ";
|
|
1216
|
+
result += sorted[i].text;
|
|
1279
1217
|
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1218
|
+
return result;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/pdf/cluster-detector.ts
|
|
1222
|
+
var Y_TOL = 3;
|
|
1223
|
+
var COL_CLUSTER_TOL = 15;
|
|
1224
|
+
var MIN_ROWS = 3;
|
|
1225
|
+
var MIN_COLS = 2;
|
|
1226
|
+
var MIN_GAP_FACTOR = 2;
|
|
1227
|
+
var MIN_GAP_ABSOLUTE = 20;
|
|
1228
|
+
var MIN_COL_FILL_RATIO = 0.4;
|
|
1229
|
+
function detectClusterTables(items, pageNum) {
|
|
1230
|
+
if (items.length < MIN_ROWS * MIN_COLS) return [];
|
|
1231
|
+
const { merged, originMap } = mergeEvenSpacedClusters(items);
|
|
1232
|
+
const rows = mergeOverlappingRows(groupByBaseline(merged));
|
|
1233
|
+
if (rows.length < MIN_ROWS) return [];
|
|
1234
|
+
const results = [];
|
|
1235
|
+
const headerResult = detectHeaderRow(rows);
|
|
1236
|
+
if (headerResult) {
|
|
1237
|
+
const { columns, headerIdx } = headerResult;
|
|
1238
|
+
const headerRow = rows[headerIdx];
|
|
1239
|
+
const headerItems = [...headerRow.items].sort((a, b) => a.x - b.x);
|
|
1240
|
+
const headerAndBelow = rows.slice(headerIdx);
|
|
1241
|
+
const mergedRows = mergeMultiLineRows(headerAndBelow, columns);
|
|
1242
|
+
const tableRegions = findTableRegionsByHeader(mergedRows, columns, headerItems);
|
|
1243
|
+
for (const region of tableRegions) {
|
|
1244
|
+
const table = buildClusterTable(region.rows, columns, pageNum);
|
|
1245
|
+
if (table) {
|
|
1246
|
+
expandUsedItems(table.usedItems, originMap);
|
|
1247
|
+
results.push(table);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1288
1250
|
}
|
|
1289
|
-
|
|
1290
|
-
const
|
|
1291
|
-
if (
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1251
|
+
if (results.length === 0) {
|
|
1252
|
+
const suspiciousRows = rows.filter((row) => hasSuspiciousGaps(row));
|
|
1253
|
+
if (suspiciousRows.length >= MIN_ROWS) {
|
|
1254
|
+
const columns = extractColumnClusters(suspiciousRows);
|
|
1255
|
+
if (columns.length >= MIN_COLS) {
|
|
1256
|
+
const tableRegions = findTableRegions(rows, columns);
|
|
1257
|
+
for (const region of tableRegions) {
|
|
1258
|
+
const mergedRows = mergeMultiLineRows(region.rows, columns);
|
|
1259
|
+
const table = buildClusterTable(mergedRows, columns, pageNum);
|
|
1260
|
+
if (table) {
|
|
1261
|
+
expandUsedItems(table.usedItems, originMap);
|
|
1262
|
+
results.push(table);
|
|
1263
|
+
}
|
|
1301
1264
|
}
|
|
1302
|
-
for (let c = 0; c < numCols; c++) cells[r][c].text = "";
|
|
1303
|
-
break;
|
|
1304
1265
|
}
|
|
1305
1266
|
}
|
|
1306
1267
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1268
|
+
return results;
|
|
1269
|
+
}
|
|
1270
|
+
function mergeEvenSpacedClusters(items) {
|
|
1271
|
+
const originMap = /* @__PURE__ */ new Map();
|
|
1272
|
+
const rows = groupByBaseline(items);
|
|
1273
|
+
const merged = [];
|
|
1274
|
+
for (const row of rows) {
|
|
1275
|
+
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
1276
|
+
let i = 0;
|
|
1277
|
+
while (i < sorted.length) {
|
|
1278
|
+
if (/^[가-힣\d]$/.test(sorted[i].text)) {
|
|
1279
|
+
let runEnd = i + 1;
|
|
1280
|
+
while (runEnd < sorted.length && /^[가-힣\d]$/.test(sorted[runEnd].text)) {
|
|
1281
|
+
if (sorted[runEnd].hasSpaceBefore) break;
|
|
1282
|
+
const gap = sorted[runEnd].x - (sorted[runEnd - 1].x + sorted[runEnd - 1].w);
|
|
1283
|
+
const fs = sorted[runEnd].fontSize;
|
|
1284
|
+
if (gap < fs * 0.1 || gap > fs * 3) break;
|
|
1285
|
+
runEnd++;
|
|
1286
|
+
}
|
|
1287
|
+
if (runEnd - i >= 3) {
|
|
1288
|
+
const gaps = [];
|
|
1289
|
+
for (let g2 = i + 1; g2 < runEnd; g2++) {
|
|
1290
|
+
gaps.push(sorted[g2].x - (sorted[g2 - 1].x + sorted[g2 - 1].w));
|
|
1291
|
+
}
|
|
1292
|
+
let minG = Infinity, maxG = -Infinity;
|
|
1293
|
+
for (const g2 of gaps) {
|
|
1294
|
+
if (g2 < minG) minG = g2;
|
|
1295
|
+
if (g2 > maxG) maxG = g2;
|
|
1296
|
+
}
|
|
1297
|
+
if (minG > 0 && maxG / minG <= 3) {
|
|
1298
|
+
const run = sorted.slice(i, runEnd);
|
|
1299
|
+
const text = run.map((r) => r.text).join("");
|
|
1300
|
+
const first = run[0], last = run[runEnd - i - 1];
|
|
1301
|
+
const item = {
|
|
1302
|
+
text,
|
|
1303
|
+
x: first.x,
|
|
1304
|
+
y: first.y,
|
|
1305
|
+
w: last.x + last.w - first.x,
|
|
1306
|
+
h: first.h,
|
|
1307
|
+
fontSize: first.fontSize,
|
|
1308
|
+
fontName: first.fontName
|
|
1309
|
+
};
|
|
1310
|
+
originMap.set(item, run);
|
|
1311
|
+
merged.push(item);
|
|
1312
|
+
i = runEnd;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1318
1315
|
}
|
|
1319
|
-
for (let c = 0; c < numCols; c++) next[c].text = "";
|
|
1320
1316
|
}
|
|
1317
|
+
merged.push(sorted[i]);
|
|
1318
|
+
i++;
|
|
1321
1319
|
}
|
|
1322
1320
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1321
|
+
return { merged, originMap };
|
|
1322
|
+
}
|
|
1323
|
+
function expandUsedItems(usedItems, originMap) {
|
|
1324
|
+
const toAdd = [];
|
|
1325
|
+
for (const item of usedItems) {
|
|
1326
|
+
const origins = originMap.get(item);
|
|
1327
|
+
if (origins) for (const o of origins) toAdd.push(o);
|
|
1328
|
+
}
|
|
1329
|
+
for (const a of toAdd) usedItems.add(a);
|
|
1330
|
+
}
|
|
1331
|
+
function detectHeaderRow(rows) {
|
|
1332
1332
|
const allItems = rows.flatMap((r) => r.items);
|
|
1333
|
-
|
|
1333
|
+
if (allItems.length === 0) return null;
|
|
1334
|
+
let allMinX = Infinity, allMaxX = -Infinity;
|
|
1334
1335
|
for (const i of allItems) {
|
|
1335
|
-
if (i.x <
|
|
1336
|
-
|
|
1337
|
-
if (
|
|
1338
|
-
const h = i.h > 0 ? i.h : i.fontSize;
|
|
1339
|
-
if (i.y + h > maxY) maxY = i.y + h;
|
|
1336
|
+
if (i.x < allMinX) allMinX = i.x;
|
|
1337
|
+
const r = i.x + i.w;
|
|
1338
|
+
if (r > allMaxX) allMaxX = r;
|
|
1340
1339
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
control++;
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
if (code === 65533) {
|
|
1364
|
-
replacement++;
|
|
1365
|
-
continue;
|
|
1340
|
+
const pageSpan = allMaxX - allMinX;
|
|
1341
|
+
if (pageSpan <= 0) return null;
|
|
1342
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
1343
|
+
const row = rows[ri];
|
|
1344
|
+
if (row.items.length < MIN_COLS || row.items.length > 6) continue;
|
|
1345
|
+
if (row.items.some((i) => i.text.length > 8)) continue;
|
|
1346
|
+
if (!row.items.some((i) => /[가-힣]/.test(i.text))) continue;
|
|
1347
|
+
if (row.items.some((i) => /^[□■○●·※▶▷◆◇\-]/.test(i.text))) continue;
|
|
1348
|
+
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
1349
|
+
const xSpan = sorted[sorted.length - 1].x + sorted[sorted.length - 1].w - sorted[0].x;
|
|
1350
|
+
if (xSpan / pageSpan < 0.4) continue;
|
|
1351
|
+
const avgFs = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
|
|
1352
|
+
let hasLargeGap = false;
|
|
1353
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1354
|
+
const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
|
|
1355
|
+
if (gap >= avgFs * 2.5) {
|
|
1356
|
+
hasLargeGap = true;
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1366
1359
|
}
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
continue;
|
|
1360
|
+
if (!hasLargeGap) continue;
|
|
1361
|
+
const columns = sorted.map((item) => ({ x: item.x, count: 0 }));
|
|
1362
|
+
let matchCount = 0;
|
|
1363
|
+
for (let j = ri + 1; j < rows.length && matchCount < MIN_ROWS + 2; j++) {
|
|
1364
|
+
const matched = countMatchedColumnsRange(rows[j], columns, sorted);
|
|
1365
|
+
if (matched >= MIN_COLS) matchCount++;
|
|
1374
1366
|
}
|
|
1367
|
+
if (matchCount < MIN_ROWS) continue;
|
|
1368
|
+
return { columns, headerIdx: ri };
|
|
1375
1369
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
let
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
function mergeOverlappingRows(rows) {
|
|
1373
|
+
if (rows.length <= 1) return rows;
|
|
1374
|
+
const result = [rows[0]];
|
|
1375
|
+
for (let i = 1; i < rows.length; i++) {
|
|
1376
|
+
const prev = result[result.length - 1];
|
|
1377
|
+
const curr = rows[i];
|
|
1378
|
+
const a = rowBand(prev);
|
|
1379
|
+
const b = rowBand(curr);
|
|
1380
|
+
const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
|
|
1381
|
+
const prevIsFrag = isFragmentRow(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
|
|
1382
|
+
const currIsFrag = isFragmentRow(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
|
|
1383
|
+
if (prevIsFrag || currIsFrag) {
|
|
1384
|
+
const baseY = prevIsFrag ? curr.y : prev.y;
|
|
1385
|
+
result[result.length - 1] = { y: baseY, items: [...prev.items, ...curr.items] };
|
|
1386
|
+
} else {
|
|
1387
|
+
result.push(curr);
|
|
1388
|
+
}
|
|
1394
1389
|
}
|
|
1395
|
-
return
|
|
1396
|
-
page,
|
|
1397
|
-
textChars: total,
|
|
1398
|
-
hangulRatio: hangul / denom,
|
|
1399
|
-
controlCharRatio,
|
|
1400
|
-
replacementCharRatio,
|
|
1401
|
-
puaRatio,
|
|
1402
|
-
needsOcr,
|
|
1403
|
-
ocrReason
|
|
1404
|
-
};
|
|
1390
|
+
return result;
|
|
1405
1391
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
var HIGH_CONTROL_THRESHOLD = 0.05;
|
|
1409
|
-
var HIGH_REPLACEMENT_THRESHOLD = 0.05;
|
|
1410
|
-
var DOC_NEEDS_OCR_PAGE_RATIO = 0.3;
|
|
1411
|
-
function stripControlChars(text) {
|
|
1412
|
-
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
1392
|
+
function isFragmentRow(row) {
|
|
1393
|
+
return row.items.length <= 3 && row.items.every((i) => i.text.length <= 8);
|
|
1413
1394
|
}
|
|
1414
|
-
function
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
avgControlCharRatio: 0,
|
|
1421
|
-
avgReplacementCharRatio: 0,
|
|
1422
|
-
avgPuaRatio: 0,
|
|
1423
|
-
lowTextPageCount: 0,
|
|
1424
|
-
highPuaPageCount: 0,
|
|
1425
|
-
needsOcr: false,
|
|
1426
|
-
ocrCandidatePages: []
|
|
1427
|
-
};
|
|
1395
|
+
function rowBand(row) {
|
|
1396
|
+
let bottom = Infinity, top = -Infinity;
|
|
1397
|
+
for (const i of row.items) {
|
|
1398
|
+
const h = i.h > 0 ? i.h : i.fontSize;
|
|
1399
|
+
if (i.y < bottom) bottom = i.y;
|
|
1400
|
+
if (i.y + h > top) top = i.y + h;
|
|
1428
1401
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1402
|
+
return { bottom, top, height: top - bottom };
|
|
1403
|
+
}
|
|
1404
|
+
function mergeMultiLineRows(rows, columns) {
|
|
1405
|
+
if (rows.length <= 1) return rows;
|
|
1406
|
+
const result = [rows[0]];
|
|
1407
|
+
const allFontSizes = rows.flatMap((r) => r.items).map((i) => i.fontSize);
|
|
1408
|
+
const avgFontSize = allFontSizes.length > 0 ? allFontSizes.reduce((s, v) => s + v, 0) / allFontSizes.length : 12;
|
|
1409
|
+
for (let i = 1; i < rows.length; i++) {
|
|
1410
|
+
const prev = result[result.length - 1];
|
|
1411
|
+
const curr = rows[i];
|
|
1412
|
+
const yGap = Math.abs(prev.y - curr.y);
|
|
1413
|
+
const matchedCols = countMatchedColumns(curr, columns);
|
|
1414
|
+
if (yGap < avgFontSize * 1.8 && curr.items.length <= 2 && (matchedCols < MIN_COLS || curr.items.length === 1)) {
|
|
1415
|
+
result[result.length - 1] = {
|
|
1416
|
+
y: prev.y,
|
|
1417
|
+
items: [...prev.items, ...curr.items]
|
|
1418
|
+
};
|
|
1419
|
+
} else {
|
|
1420
|
+
result.push(curr);
|
|
1421
|
+
}
|
|
1446
1422
|
}
|
|
1447
|
-
|
|
1448
|
-
return {
|
|
1449
|
-
totalPages: n,
|
|
1450
|
-
totalTextChars: textChars,
|
|
1451
|
-
avgHangulRatio: hangul / n,
|
|
1452
|
-
avgControlCharRatio: control / n,
|
|
1453
|
-
avgReplacementCharRatio: replacement / n,
|
|
1454
|
-
avgPuaRatio: pua / n,
|
|
1455
|
-
lowTextPageCount: lowText,
|
|
1456
|
-
highPuaPageCount: highPua,
|
|
1457
|
-
needsOcr: ocrCandidatePages.length / n >= DOC_NEEDS_OCR_PAGE_RATIO,
|
|
1458
|
-
ocrCandidatePages
|
|
1459
|
-
};
|
|
1423
|
+
return result;
|
|
1460
1424
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1425
|
+
function groupByBaseline(items) {
|
|
1426
|
+
if (items.length === 0) return [];
|
|
1427
|
+
const sorted = [...items].sort((a, b) => b.y - a.y || a.x - b.x);
|
|
1428
|
+
const rows = [];
|
|
1429
|
+
let curItems = [sorted[0]];
|
|
1430
|
+
let curY = sorted[0].y;
|
|
1431
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1432
|
+
if (Math.abs(sorted[i].y - curY) <= Y_TOL) {
|
|
1433
|
+
curItems.push(sorted[i]);
|
|
1434
|
+
} else {
|
|
1435
|
+
rows.push({ y: curY, items: curItems });
|
|
1436
|
+
curItems = [sorted[i]];
|
|
1437
|
+
curY = sorted[i].y;
|
|
1470
1438
|
}
|
|
1471
|
-
}
|
|
1439
|
+
}
|
|
1440
|
+
if (curItems.length > 0) rows.push({ y: curY, items: curItems });
|
|
1441
|
+
return rows;
|
|
1472
1442
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1443
|
+
function hasSuspiciousGaps(row) {
|
|
1444
|
+
if (row.items.length < 2) return false;
|
|
1445
|
+
const sorted = [...row.items].sort((a, b) => a.x - b.x);
|
|
1446
|
+
if (sorted.length === 2 && sorted[1].text.length > 20) return false;
|
|
1447
|
+
const avgFontSize = sorted.reduce((s, i) => s + i.fontSize, 0) / sorted.length;
|
|
1448
|
+
const minGap = Math.max(avgFontSize * MIN_GAP_FACTOR, MIN_GAP_ABSOLUTE);
|
|
1449
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1450
|
+
const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
|
|
1451
|
+
if (gap >= minGap) return true;
|
|
1452
|
+
}
|
|
1453
|
+
return false;
|
|
1476
1454
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
try {
|
|
1494
|
-
return await Promise.race([
|
|
1495
|
-
loadingTask.promise,
|
|
1496
|
-
new Promise((_, reject) => {
|
|
1497
|
-
timer = setTimeout(() => {
|
|
1498
|
-
loadingTask.destroy();
|
|
1499
|
-
reject(new (0, _chunkITJIALN5cjs.KordocError)("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
|
|
1500
|
-
}, PDF_LOAD_TIMEOUT_MS);
|
|
1501
|
-
})
|
|
1502
|
-
]);
|
|
1503
|
-
} finally {
|
|
1504
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
1455
|
+
function extractColumnClusters(rows) {
|
|
1456
|
+
const allX = [];
|
|
1457
|
+
for (const row of rows) {
|
|
1458
|
+
for (const item of row.items) allX.push(item.x);
|
|
1459
|
+
}
|
|
1460
|
+
if (allX.length === 0) return [];
|
|
1461
|
+
allX.sort((a, b) => a - b);
|
|
1462
|
+
const clusters = [];
|
|
1463
|
+
let clusterStart = 0;
|
|
1464
|
+
for (let i = 1; i <= allX.length; i++) {
|
|
1465
|
+
if (i === allX.length || allX[i] - allX[i - 1] > COL_CLUSTER_TOL) {
|
|
1466
|
+
const slice = allX.slice(clusterStart, i);
|
|
1467
|
+
const avg = Math.round(slice.reduce((s, v) => s + v, 0) / slice.length);
|
|
1468
|
+
clusters.push({ x: avg, count: slice.length });
|
|
1469
|
+
clusterStart = i;
|
|
1470
|
+
}
|
|
1505
1471
|
}
|
|
1472
|
+
const minCount = Math.max(2, Math.floor(rows.length * MIN_COL_FILL_RATIO));
|
|
1473
|
+
return clusters.filter((c) => c.count >= minCount).sort((a, b) => a.x - b.x);
|
|
1506
1474
|
}
|
|
1507
|
-
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
const pageHeights = /* @__PURE__ */ new Map();
|
|
1525
|
-
const pagesWithLargeImage = /* @__PURE__ */ new Set();
|
|
1526
|
-
const skippedImagePages = /* @__PURE__ */ new Map();
|
|
1527
|
-
let parsedPages = 0;
|
|
1528
|
-
for (let i = 1; i <= effectivePageCount; i++) {
|
|
1529
|
-
if (pageFilter && !pageFilter.has(i)) continue;
|
|
1530
|
-
try {
|
|
1531
|
-
const page = await doc.getPage(i);
|
|
1532
|
-
const tc = await page.getTextContent();
|
|
1533
|
-
const viewport = page.getViewport({ scale: 1 });
|
|
1534
|
-
pageHeights.set(i, viewport.height);
|
|
1535
|
-
const rawItems = tc.items;
|
|
1536
|
-
const items = normalizeItems(rawItems);
|
|
1537
|
-
const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
|
|
1538
|
-
if (hiddenCount > 0) {
|
|
1539
|
-
warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
1540
|
-
}
|
|
1541
|
-
for (const item of visible) {
|
|
1542
|
-
if (item.fontSize > 0) fontSizeFreq.set(item.fontSize, (fontSizeFreq.get(item.fontSize) || 0) + 1);
|
|
1543
|
-
}
|
|
1544
|
-
const opList = await page.getOperatorList();
|
|
1545
|
-
const pageArea = viewport.width * viewport.height;
|
|
1546
|
-
if (pageArea > 0) {
|
|
1547
|
-
const imageRegions = extractImageRegions(opList.fnArray, opList.argsArray);
|
|
1548
|
-
let uncovered = 0;
|
|
1549
|
-
for (const r of imageRegions) {
|
|
1550
|
-
const area = (r.x2 - r.x1) * (r.y2 - r.y1);
|
|
1551
|
-
if (area < pageArea * 0.05) continue;
|
|
1552
|
-
pagesWithLargeImage.add(i);
|
|
1553
|
-
const hasText = visible.some((it) => {
|
|
1554
|
-
const cx = it.x + it.w / 2;
|
|
1555
|
-
const cy = it.y + (it.h || it.fontSize) / 2;
|
|
1556
|
-
return cx >= r.x1 && cx <= r.x2 && cy >= r.y1 && cy <= r.y2;
|
|
1557
|
-
});
|
|
1558
|
-
if (!hasText) uncovered++;
|
|
1559
|
-
}
|
|
1560
|
-
if (uncovered > 0) skippedImagePages.set(i, uncovered);
|
|
1561
|
-
}
|
|
1562
|
-
const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
|
|
1563
|
-
for (const b of pageBlocks) blocks.push(b);
|
|
1564
|
-
let pageText = "";
|
|
1565
|
-
for (const b of pageBlocks) {
|
|
1566
|
-
const t = b.text || "";
|
|
1567
|
-
totalChars += t.replace(/\s/g, "").length;
|
|
1568
|
-
totalTextBytes += t.length * 2;
|
|
1569
|
-
pageText += pageText ? "\n" + t : t;
|
|
1570
|
-
}
|
|
1571
|
-
pageQuality.push(computePageQuality(i, pageText));
|
|
1572
|
-
if (totalTextBytes > MAX_TOTAL_TEXT) throw new (0, _chunkITJIALN5cjs.KordocError)("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
|
|
1573
|
-
parsedPages++;
|
|
1574
|
-
_optionalChain([options, 'optionalAccess', _12 => _12.onProgress, 'optionalCall', _13 => _13(parsedPages, totalTarget)]);
|
|
1575
|
-
} catch (pageErr) {
|
|
1576
|
-
if (pageErr instanceof _chunkITJIALN5cjs.KordocError) throw pageErr;
|
|
1577
|
-
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" });
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
|
|
1581
|
-
let isImageBased = false;
|
|
1582
|
-
if (totalChars / Math.max(parsedPageCount, 1) < 10) {
|
|
1583
|
-
if (_optionalChain([options, 'optionalAccess', _14 => _14.ocr])) {
|
|
1584
|
-
try {
|
|
1585
|
-
const { ocrPages } = await Promise.resolve().then(() => _interopRequireWildcard(require("./provider-G4C2V2PD.cjs")));
|
|
1586
|
-
const ocrBlocks = await ocrPages(doc, options.ocr, pageFilter, effectivePageCount);
|
|
1587
|
-
if (ocrBlocks.length > 0) {
|
|
1588
|
-
const ocrMarkdown = ocrBlocks.map((b) => b.text || "").filter(Boolean).join("\n\n");
|
|
1589
|
-
return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true, pageQuality, qualitySummary: summarizeDocumentQuality(pageQuality) };
|
|
1590
|
-
}
|
|
1591
|
-
} catch (e2) {
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
isImageBased = true;
|
|
1595
|
-
warnings.push({
|
|
1596
|
-
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`,
|
|
1597
|
-
code: "NEEDS_OCR"
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
if (!isImageBased) {
|
|
1601
|
-
const OCR_REASON_MESSAGES = {
|
|
1602
|
-
low_text: "\uD14D\uC2A4\uD2B8\uAC00 \uAC70\uC758 \uC5C6\uB294 \uD398\uC774\uC9C0 (\uC2A4\uCE94/\uC774\uBBF8\uC9C0 \uCD94\uC815)",
|
|
1603
|
-
high_pua: "\uAE00\uAF34 \uB9E4\uD551 \uC2E4\uD328 (PUA \uBE44\uC728 \uB192\uC74C) \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
1604
|
-
high_control: "\uC81C\uC5B4\uBB38\uC790 \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
1605
|
-
high_replacement: "\uB300\uCCB4\uBB38\uC790(U+FFFD) \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00"
|
|
1606
|
-
};
|
|
1607
|
-
for (const pq of pageQuality) {
|
|
1608
|
-
if (!pq.needsOcr || !pq.ocrReason) continue;
|
|
1609
|
-
if (pq.ocrReason === "low_text" && !pagesWithLargeImage.has(pq.page)) continue;
|
|
1610
|
-
warnings.push({ page: pq.page, message: `${OCR_REASON_MESSAGES[pq.ocrReason]} \u2014 OCR \uAC80\uD1A0 \uD544\uC694`, code: "NEEDS_OCR" });
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
if (!isImageBased) {
|
|
1614
|
-
for (const [page, count] of [...skippedImagePages.entries()].sort((a, b) => a[0] - b[0])) {
|
|
1615
|
-
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" });
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
if (_optionalChain([options, 'optionalAccess', _15 => _15.removeHeaderFooter]) !== false && parsedPageCount >= 3) {
|
|
1619
|
-
const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
|
|
1620
|
-
for (let ri = removed.length - 1; ri >= 0; ri--) {
|
|
1621
|
-
blocks.splice(removed[ri], 1);
|
|
1475
|
+
function findTableRegionsByHeader(allRows, columns, headerItems) {
|
|
1476
|
+
const regions = [];
|
|
1477
|
+
let currentRegion = [];
|
|
1478
|
+
let missStreak = 0;
|
|
1479
|
+
for (const row of allRows) {
|
|
1480
|
+
const matchedCols = countMatchedColumnsRange(row, columns, headerItems);
|
|
1481
|
+
if (matchedCols >= MIN_COLS) {
|
|
1482
|
+
currentRegion.push(row);
|
|
1483
|
+
missStreak = 0;
|
|
1484
|
+
} else if (currentRegion.length > 0 && (row.items.length <= 2 || missStreak === 0)) {
|
|
1485
|
+
currentRegion.push(row);
|
|
1486
|
+
missStreak++;
|
|
1487
|
+
} else {
|
|
1488
|
+
while (currentRegion.length > 0) {
|
|
1489
|
+
const last = currentRegion[currentRegion.length - 1];
|
|
1490
|
+
if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
|
|
1491
|
+
currentRegion.pop();
|
|
1622
1492
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (_optionalChain([options, 'optionalAccess', _16 => _16.formulaOcr]) && formulaBuffer) {
|
|
1626
|
-
try {
|
|
1627
|
-
await applyFormulaOcr(formulaBuffer, blocks, pageFilter, effectivePageCount, warnings, options.onProgress);
|
|
1628
|
-
} catch (e) {
|
|
1629
|
-
warnings.push({
|
|
1630
|
-
message: `\uC218\uC2DD OCR \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`,
|
|
1631
|
-
code: "PARTIAL_PARSE"
|
|
1632
|
-
});
|
|
1493
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
1494
|
+
regions.push({ rows: [...currentRegion] });
|
|
1633
1495
|
}
|
|
1496
|
+
currentRegion = [];
|
|
1497
|
+
missStreak = 0;
|
|
1634
1498
|
}
|
|
1635
|
-
const medianFontSize = computeMedianFontSizeFromFreq(fontSizeFreq);
|
|
1636
|
-
if (medianFontSize > 0) {
|
|
1637
|
-
detectHeadings(blocks, medianFontSize);
|
|
1638
|
-
}
|
|
1639
|
-
detectMarkerHeadings(blocks);
|
|
1640
|
-
detectTableCaptions(blocks);
|
|
1641
|
-
detectKoreanListBlocks(blocks);
|
|
1642
|
-
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
1643
|
-
sanitizeBlockControlChars(blocks);
|
|
1644
|
-
let markdown = cleanPdfText(_chunkITJIALN5cjs.blocksToMarkdown.call(void 0, blocks));
|
|
1645
|
-
return {
|
|
1646
|
-
markdown,
|
|
1647
|
-
blocks,
|
|
1648
|
-
metadata,
|
|
1649
|
-
outline: outline.length > 0 ? outline : void 0,
|
|
1650
|
-
warnings: warnings.length > 0 ? warnings : void 0,
|
|
1651
|
-
isImageBased: isImageBased || void 0,
|
|
1652
|
-
pageQuality,
|
|
1653
|
-
qualitySummary: summarizeDocumentQuality(pageQuality)
|
|
1654
|
-
};
|
|
1655
|
-
} finally {
|
|
1656
|
-
await doc.destroy().catch(() => {
|
|
1657
|
-
});
|
|
1658
1499
|
}
|
|
1500
|
+
while (currentRegion.length > 0) {
|
|
1501
|
+
const last = currentRegion[currentRegion.length - 1];
|
|
1502
|
+
if (countMatchedColumnsRange(last, columns, headerItems) >= MIN_COLS) break;
|
|
1503
|
+
currentRegion.pop();
|
|
1504
|
+
}
|
|
1505
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
1506
|
+
regions.push({ rows: currentRegion });
|
|
1507
|
+
}
|
|
1508
|
+
return regions;
|
|
1659
1509
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
const
|
|
1665
|
-
if (
|
|
1666
|
-
|
|
1667
|
-
if (
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1510
|
+
function findTableRegions(allRows, columns) {
|
|
1511
|
+
const regions = [];
|
|
1512
|
+
let currentRegion = [];
|
|
1513
|
+
for (const row of allRows) {
|
|
1514
|
+
const matchedCols = countMatchedColumns(row, columns);
|
|
1515
|
+
if (matchedCols >= MIN_COLS) {
|
|
1516
|
+
currentRegion.push(row);
|
|
1517
|
+
} else if (row.items.length === 1) {
|
|
1518
|
+
if (currentRegion.length > 0) {
|
|
1519
|
+
currentRegion.push(row);
|
|
1520
|
+
}
|
|
1521
|
+
} else {
|
|
1522
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
1523
|
+
regions.push({ rows: [...currentRegion] });
|
|
1524
|
+
}
|
|
1525
|
+
currentRegion = [];
|
|
1671
1526
|
}
|
|
1672
|
-
if (typeof info.CreationDate === "string") metadata.createdAt = parsePdfDate(info.CreationDate);
|
|
1673
|
-
if (typeof info.ModDate === "string") metadata.modifiedAt = parsePdfDate(info.ModDate);
|
|
1674
|
-
} catch (e3) {
|
|
1675
1527
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const m = dateStr.match(/D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?/);
|
|
1679
|
-
if (!m) return void 0;
|
|
1680
|
-
const [, year, month = "01", day = "01", hour = "00", min = "00", sec = "00"] = m;
|
|
1681
|
-
return `${year}-${month}-${day}T${hour}:${min}:${sec}`;
|
|
1682
|
-
}
|
|
1683
|
-
async function extractPdfMetadataOnly(buffer) {
|
|
1684
|
-
const doc = await loadPdfWithTimeout(buffer);
|
|
1685
|
-
try {
|
|
1686
|
-
const metadata = { pageCount: doc.numPages };
|
|
1687
|
-
await extractPdfMetadata(doc, metadata);
|
|
1688
|
-
return metadata;
|
|
1689
|
-
} finally {
|
|
1690
|
-
await doc.destroy().catch(() => {
|
|
1691
|
-
});
|
|
1528
|
+
if (currentRegion.length >= MIN_ROWS) {
|
|
1529
|
+
regions.push({ rows: currentRegion });
|
|
1692
1530
|
}
|
|
1531
|
+
return regions;
|
|
1693
1532
|
}
|
|
1694
|
-
function
|
|
1695
|
-
|
|
1696
|
-
const
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
const margin = Math.max(pageWidth, pageHeight) * 0.1;
|
|
1703
|
-
if (item.x < -margin || item.x > pageWidth + margin || item.y < -margin || item.y > pageHeight + margin) {
|
|
1704
|
-
hiddenCount++;
|
|
1705
|
-
continue;
|
|
1533
|
+
function countMatchedColumns(row, columns) {
|
|
1534
|
+
const matched = /* @__PURE__ */ new Set();
|
|
1535
|
+
for (const item of row.items) {
|
|
1536
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
1537
|
+
if (Math.abs(item.x - columns[ci].x) <= COL_CLUSTER_TOL * 2) {
|
|
1538
|
+
matched.add(ci);
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1706
1541
|
}
|
|
1707
|
-
visible.push(item);
|
|
1708
1542
|
}
|
|
1709
|
-
return
|
|
1543
|
+
return matched.size;
|
|
1710
1544
|
}
|
|
1711
|
-
function
|
|
1712
|
-
|
|
1713
|
-
let
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
let cumulative = 0;
|
|
1718
|
-
for (const [size, count] of sorted) {
|
|
1719
|
-
cumulative += count;
|
|
1720
|
-
if (cumulative > mid) return size;
|
|
1545
|
+
function countMatchedColumnsRange(row, columns, headerItems) {
|
|
1546
|
+
const boundaries = [];
|
|
1547
|
+
for (let ci = 0; ci < headerItems.length; ci++) {
|
|
1548
|
+
const left = ci === 0 ? 0 : (headerItems[ci - 1].x + headerItems[ci - 1].w + headerItems[ci].x) / 2;
|
|
1549
|
+
const right = ci === headerItems.length - 1 ? Infinity : (headerItems[ci].x + headerItems[ci].w + headerItems[ci + 1].x) / 2;
|
|
1550
|
+
boundaries.push({ left, right });
|
|
1721
1551
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
if (/^\d+$/.test(text)) continue;
|
|
1730
|
-
const ratio = block.style.fontSize / medianFontSize;
|
|
1731
|
-
let level = 0;
|
|
1732
|
-
if (ratio >= _chunkITJIALN5cjs.HEADING_RATIO_H1) level = 1;
|
|
1733
|
-
else if (ratio >= _chunkITJIALN5cjs.HEADING_RATIO_H2) level = 2;
|
|
1734
|
-
else if (ratio >= _chunkITJIALN5cjs.HEADING_RATIO_H3) level = 3;
|
|
1735
|
-
if (level > 0) {
|
|
1736
|
-
block.type = "heading";
|
|
1737
|
-
block.level = level;
|
|
1738
|
-
block.text = collapseEvenSpacing(text);
|
|
1552
|
+
const matched = /* @__PURE__ */ new Set();
|
|
1553
|
+
for (const item of row.items) {
|
|
1554
|
+
for (let ci = 0; ci < boundaries.length; ci++) {
|
|
1555
|
+
if (item.x >= boundaries[ci].left && item.x < boundaries[ci].right) {
|
|
1556
|
+
matched.add(ci);
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1739
1559
|
}
|
|
1740
1560
|
}
|
|
1561
|
+
return matched.size;
|
|
1741
1562
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1563
|
+
function assignRowItems(items, columns, numCols) {
|
|
1564
|
+
if (items.length === 0) return [];
|
|
1565
|
+
const sorted = [...items].sort((a, b) => a.x - b.x);
|
|
1566
|
+
const colCenters = columns.map((c) => c.x);
|
|
1567
|
+
const gaps = [];
|
|
1568
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1569
|
+
gaps.push({ idx: i, size: sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w) });
|
|
1747
1570
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
);
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
const totalCells2 = table.rows * table.cols;
|
|
1758
|
-
const emptyCells2 = totalCells2 - allCells.length;
|
|
1759
|
-
if (emptyCells2 >= totalCells2 * 0.3) return true;
|
|
1760
|
-
if (/[□■◆○●▶ㅇ]/.test(allText)) return true;
|
|
1761
|
-
if (/<[^>]+>/.test(allText)) return true;
|
|
1571
|
+
const gapSizes = gaps.map((g2) => g2.size).sort((a, b) => a - b);
|
|
1572
|
+
const medianGap = gapSizes.length > 0 ? gapSizes[Math.floor(gapSizes.length / 2)] : 0;
|
|
1573
|
+
const gapThreshold = sorted.length <= numCols + 1 ? 12 : Math.max(medianGap * 2.5, 12);
|
|
1574
|
+
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);
|
|
1575
|
+
const groups = [];
|
|
1576
|
+
let start = 0;
|
|
1577
|
+
for (const gap of significantGaps) {
|
|
1578
|
+
groups.push(sorted.slice(start, gap.idx));
|
|
1579
|
+
start = gap.idx;
|
|
1762
1580
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const
|
|
1766
|
-
const
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
lines.push(cells.join(" "));
|
|
1581
|
+
groups.push(sorted.slice(start));
|
|
1582
|
+
const result = [];
|
|
1583
|
+
const usedCols = /* @__PURE__ */ new Set();
|
|
1584
|
+
const groupCenters = groups.map((g2) => {
|
|
1585
|
+
let minX = Infinity, maxX = -Infinity;
|
|
1586
|
+
for (const i of g2) {
|
|
1587
|
+
if (i.x < minX) minX = i.x;
|
|
1588
|
+
const r = i.x + i.w;
|
|
1589
|
+
if (r > maxX) maxX = r;
|
|
1590
|
+
}
|
|
1591
|
+
return (minX + maxX) / 2;
|
|
1592
|
+
});
|
|
1593
|
+
const assignments = [];
|
|
1594
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
1595
|
+
for (let ci = 0; ci < numCols; ci++) {
|
|
1596
|
+
assignments.push({ gi, ci, dist: Math.abs(groupCenters[gi] - colCenters[ci]) });
|
|
1780
1597
|
}
|
|
1781
1598
|
}
|
|
1782
|
-
|
|
1599
|
+
assignments.sort((a, b) => a.dist - b.dist);
|
|
1600
|
+
const assignedGroups = /* @__PURE__ */ new Set();
|
|
1601
|
+
for (const { gi, ci } of assignments) {
|
|
1602
|
+
if (assignedGroups.has(gi) || usedCols.has(ci)) continue;
|
|
1603
|
+
result.push({ col: ci, items: groups[gi] });
|
|
1604
|
+
assignedGroups.add(gi);
|
|
1605
|
+
usedCols.add(ci);
|
|
1606
|
+
}
|
|
1607
|
+
for (let gi = 0; gi < groups.length; gi++) {
|
|
1608
|
+
if (assignedGroups.has(gi)) continue;
|
|
1609
|
+
let bestCol = 0, bestDist = Infinity;
|
|
1610
|
+
for (let ci = 0; ci < numCols; ci++) {
|
|
1611
|
+
const d = Math.abs(groupCenters[gi] - colCenters[ci]);
|
|
1612
|
+
if (d < bestDist) {
|
|
1613
|
+
bestDist = d;
|
|
1614
|
+
bestCol = ci;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
result.push({ col: bestCol, items: groups[gi] });
|
|
1618
|
+
}
|
|
1619
|
+
return result;
|
|
1783
1620
|
}
|
|
1784
|
-
function
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1621
|
+
function buildClusterTable(rows, columns, pageNum) {
|
|
1622
|
+
const numCols = columns.length;
|
|
1623
|
+
const numRows = rows.length;
|
|
1624
|
+
if (numRows < MIN_ROWS || numCols < MIN_COLS) return null;
|
|
1625
|
+
const cells = Array.from(
|
|
1626
|
+
{ length: numRows },
|
|
1627
|
+
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
1628
|
+
);
|
|
1629
|
+
const usedItems = /* @__PURE__ */ new Set();
|
|
1630
|
+
for (let r = 0; r < numRows; r++) {
|
|
1631
|
+
const row = rows[r];
|
|
1632
|
+
if (row.items.length === 1 && numCols > 1) {
|
|
1633
|
+
cells[r][0] = { text: row.items[0].text, colSpan: numCols, rowSpan: 1 };
|
|
1634
|
+
usedItems.add(row.items[0]);
|
|
1792
1635
|
continue;
|
|
1793
1636
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
const
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1637
|
+
const assignments = assignRowItems(row.items, columns, numCols);
|
|
1638
|
+
for (const { col, items } of assignments) {
|
|
1639
|
+
const text = items.map((i) => i.text).join(" ");
|
|
1640
|
+
const existing = cells[r][col].text;
|
|
1641
|
+
cells[r][col].text = existing ? existing + " " + text : text;
|
|
1642
|
+
for (const item of items) usedItems.add(item);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
let emptyRows = 0;
|
|
1646
|
+
for (const row of cells) {
|
|
1647
|
+
if (row.every((c) => c.text === "")) emptyRows++;
|
|
1648
|
+
}
|
|
1649
|
+
if (emptyRows > numRows * 0.5) return null;
|
|
1650
|
+
for (let c = 0; c < numCols; c++) {
|
|
1651
|
+
const hasValue = cells.some((row) => row[c].text !== "");
|
|
1652
|
+
if (!hasValue) return null;
|
|
1653
|
+
}
|
|
1654
|
+
for (let r = numRows - 1; r >= 1; r--) {
|
|
1655
|
+
const nonEmptyCols = cells[r].filter((c) => c.text.trim()).length;
|
|
1656
|
+
if (nonEmptyCols !== 1) continue;
|
|
1657
|
+
if (cells[r][0].text.trim() !== "") continue;
|
|
1658
|
+
const contentText = _optionalChain([cells, 'access', _9 => _9[r], 'access', _10 => _10.find, 'call', _11 => _11((c) => c.text.trim()), 'optionalAccess', _12 => _12.text, 'access', _13 => _13.trim, 'call', _14 => _14()]) || "";
|
|
1659
|
+
if (/^[○●▶\-·]/.test(contentText)) continue;
|
|
1660
|
+
for (let pr = r - 1; pr >= 0; pr--) {
|
|
1661
|
+
if (cells[pr].some((c) => c.text.trim())) {
|
|
1662
|
+
for (let c = 0; c < numCols; c++) {
|
|
1663
|
+
const prev = cells[pr][c].text.trim();
|
|
1664
|
+
const curr = cells[r][c].text.trim();
|
|
1665
|
+
if (curr) cells[pr][c].text = prev ? prev + " " + curr : curr;
|
|
1666
|
+
}
|
|
1667
|
+
for (let c = 0; c < numCols; c++) cells[r][c].text = "";
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
for (let r = 0; r < cells.length - 1; r++) {
|
|
1673
|
+
const row = cells[r];
|
|
1674
|
+
const hasCol0 = row[0].text.trim() !== "";
|
|
1675
|
+
const hasColLast = numCols > 1 && row[numCols - 1].text.trim() !== "";
|
|
1676
|
+
const midEmpty = row.slice(1, numCols - 1).every((c) => c.text.trim() === "");
|
|
1677
|
+
if (hasCol0 && hasColLast && midEmpty) {
|
|
1678
|
+
const next = cells[r + 1];
|
|
1679
|
+
if (next[0].text.trim() === "" && next.some((c) => c.text.trim())) {
|
|
1680
|
+
for (let c = 1; c < numCols; c++) {
|
|
1681
|
+
const curr = next[c].text.trim();
|
|
1682
|
+
if (curr) row[c].text = row[c].text.trim() ? row[c].text.trim() + " " + curr : curr;
|
|
1683
|
+
}
|
|
1684
|
+
for (let c = 0; c < numCols; c++) next[c].text = "";
|
|
1802
1685
|
}
|
|
1803
1686
|
}
|
|
1804
1687
|
}
|
|
1688
|
+
const filteredCells = cells.filter((row) => row.some((c) => c.text.trim()));
|
|
1689
|
+
const finalRowCount = filteredCells.length;
|
|
1690
|
+
if (finalRowCount < MIN_ROWS) return null;
|
|
1691
|
+
const irTable = {
|
|
1692
|
+
rows: finalRowCount,
|
|
1693
|
+
cols: numCols,
|
|
1694
|
+
cells: filteredCells,
|
|
1695
|
+
hasHeader: finalRowCount > 1
|
|
1696
|
+
};
|
|
1697
|
+
const allItems = rows.flatMap((r) => r.items);
|
|
1698
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
1699
|
+
for (const i of allItems) {
|
|
1700
|
+
if (i.x < minX) minX = i.x;
|
|
1701
|
+
if (i.y < minY) minY = i.y;
|
|
1702
|
+
if (i.x + i.w > maxX) maxX = i.x + i.w;
|
|
1703
|
+
const h = i.h > 0 ? i.h : i.fontSize;
|
|
1704
|
+
if (i.y + h > maxY) maxY = i.y + h;
|
|
1705
|
+
}
|
|
1706
|
+
return {
|
|
1707
|
+
table: irTable,
|
|
1708
|
+
bbox: { page: pageNum, x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
1709
|
+
usedItems
|
|
1710
|
+
};
|
|
1805
1711
|
}
|
|
1712
|
+
|
|
1713
|
+
// src/pdf/xy-cut.ts
|
|
1806
1714
|
var MAX_XYCUT_DEPTH = 50;
|
|
1807
1715
|
var XYCUT_MIN_GAP = 5;
|
|
1808
1716
|
var CROSS_LAYOUT_BETA = 2;
|
|
@@ -1960,1033 +1868,947 @@ function findVerticalCut(items) {
|
|
|
1960
1868
|
}
|
|
1961
1869
|
return { position, gap: largestGap };
|
|
1962
1870
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
(
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
if (grids.length > 0) {
|
|
1972
|
-
return extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals);
|
|
1871
|
+
|
|
1872
|
+
// src/pdf/columns.ts
|
|
1873
|
+
function isProseSpread(items) {
|
|
1874
|
+
if (items.length < 4) return false;
|
|
1875
|
+
const sorted = [...items].sort((a, b) => a.x - b.x);
|
|
1876
|
+
const gaps = [];
|
|
1877
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1878
|
+
gaps.push(sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w));
|
|
1973
1879
|
}
|
|
1974
|
-
|
|
1880
|
+
const maxGap = _chunkYBPNKFJWcjs.safeMax.call(void 0, gaps);
|
|
1881
|
+
const avgLen = items.reduce((s, i) => s + i.text.length, 0) / items.length;
|
|
1882
|
+
return maxGap < 40 && avgLen < 5;
|
|
1975
1883
|
}
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
for (const item of items) {
|
|
1987
|
-
const h = item.h > 0 ? item.h : item.fontSize;
|
|
1988
|
-
if (h <= 0 || item.w <= 0) continue;
|
|
1989
|
-
if (line.lineWidth > h * STRIKE_MAX_THICKNESS_RATIO) continue;
|
|
1990
|
-
const centerY = item.y + h * 0.4;
|
|
1991
|
-
if (Math.abs(line.y1 - centerY) > h * STRIKE_CENTER_TOLERANCE) continue;
|
|
1992
|
-
const overlap = Math.min(line.x2, item.x + item.w) - Math.max(line.x1, item.x);
|
|
1993
|
-
if (overlap / item.w < STRIKE_MIN_OVERLAP_RATIO) continue;
|
|
1994
|
-
matches.push(item);
|
|
1884
|
+
function detectColumns(yLines) {
|
|
1885
|
+
const allItems = yLines.flat();
|
|
1886
|
+
if (allItems.length === 0) return null;
|
|
1887
|
+
const pageWidth = _chunkYBPNKFJWcjs.safeMax.call(void 0, allItems.map((i) => i.x + i.w)) - _chunkYBPNKFJWcjs.safeMin.call(void 0, allItems.map((i) => i.x));
|
|
1888
|
+
if (pageWidth < 100) return null;
|
|
1889
|
+
let bigoLineIdx = -1;
|
|
1890
|
+
for (let i = 0; i < yLines.length; i++) {
|
|
1891
|
+
if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
|
|
1892
|
+
bigoLineIdx = i;
|
|
1893
|
+
break;
|
|
1995
1894
|
}
|
|
1996
|
-
if (matches.length === 0) continue;
|
|
1997
|
-
let totalW = 0;
|
|
1998
|
-
for (const m of matches) totalW += m.w;
|
|
1999
|
-
if (totalW <= 0 || (line.x2 - line.x1) / totalW > STRIKE_MAX_LINE_TO_TEXT_RATIO) continue;
|
|
2000
|
-
for (const m of matches) m.strike = true;
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
function wrapStrikethroughRuns(items) {
|
|
2004
|
-
const struck = items.filter((i) => i.strike);
|
|
2005
|
-
if (struck.length === 0) return;
|
|
2006
|
-
const lines = /* @__PURE__ */ new Map();
|
|
2007
|
-
for (const item of struck) {
|
|
2008
|
-
const key = Math.round(item.y / 3);
|
|
2009
|
-
const arr = lines.get(key) || [];
|
|
2010
|
-
arr.push(item);
|
|
2011
|
-
lines.set(key, arr);
|
|
2012
|
-
}
|
|
2013
|
-
for (const arr of lines.values()) {
|
|
2014
|
-
arr.sort((a, b) => a.x - b.x);
|
|
2015
|
-
arr[0].text = "~~" + arr[0].text;
|
|
2016
|
-
arr[arr.length - 1].text = arr[arr.length - 1].text + "~~";
|
|
2017
1895
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const
|
|
2021
|
-
const
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
if (
|
|
2035
|
-
|
|
2036
|
-
tableItems.push(item);
|
|
2037
|
-
usedItems.add(item);
|
|
2038
|
-
}
|
|
2039
|
-
const cells = extractCells(grid, horizontals, verticals);
|
|
2040
|
-
if (cells.length === 0) continue;
|
|
2041
|
-
const textItems = tableItems.map((i) => ({
|
|
2042
|
-
text: i.text,
|
|
2043
|
-
x: i.x,
|
|
2044
|
-
y: i.y,
|
|
2045
|
-
w: i.w,
|
|
2046
|
-
h: i.h,
|
|
2047
|
-
fontSize: i.fontSize,
|
|
2048
|
-
fontName: i.fontName,
|
|
2049
|
-
hasSpaceBefore: i.hasSpaceBefore
|
|
2050
|
-
}));
|
|
2051
|
-
const cellTextMap = mapTextToCells(textItems, cells);
|
|
2052
|
-
const numRows = grid.rowYs.length - 1;
|
|
2053
|
-
const numCols = grid.colXs.length - 1;
|
|
2054
|
-
const irGrid = Array.from(
|
|
2055
|
-
{ length: numRows },
|
|
2056
|
-
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
2057
|
-
);
|
|
2058
|
-
for (const cell of cells) {
|
|
2059
|
-
const cellItems = cellTextMap.get(cell) || [];
|
|
2060
|
-
let text = cellTextToString(cellItems);
|
|
2061
|
-
text = text.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
|
|
2062
|
-
text = text.split("\n").map((line) => collapseEvenSpacing(line)).join("\n");
|
|
2063
|
-
irGrid[cell.row][cell.col] = {
|
|
2064
|
-
text,
|
|
2065
|
-
colSpan: cell.colSpan,
|
|
2066
|
-
rowSpan: cell.rowSpan
|
|
2067
|
-
};
|
|
2068
|
-
}
|
|
2069
|
-
let finalGrid = irGrid;
|
|
2070
|
-
let finalRows = numRows;
|
|
2071
|
-
if (numRows <= 2 && numCols >= 3) {
|
|
2072
|
-
const rebuilt = normalizeUndersegmentedTable(irGrid, grid.colXs, textItems);
|
|
2073
|
-
if (rebuilt) {
|
|
2074
|
-
finalGrid = rebuilt.map((row) => row.map((rawText) => {
|
|
2075
|
-
const cleaned = rawText.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
|
|
2076
|
-
return {
|
|
2077
|
-
text: cleaned.split("\n").map((line) => collapseEvenSpacing(line)).join("\n"),
|
|
2078
|
-
colSpan: 1,
|
|
2079
|
-
rowSpan: 1
|
|
2080
|
-
};
|
|
2081
|
-
}));
|
|
2082
|
-
finalRows = finalGrid.length;
|
|
1896
|
+
const tableYLines = bigoLineIdx >= 0 ? yLines.slice(0, bigoLineIdx) : yLines;
|
|
1897
|
+
const CLUSTER_TOL = 22;
|
|
1898
|
+
const xClusters = [];
|
|
1899
|
+
for (const line of tableYLines) {
|
|
1900
|
+
if (isProseSpread(line)) continue;
|
|
1901
|
+
for (const item of line) {
|
|
1902
|
+
let found = false;
|
|
1903
|
+
for (const c of xClusters) {
|
|
1904
|
+
if (Math.abs(item.x - c.center) <= CLUSTER_TOL) {
|
|
1905
|
+
c.center = Math.round((c.center * c.count + item.x) / (c.count + 1));
|
|
1906
|
+
c.minX = Math.min(c.minX, item.x);
|
|
1907
|
+
c.count++;
|
|
1908
|
+
found = true;
|
|
1909
|
+
break;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (!found) {
|
|
1913
|
+
xClusters.push({ center: item.x, count: 1, minX: item.x });
|
|
2083
1914
|
}
|
|
2084
1915
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const
|
|
2092
|
-
if (
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
x: grid.bbox.x1,
|
|
2096
|
-
y: grid.bbox.y1,
|
|
2097
|
-
width: grid.bbox.x2 - grid.bbox.x1,
|
|
2098
|
-
height: grid.bbox.y2 - grid.bbox.y1
|
|
2099
|
-
};
|
|
2100
|
-
if (shouldDemoteTable(irTable)) {
|
|
2101
|
-
const demoted = demoteTableToText(irTable);
|
|
2102
|
-
if (demoted) {
|
|
2103
|
-
const text = numGridRows === 1 ? "\n" + demoted + "\n" : demoted;
|
|
2104
|
-
blocks.push({ type: "paragraph", text, pageNumber: pageNum, bbox: tableBbox, style: dominantStyle(tableItems) });
|
|
1916
|
+
}
|
|
1917
|
+
const peaks = xClusters.filter((c) => c.count >= 3).sort((a, b) => a.minX - b.minX);
|
|
1918
|
+
if (peaks.length < 3) return null;
|
|
1919
|
+
const MERGE_TOL = 40;
|
|
1920
|
+
const merged = [peaks[0]];
|
|
1921
|
+
for (let i = 1; i < peaks.length; i++) {
|
|
1922
|
+
const prev = merged[merged.length - 1];
|
|
1923
|
+
if (peaks[i].minX - prev.minX < MERGE_TOL) {
|
|
1924
|
+
if (peaks[i].count > prev.count) {
|
|
1925
|
+
prev.center = peaks[i].center;
|
|
2105
1926
|
}
|
|
2106
|
-
|
|
1927
|
+
prev.count += peaks[i].count;
|
|
1928
|
+
prev.minX = Math.min(prev.minX, peaks[i].minX);
|
|
1929
|
+
} else {
|
|
1930
|
+
merged.push({ ...peaks[i] });
|
|
2107
1931
|
}
|
|
2108
|
-
blocks.push({ type: "table", table: irTable, pageNumber: pageNum, bbox: tableBbox });
|
|
2109
1932
|
}
|
|
2110
|
-
|
|
2111
|
-
if (
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
1933
|
+
const rawColumns = merged.filter((c) => c.count >= 3).map((c) => c.minX);
|
|
1934
|
+
if (rawColumns.length < 3) return null;
|
|
1935
|
+
const MIN_DETECT_COL_WIDTH = 30;
|
|
1936
|
+
const columns = [rawColumns[0]];
|
|
1937
|
+
for (let i = 1; i < rawColumns.length; i++) {
|
|
1938
|
+
if (rawColumns[i] - columns[columns.length - 1] < MIN_DETECT_COL_WIDTH) continue;
|
|
1939
|
+
columns.push(rawColumns[i]);
|
|
1940
|
+
}
|
|
1941
|
+
return columns.length >= 3 ? columns : null;
|
|
1942
|
+
}
|
|
1943
|
+
function findColumn(x, columns) {
|
|
1944
|
+
for (let i = columns.length - 1; i >= 0; i--) {
|
|
1945
|
+
if (x >= columns[i] - 10) return i;
|
|
1946
|
+
}
|
|
1947
|
+
return 0;
|
|
1948
|
+
}
|
|
1949
|
+
function extractWithColumns(yLines, columns) {
|
|
1950
|
+
const result = [];
|
|
1951
|
+
const colMin = columns[0];
|
|
1952
|
+
const colMax = columns[columns.length - 1];
|
|
1953
|
+
let bigoIdx = -1;
|
|
1954
|
+
for (let i = 0; i < yLines.length; i++) {
|
|
1955
|
+
if (yLines[i].length <= 2 && yLines[i].some((item) => item.text === "\uBE44\uACE0")) {
|
|
1956
|
+
bigoIdx = i;
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
let tableStart = -1;
|
|
1961
|
+
for (let i = 0; i < (bigoIdx >= 0 ? bigoIdx : yLines.length); i++) {
|
|
1962
|
+
const usedCols = new Set(yLines[i].map((item) => findColumn(item.x, columns)));
|
|
1963
|
+
if (usedCols.size >= 3) {
|
|
1964
|
+
tableStart = i;
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
const tableEnd = bigoIdx >= 0 ? bigoIdx : yLines.length;
|
|
1969
|
+
for (let i = 0; i < (tableStart >= 0 ? tableStart : tableEnd); i++) {
|
|
1970
|
+
result.push(mergeLineSimple(yLines[i]));
|
|
1971
|
+
}
|
|
1972
|
+
if (tableStart >= 0) {
|
|
1973
|
+
const tableLines = yLines.slice(tableStart, tableEnd);
|
|
1974
|
+
const gridLines = [];
|
|
1975
|
+
for (const line of tableLines) {
|
|
1976
|
+
const inRange = line.some(
|
|
1977
|
+
(item) => item.x >= colMin - 20 && item.x <= colMax + 200
|
|
1978
|
+
);
|
|
1979
|
+
if (inRange && !isProseSpread(line)) {
|
|
1980
|
+
gridLines.push(line);
|
|
1981
|
+
} else {
|
|
1982
|
+
if (gridLines.length > 0) {
|
|
1983
|
+
result.push(buildGridTable(gridLines.splice(0), columns));
|
|
2132
1984
|
}
|
|
2133
|
-
|
|
1985
|
+
result.push(mergeLineSimple(line));
|
|
2134
1986
|
}
|
|
2135
|
-
remaining = remaining.filter((_, idx) => !usedClusterIndices.has(idx));
|
|
2136
1987
|
}
|
|
2137
|
-
if (
|
|
2138
|
-
|
|
2139
|
-
const pageH = _chunkITJIALN5cjs.safeMax.call(void 0, allY) - _chunkITJIALN5cjs.safeMin.call(void 0, allY);
|
|
2140
|
-
const groups = xyCutOrder(remaining, Math.max(15, pageH * 0.03));
|
|
2141
|
-
const textBlocks = [];
|
|
2142
|
-
for (const group of groups) {
|
|
2143
|
-
if (group.length === 0) continue;
|
|
2144
|
-
const groupBlocks = extractPageBlocksFallback(group, pageNum);
|
|
2145
|
-
for (const b of groupBlocks) textBlocks.push(b);
|
|
2146
|
-
}
|
|
2147
|
-
const finalTextBlocks = detectListBlocks(textBlocks);
|
|
2148
|
-
for (const b of finalTextBlocks) blocks.push(b);
|
|
1988
|
+
if (gridLines.length > 0) {
|
|
1989
|
+
result.push(buildGridTable(gridLines, columns));
|
|
2149
1990
|
}
|
|
2150
|
-
blocks.sort((a, b) => {
|
|
2151
|
-
const ay = a.bbox ? a.bbox.y + a.bbox.height : 0;
|
|
2152
|
-
const by = b.bbox ? b.bbox.y + b.bbox.height : 0;
|
|
2153
|
-
return by - ay;
|
|
2154
|
-
});
|
|
2155
|
-
return mergeAdjacentTableBlocks(blocks);
|
|
2156
1991
|
}
|
|
2157
|
-
|
|
1992
|
+
if (bigoIdx >= 0) {
|
|
1993
|
+
result.push("");
|
|
1994
|
+
for (let i = bigoIdx; i < yLines.length; i++) {
|
|
1995
|
+
result.push(mergeLineSimple(yLines[i]));
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
return result.join("\n");
|
|
2158
1999
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
const
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
if (prev.table.cols !== curr.table.cols) continue;
|
|
2167
|
-
if (!prev.bbox || !curr.bbox) continue;
|
|
2168
|
-
const width = Math.max(prev.bbox.width, curr.bbox.width, 1);
|
|
2169
|
-
const leftDiff = Math.abs(prev.bbox.x - curr.bbox.x);
|
|
2170
|
-
const rightDiff = Math.abs(prev.bbox.x + prev.bbox.width - (curr.bbox.x + curr.bbox.width));
|
|
2171
|
-
if (leftDiff > width * NEIGHBOR_TABLE_EPSILON || rightDiff > width * NEIGHBOR_TABLE_EPSILON) continue;
|
|
2172
|
-
let currCells = curr.table.cells;
|
|
2173
|
-
if (currCells.length > 1 && prev.table.cells.length > 0 && rowTextsEqual(prev.table.cells[0], currCells[0])) {
|
|
2174
|
-
currCells = currCells.slice(1);
|
|
2000
|
+
function buildGridTable(lines, columns) {
|
|
2001
|
+
const numCols = columns.length;
|
|
2002
|
+
const yRows = lines.map((items) => {
|
|
2003
|
+
const row = Array(numCols).fill("");
|
|
2004
|
+
for (const item of items) {
|
|
2005
|
+
const col = findColumn(item.x, columns);
|
|
2006
|
+
row[col] = row[col] ? row[col] + " " + item.text : item.text;
|
|
2175
2007
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2008
|
+
return row;
|
|
2009
|
+
});
|
|
2010
|
+
const dataColStart = Math.max(2, Math.floor(numCols / 2));
|
|
2011
|
+
const merged = [];
|
|
2012
|
+
for (const row of yRows) {
|
|
2013
|
+
if (row.every((c) => c === "")) continue;
|
|
2014
|
+
if (merged.length === 0) {
|
|
2015
|
+
merged.push([...row]);
|
|
2178
2016
|
continue;
|
|
2179
2017
|
}
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
};
|
|
2187
|
-
blocks[i] = { ...prev, table: merged };
|
|
2188
|
-
blocks.splice(i + 1, 1);
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
function rowTextsEqual(a, b) {
|
|
2192
|
-
if (a.length !== b.length) return false;
|
|
2193
|
-
const norm = (t) => t.replace(/\s+/g, "");
|
|
2194
|
-
for (let i = 0; i < a.length; i++) {
|
|
2195
|
-
if (norm(a[i].text) !== norm(b[i].text)) return false;
|
|
2196
|
-
}
|
|
2197
|
-
return a.some((c) => c.text.trim() !== "");
|
|
2198
|
-
}
|
|
2199
|
-
function mergeAdjacentTableBlocks(blocks) {
|
|
2200
|
-
if (blocks.length <= 1) return blocks;
|
|
2201
|
-
const result = [blocks[0]];
|
|
2202
|
-
for (let i = 1; i < blocks.length; i++) {
|
|
2203
|
-
const prev = result[result.length - 1];
|
|
2204
|
-
const curr = blocks[i];
|
|
2205
|
-
if (prev.type === "table" && curr.type === "table" && prev.table && curr.table && prev.table.cols === curr.table.cols) {
|
|
2206
|
-
const merged = {
|
|
2207
|
-
rows: prev.table.rows + curr.table.rows,
|
|
2208
|
-
cols: prev.table.cols,
|
|
2209
|
-
cells: [...prev.table.cells, ...curr.table.cells],
|
|
2210
|
-
hasHeader: prev.table.hasHeader
|
|
2211
|
-
};
|
|
2212
|
-
result[result.length - 1] = { ...prev, table: merged };
|
|
2213
|
-
} else {
|
|
2214
|
-
result.push(curr);
|
|
2018
|
+
const prev = merged[merged.length - 1];
|
|
2019
|
+
const filledCols = row.map((c, i) => c ? i : -1).filter((i) => i >= 0);
|
|
2020
|
+
const filledCount = filledCols.length;
|
|
2021
|
+
let isNewRow = false;
|
|
2022
|
+
if (row[0] && row[0].length >= 3) {
|
|
2023
|
+
isNewRow = true;
|
|
2215
2024
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
}
|
|
2219
|
-
function extractPageBlocksFallback(items, pageNum) {
|
|
2220
|
-
if (items.length === 0) return [];
|
|
2221
|
-
const blocks = [];
|
|
2222
|
-
const clusterItems = items.map((i) => ({
|
|
2223
|
-
text: i.text,
|
|
2224
|
-
x: i.x,
|
|
2225
|
-
y: i.y,
|
|
2226
|
-
w: i.w,
|
|
2227
|
-
h: i.h,
|
|
2228
|
-
fontSize: i.fontSize,
|
|
2229
|
-
fontName: i.fontName,
|
|
2230
|
-
hasSpaceBefore: i.hasSpaceBefore
|
|
2231
|
-
}));
|
|
2232
|
-
const clusterResults = detectClusterTables(clusterItems, pageNum);
|
|
2233
|
-
if (clusterResults.length > 0) {
|
|
2234
|
-
const ciToIdx = /* @__PURE__ */ new Map();
|
|
2235
|
-
for (let ci = 0; ci < clusterItems.length; ci++) ciToIdx.set(clusterItems[ci], ci);
|
|
2236
|
-
const usedIndices = /* @__PURE__ */ new Set();
|
|
2237
|
-
for (const cr of clusterResults) {
|
|
2238
|
-
for (const ci of cr.usedItems) {
|
|
2239
|
-
const idx = ciToIdx.get(ci);
|
|
2240
|
-
if (idx !== void 0) usedIndices.add(idx);
|
|
2241
|
-
}
|
|
2242
|
-
blocks.push({ type: "table", table: cr.table, pageNumber: pageNum, bbox: cr.bbox });
|
|
2025
|
+
if (!isNewRow && numCols > 1 && row[1]) {
|
|
2026
|
+
isNewRow = true;
|
|
2243
2027
|
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
if (!text.trim()) continue;
|
|
2250
|
-
const bbox = computeBBox(line, pageNum);
|
|
2251
|
-
blocks.push({ type: "paragraph", text, pageNumber: pageNum, bbox, style: dominantStyle(line) });
|
|
2028
|
+
if (!isNewRow) {
|
|
2029
|
+
const hasData = row.slice(dataColStart).some((c) => c !== "");
|
|
2030
|
+
const prevHasData = prev.slice(dataColStart).some((c) => c !== "");
|
|
2031
|
+
if (hasData && prevHasData) {
|
|
2032
|
+
isNewRow = true;
|
|
2252
2033
|
}
|
|
2253
2034
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
} else {
|
|
2260
|
-
const allYLines = mergeSuperscriptLines(groupByY(items));
|
|
2261
|
-
const columns = detectColumns(allYLines);
|
|
2262
|
-
if (columns && columns.length >= 3) {
|
|
2263
|
-
const tableText = extractWithColumns(allYLines, columns);
|
|
2264
|
-
const bbox = computeBBox(items, pageNum);
|
|
2265
|
-
blocks.push({ type: "paragraph", text: tableText, pageNumber: pageNum, bbox, style: dominantStyle(items) });
|
|
2035
|
+
if (isNewRow && filledCount === 1 && row[0] && row[0].length <= 2) {
|
|
2036
|
+
isNewRow = false;
|
|
2037
|
+
}
|
|
2038
|
+
if (isNewRow) {
|
|
2039
|
+
merged.push([...row]);
|
|
2266
2040
|
} else {
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
const orderedGroups = xyCutOrder(items, gapThreshold);
|
|
2271
|
-
for (const group of orderedGroups) {
|
|
2272
|
-
if (group.length === 0) continue;
|
|
2273
|
-
const yLines = mergeSuperscriptLines(groupByY(group));
|
|
2274
|
-
const groupColumns = detectColumns(yLines);
|
|
2275
|
-
if (groupColumns && groupColumns.length >= 3) {
|
|
2276
|
-
const tableText = extractWithColumns(yLines, groupColumns);
|
|
2277
|
-
const bbox = computeBBox(group, pageNum);
|
|
2278
|
-
blocks.push({ type: "paragraph", text: tableText, pageNumber: pageNum, bbox, style: dominantStyle(group) });
|
|
2279
|
-
} else {
|
|
2280
|
-
for (const line of yLines) {
|
|
2281
|
-
const text = mergeLineSimple(line);
|
|
2282
|
-
if (!text.trim()) continue;
|
|
2283
|
-
const bbox = computeBBox(line, pageNum);
|
|
2284
|
-
blocks.push({ type: "paragraph", text, pageNumber: pageNum, bbox, style: dominantStyle(line) });
|
|
2285
|
-
}
|
|
2041
|
+
for (let c = 0; c < numCols; c++) {
|
|
2042
|
+
if (row[c]) {
|
|
2043
|
+
prev[c] = prev[c] ? prev[c] + " " + row[c] : row[c];
|
|
2286
2044
|
}
|
|
2287
2045
|
}
|
|
2288
2046
|
}
|
|
2289
2047
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
function computeBBox(items, pageNum) {
|
|
2293
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2294
|
-
for (const i of items) {
|
|
2295
|
-
if (i.x < minX) minX = i.x;
|
|
2296
|
-
if (i.y < minY) minY = i.y;
|
|
2297
|
-
if (i.x + i.w > maxX) maxX = i.x + i.w;
|
|
2298
|
-
const effectiveH = i.h > 0 ? i.h : i.fontSize;
|
|
2299
|
-
if (i.y + effectiveH > maxY) maxY = i.y + effectiveH;
|
|
2048
|
+
if (merged.length < 2) {
|
|
2049
|
+
return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
|
|
2300
2050
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
let maxCount = 0, dominantSize = 0;
|
|
2307
|
-
for (const i of items) {
|
|
2308
|
-
if (i.fontSize <= 0) continue;
|
|
2309
|
-
const count = (freq.get(i.fontSize) || 0) + 1;
|
|
2310
|
-
freq.set(i.fontSize, count);
|
|
2311
|
-
if (count > maxCount) {
|
|
2312
|
-
maxCount = count;
|
|
2313
|
-
dominantSize = i.fontSize;
|
|
2314
|
-
}
|
|
2051
|
+
let headerEnd = 0;
|
|
2052
|
+
for (let r = 0; r < merged.length; r++) {
|
|
2053
|
+
const hasDataValues = merged[r].slice(dataColStart).some((c) => c && /\d/.test(c));
|
|
2054
|
+
if (hasDataValues) break;
|
|
2055
|
+
headerEnd = r + 1;
|
|
2315
2056
|
}
|
|
2316
|
-
if (
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
for (const i of rawItems) {
|
|
2324
|
-
if (typeof i.str !== "string") continue;
|
|
2325
|
-
const x = Math.round(i.transform[4]);
|
|
2326
|
-
const y = Math.round(i.transform[5]);
|
|
2327
|
-
if (!i.str.trim()) {
|
|
2328
|
-
spacePositions.push({ x, y });
|
|
2329
|
-
continue;
|
|
2330
|
-
}
|
|
2331
|
-
const scaleY = Math.abs(i.transform[3]);
|
|
2332
|
-
const scaleX = Math.abs(i.transform[0]);
|
|
2333
|
-
const fontSize = Math.round(Math.max(scaleY, scaleX));
|
|
2334
|
-
const w = Math.round(i.width);
|
|
2335
|
-
const h = Math.round(i.height);
|
|
2336
|
-
const isHidden = fontSize === 0 || i.width === 0 && i.str.trim().length > 0;
|
|
2337
|
-
let text = i.str.trim();
|
|
2338
|
-
if (/^[\d\s\-().·,☎]+$/.test(text) && /\d/.test(text) && / /.test(text)) {
|
|
2339
|
-
text = text.replace(/ /g, "");
|
|
2340
|
-
}
|
|
2341
|
-
const split = splitEvenSpacedItem(text, x, w, fontSize);
|
|
2342
|
-
if (split) {
|
|
2343
|
-
for (const s of split) {
|
|
2344
|
-
items.push({ text: s.text, x: s.x, y, w: s.w, h, fontSize, fontName: i.fontName || "", isHidden });
|
|
2057
|
+
if (headerEnd > 1) {
|
|
2058
|
+
const headerRow = Array(numCols).fill("");
|
|
2059
|
+
for (let r = 0; r < headerEnd; r++) {
|
|
2060
|
+
for (let c = 0; c < numCols; c++) {
|
|
2061
|
+
if (merged[r][c]) {
|
|
2062
|
+
headerRow[c] = headerRow[c] ? headerRow[c] + " " + merged[r][c] : merged[r][c];
|
|
2063
|
+
}
|
|
2345
2064
|
}
|
|
2346
|
-
} else {
|
|
2347
|
-
items.push({ text, x, y, w, h, fontSize, fontName: i.fontName || "", isHidden });
|
|
2348
2065
|
}
|
|
2066
|
+
merged.splice(0, headerEnd, headerRow);
|
|
2349
2067
|
}
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
let isDup = false;
|
|
2354
|
-
for (let j = deduped.length - 1; j >= 0; j--) {
|
|
2355
|
-
const prev = deduped[j];
|
|
2356
|
-
if (prev.y - sorted[i].y > 3) break;
|
|
2357
|
-
if (Math.abs(prev.y - sorted[i].y) <= 3 && prev.text === sorted[i].text && Math.abs(prev.x - sorted[i].x) <= 3) {
|
|
2358
|
-
isDup = true;
|
|
2359
|
-
break;
|
|
2360
|
-
}
|
|
2068
|
+
for (const row of merged) {
|
|
2069
|
+
for (let c = 0; c < row.length; c++) {
|
|
2070
|
+
if (row[c]) row[c] = collapseEvenSpacing(row[c]);
|
|
2361
2071
|
}
|
|
2362
|
-
if (!isDup) deduped.push(sorted[i]);
|
|
2363
2072
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
if (Math.abs(sp.y - item.y) > 3) continue;
|
|
2369
|
-
const dist = item.x - sp.x;
|
|
2370
|
-
if (dist >= -1 && dist <= 20 && (!nearest || item.x < nearest.x)) {
|
|
2371
|
-
nearest = item;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
if (nearest) nearest.hasSpaceBefore = true;
|
|
2375
|
-
}
|
|
2073
|
+
const totalCells = merged.length * numCols;
|
|
2074
|
+
const filledCells = merged.reduce((s, row) => s + row.filter((c) => c).length, 0);
|
|
2075
|
+
if (filledCells < totalCells * 0.35 || merged.length < 2 || merged.length <= 3 && numCols >= 7) {
|
|
2076
|
+
return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
|
|
2376
2077
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
if (charW > fontSize * 2) return null;
|
|
2385
|
-
return chars.map((ch, idx) => ({
|
|
2386
|
-
text: ch,
|
|
2387
|
-
x: Math.round(itemX + idx * charW),
|
|
2388
|
-
w: Math.round(charW * 0.8)
|
|
2389
|
-
// 실제 글자 폭은 간격보다 좁음
|
|
2390
|
-
}));
|
|
2078
|
+
const md = [];
|
|
2079
|
+
md.push("| " + merged[0].join(" | ") + " |");
|
|
2080
|
+
md.push("| " + merged[0].map(() => "---").join(" | ") + " |");
|
|
2081
|
+
for (let r = 1; r < merged.length; r++) {
|
|
2082
|
+
md.push("| " + merged[r].join(" | ") + " |");
|
|
2083
|
+
}
|
|
2084
|
+
return md.join("\n");
|
|
2391
2085
|
}
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
let
|
|
2397
|
-
for (
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2086
|
+
|
|
2087
|
+
// src/pdf/block-detect.ts
|
|
2088
|
+
function computeMedianFontSizeFromFreq(freq) {
|
|
2089
|
+
if (freq.size === 0) return 0;
|
|
2090
|
+
let total = 0;
|
|
2091
|
+
for (const count of freq.values()) total += count;
|
|
2092
|
+
const sorted = [...freq.entries()].sort((a, b) => a[0] - b[0]);
|
|
2093
|
+
const mid = Math.floor(total / 2);
|
|
2094
|
+
let cumulative = 0;
|
|
2095
|
+
for (const [size, count] of sorted) {
|
|
2096
|
+
cumulative += count;
|
|
2097
|
+
if (cumulative > mid) return size;
|
|
2404
2098
|
}
|
|
2405
|
-
|
|
2406
|
-
return lines;
|
|
2099
|
+
return sorted[sorted.length - 1][0];
|
|
2407
2100
|
}
|
|
2408
|
-
function
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
const curr = lines[i];
|
|
2424
|
-
const a = band(prev);
|
|
2425
|
-
const b = band(curr);
|
|
2426
|
-
const overlap = Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom);
|
|
2427
|
-
const prevIsFrag = isFrag(prev) && a.height <= b.height * 0.8 && overlap >= a.height * 0.5;
|
|
2428
|
-
const currIsFrag = isFrag(curr) && b.height <= a.height * 0.8 && overlap >= b.height * 0.5;
|
|
2429
|
-
if (prevIsFrag || currIsFrag) {
|
|
2430
|
-
result[result.length - 1] = [...prev, ...curr];
|
|
2431
|
-
} else {
|
|
2432
|
-
result.push(curr);
|
|
2101
|
+
function detectHeadings(blocks, medianFontSize) {
|
|
2102
|
+
for (const block of blocks) {
|
|
2103
|
+
if (block.type !== "paragraph" || !block.text || !_optionalChain([block, 'access', _15 => _15.style, 'optionalAccess', _16 => _16.fontSize])) continue;
|
|
2104
|
+
const text = block.text.trim();
|
|
2105
|
+
if (text.length === 0 || text.length > 200) continue;
|
|
2106
|
+
if (/^\d+$/.test(text)) continue;
|
|
2107
|
+
const ratio = block.style.fontSize / medianFontSize;
|
|
2108
|
+
let level = 0;
|
|
2109
|
+
if (ratio >= _chunkYBPNKFJWcjs.HEADING_RATIO_H1) level = 1;
|
|
2110
|
+
else if (ratio >= _chunkYBPNKFJWcjs.HEADING_RATIO_H2) level = 2;
|
|
2111
|
+
else if (ratio >= _chunkYBPNKFJWcjs.HEADING_RATIO_H3) level = 3;
|
|
2112
|
+
if (level > 0) {
|
|
2113
|
+
block.type = "heading";
|
|
2114
|
+
block.level = level;
|
|
2115
|
+
block.text = collapseEvenSpacing(text);
|
|
2433
2116
|
}
|
|
2434
2117
|
}
|
|
2435
|
-
return result;
|
|
2436
2118
|
}
|
|
2437
|
-
function
|
|
2438
|
-
|
|
2439
|
-
const
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2119
|
+
function shouldDemoteTable(table) {
|
|
2120
|
+
const allCells = table.cells.flatMap((row) => row.map((c) => c.text.trim())).filter(Boolean);
|
|
2121
|
+
const allText = allCells.join(" ");
|
|
2122
|
+
if (table.rows <= 3 && table.cols <= 3) {
|
|
2123
|
+
const totalCells2 = table.rows * table.cols;
|
|
2124
|
+
const emptyCells2 = totalCells2 - allCells.length;
|
|
2125
|
+
if (emptyCells2 >= totalCells2 * 0.3) return true;
|
|
2126
|
+
if (/[□■◆○●▶ㅇ]/.test(allText)) return true;
|
|
2127
|
+
if (/<[^>]+>/.test(allText)) return true;
|
|
2443
2128
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2129
|
+
if (allText.length > 200) return false;
|
|
2130
|
+
if (/[□■◆○●▶]/.test(allText) && table.rows <= 3) return true;
|
|
2131
|
+
const totalCells = table.rows * table.cols;
|
|
2132
|
+
const emptyCells = totalCells - allCells.length;
|
|
2133
|
+
if (table.rows <= 2 && emptyCells > totalCells * 0.5) return true;
|
|
2134
|
+
if (table.rows === 1 && !/\d{2,}/.test(allText)) return true;
|
|
2135
|
+
return false;
|
|
2447
2136
|
}
|
|
2448
|
-
function
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
break;
|
|
2137
|
+
function demoteTableToText(table) {
|
|
2138
|
+
const lines = [];
|
|
2139
|
+
for (let r = 0; r < table.rows; r++) {
|
|
2140
|
+
const cells = table.cells[r].map((c) => c.text.trim()).filter(Boolean);
|
|
2141
|
+
if (cells.length === 0) continue;
|
|
2142
|
+
if (table.cols === 2 && cells.length === 2) {
|
|
2143
|
+
lines.push(`${cells[0]} : ${cells[1]}`);
|
|
2144
|
+
} else {
|
|
2145
|
+
lines.push(cells.join(" "));
|
|
2458
2146
|
}
|
|
2459
2147
|
}
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
for (
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
c.count++;
|
|
2472
|
-
found = true;
|
|
2473
|
-
break;
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
if (!found) {
|
|
2477
|
-
xClusters.push({ center: item.x, count: 1, minX: item.x });
|
|
2478
|
-
}
|
|
2148
|
+
return lines.join("\n");
|
|
2149
|
+
}
|
|
2150
|
+
function detectMarkerHeadings(blocks) {
|
|
2151
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2152
|
+
const block = blocks[i];
|
|
2153
|
+
if (block.type !== "paragraph" || !block.text) continue;
|
|
2154
|
+
const text = block.text.trim();
|
|
2155
|
+
if (text.length < 50 && /^[□■◆◇▶]\s*[가-힣]/.test(text)) {
|
|
2156
|
+
block.type = "heading";
|
|
2157
|
+
block.level = 4;
|
|
2158
|
+
continue;
|
|
2479
2159
|
}
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
if (peaks[i].count > prev.count) {
|
|
2489
|
-
prev.center = peaks[i].center;
|
|
2160
|
+
if (/^[가-힣]{2,6}$/.test(text) && _optionalChain([block, 'access', _17 => _17.style, 'optionalAccess', _18 => _18.fontSize])) {
|
|
2161
|
+
const prev = blocks[i - 1];
|
|
2162
|
+
const next = blocks[i + 1];
|
|
2163
|
+
const prevIsStructural = !prev || prev.type === "table" || prev.type === "heading" || prev.type === "separator";
|
|
2164
|
+
const nextIsStructural = !next || next.type === "table" || next.type === "heading" || next.type === "paragraph" && next.text && /^[□■◆○●]/.test(next.text.trim());
|
|
2165
|
+
if (prevIsStructural || nextIsStructural) {
|
|
2166
|
+
block.type = "heading";
|
|
2167
|
+
block.level = 3;
|
|
2490
2168
|
}
|
|
2491
|
-
prev.count += peaks[i].count;
|
|
2492
|
-
prev.minX = Math.min(prev.minX, peaks[i].minX);
|
|
2493
|
-
} else {
|
|
2494
|
-
merged.push({ ...peaks[i] });
|
|
2495
2169
|
}
|
|
2496
2170
|
}
|
|
2497
|
-
const rawColumns = merged.filter((c) => c.count >= 3).map((c) => c.minX);
|
|
2498
|
-
if (rawColumns.length < 3) return null;
|
|
2499
|
-
const MIN_DETECT_COL_WIDTH = 30;
|
|
2500
|
-
const columns = [rawColumns[0]];
|
|
2501
|
-
for (let i = 1; i < rawColumns.length; i++) {
|
|
2502
|
-
if (rawColumns[i] - columns[columns.length - 1] < MIN_DETECT_COL_WIDTH) continue;
|
|
2503
|
-
columns.push(rawColumns[i]);
|
|
2504
|
-
}
|
|
2505
|
-
return columns.length >= 3 ? columns : null;
|
|
2506
2171
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2172
|
+
var TABLE_CAPTION_RE = /^[<\[(【〈]?\s*(표|그림|도표|Table|Figure|Fig\.?)\s*[\d①-⑮][\d.\-]*\s*[\])】〉>]?[.:]?\s*/i;
|
|
2173
|
+
var CAPTION_MAX_LENGTH = 100;
|
|
2174
|
+
var CAPTION_MAX_GAP = 30;
|
|
2175
|
+
function detectTableCaptions(blocks) {
|
|
2176
|
+
const isCaptionCandidate = (b, table) => {
|
|
2177
|
+
if (!b || b.type !== "paragraph" || !b.text) return false;
|
|
2178
|
+
if (b.pageNumber !== table.pageNumber) return false;
|
|
2179
|
+
const text = b.text.trim();
|
|
2180
|
+
if (!text || text.length > CAPTION_MAX_LENGTH || text.includes("\n")) return false;
|
|
2181
|
+
if (!TABLE_CAPTION_RE.test(text)) return false;
|
|
2182
|
+
if (b.bbox && table.bbox) {
|
|
2183
|
+
const capTop = b.bbox.y + b.bbox.height;
|
|
2184
|
+
const capBottom = b.bbox.y;
|
|
2185
|
+
const tblTop = table.bbox.y + table.bbox.height;
|
|
2186
|
+
const tblBottom = table.bbox.y;
|
|
2187
|
+
const gap = capBottom >= tblTop ? capBottom - tblTop : tblBottom - capTop;
|
|
2188
|
+
if (gap > CAPTION_MAX_GAP) return false;
|
|
2189
|
+
const overlap = Math.min(b.bbox.x + b.bbox.width, table.bbox.x + table.bbox.width) - Math.max(b.bbox.x, table.bbox.x);
|
|
2190
|
+
if (overlap < Math.min(b.bbox.width, table.bbox.width) * 0.3) return false;
|
|
2191
|
+
}
|
|
2192
|
+
return true;
|
|
2193
|
+
};
|
|
2194
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2195
|
+
const block = blocks[i];
|
|
2196
|
+
if (block.type !== "table" || !block.table || block.table.caption) continue;
|
|
2197
|
+
if (isCaptionCandidate(blocks[i - 1], block)) {
|
|
2198
|
+
block.table.caption = blocks[i - 1].text.trim();
|
|
2199
|
+
blocks.splice(i - 1, 1);
|
|
2200
|
+
i--;
|
|
2201
|
+
} else if (isCaptionCandidate(blocks[i + 1], block)) {
|
|
2202
|
+
block.table.caption = blocks[i + 1].text.trim();
|
|
2203
|
+
blocks.splice(i + 1, 1);
|
|
2204
|
+
}
|
|
2510
2205
|
}
|
|
2511
|
-
return 0;
|
|
2512
2206
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
break;
|
|
2522
|
-
}
|
|
2207
|
+
var KOREAN_LIST_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
|
|
2208
|
+
function parseListLabel(text) {
|
|
2209
|
+
let m = text.match(/^(\d{1,2})\.(?!\d)\s+/);
|
|
2210
|
+
if (m) return { family: "arabicDot", ord: parseInt(m[1], 10) };
|
|
2211
|
+
m = text.match(/^([가-하])\.\s+/);
|
|
2212
|
+
if (m) {
|
|
2213
|
+
const idx = KOREAN_LIST_SEQ.indexOf(m[1]);
|
|
2214
|
+
if (idx >= 0) return { family: "korDot", ord: idx + 1 };
|
|
2523
2215
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
}
|
|
2216
|
+
m = text.match(/^(\d{1,2})\)\s*/);
|
|
2217
|
+
if (m) return { family: "arabicParen", ord: parseInt(m[1], 10) };
|
|
2218
|
+
m = text.match(/^([가-하])\)\s*/);
|
|
2219
|
+
if (m) {
|
|
2220
|
+
const idx = KOREAN_LIST_SEQ.indexOf(m[1]);
|
|
2221
|
+
if (idx >= 0) return { family: "korParen", ord: idx + 1 };
|
|
2531
2222
|
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2223
|
+
m = text.match(/^([①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮])\s*/);
|
|
2224
|
+
if (m) return { family: "circled", ord: m[1].charCodeAt(0) - 9312 + 1 };
|
|
2225
|
+
return null;
|
|
2226
|
+
}
|
|
2227
|
+
var ATTACHMENT_RE = /^붙\s*임\s*(\d+[.:]?)?\s/;
|
|
2228
|
+
function detectKoreanListBlocks(blocks) {
|
|
2229
|
+
const labeled = [];
|
|
2230
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2231
|
+
const b = blocks[i];
|
|
2232
|
+
if (b.type !== "paragraph" && b.type !== "list" || !b.text) continue;
|
|
2233
|
+
const label = parseListLabel(b.text.trim());
|
|
2234
|
+
if (label) labeled.push({ idx: i, label });
|
|
2535
2235
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2236
|
+
const validated = /* @__PURE__ */ new Set();
|
|
2237
|
+
const byFamily = /* @__PURE__ */ new Map();
|
|
2238
|
+
for (const l of labeled) {
|
|
2239
|
+
const arr = byFamily.get(l.label.family) || [];
|
|
2240
|
+
arr.push(l);
|
|
2241
|
+
byFamily.set(l.label.family, arr);
|
|
2242
|
+
}
|
|
2243
|
+
for (const arr of byFamily.values()) {
|
|
2244
|
+
let chain = [];
|
|
2245
|
+
for (const item of arr) {
|
|
2246
|
+
const prev = chain[chain.length - 1];
|
|
2247
|
+
if (prev && item.label.ord === prev.label.ord + 1 && item.idx - prev.idx <= 20) {
|
|
2248
|
+
chain.push(item);
|
|
2545
2249
|
} else {
|
|
2546
|
-
if (
|
|
2547
|
-
|
|
2548
|
-
}
|
|
2549
|
-
result.push(mergeLineSimple(line));
|
|
2250
|
+
if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
|
|
2251
|
+
chain = [item];
|
|
2550
2252
|
}
|
|
2551
2253
|
}
|
|
2552
|
-
if (
|
|
2553
|
-
result.push(buildGridTable(gridLines, columns));
|
|
2554
|
-
}
|
|
2254
|
+
if (chain.length >= 2) for (const c of chain) validated.add(c.idx);
|
|
2555
2255
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
function buildGridTable(lines, columns) {
|
|
2565
|
-
const numCols = columns.length;
|
|
2566
|
-
const yRows = lines.map((items) => {
|
|
2567
|
-
const row = Array(numCols).fill("");
|
|
2568
|
-
for (const item of items) {
|
|
2569
|
-
const col = findColumn(item.x, columns);
|
|
2570
|
-
row[col] = row[col] ? row[col] + " " + item.text : item.text;
|
|
2571
|
-
}
|
|
2572
|
-
return row;
|
|
2573
|
-
});
|
|
2574
|
-
const dataColStart = Math.max(2, Math.floor(numCols / 2));
|
|
2575
|
-
const merged = [];
|
|
2576
|
-
for (const row of yRows) {
|
|
2577
|
-
if (row.every((c) => c === "")) continue;
|
|
2578
|
-
if (merged.length === 0) {
|
|
2579
|
-
merged.push([...row]);
|
|
2256
|
+
let familyStack = [];
|
|
2257
|
+
let lastTopLevelList = null;
|
|
2258
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
2259
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2260
|
+
const b = blocks[i];
|
|
2261
|
+
if (b.type === "table" || b.type === "heading" || b.type === "separator") {
|
|
2262
|
+
familyStack = [];
|
|
2263
|
+
lastTopLevelList = null;
|
|
2580
2264
|
continue;
|
|
2581
2265
|
}
|
|
2582
|
-
|
|
2583
|
-
const
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
isNewRow = true;
|
|
2588
|
-
}
|
|
2589
|
-
if (!isNewRow && numCols > 1 && row[1]) {
|
|
2590
|
-
isNewRow = true;
|
|
2591
|
-
}
|
|
2592
|
-
if (!isNewRow) {
|
|
2593
|
-
const hasData = row.slice(dataColStart).some((c) => c !== "");
|
|
2594
|
-
const prevHasData = prev.slice(dataColStart).some((c) => c !== "");
|
|
2595
|
-
if (hasData && prevHasData) {
|
|
2596
|
-
isNewRow = true;
|
|
2597
|
-
}
|
|
2266
|
+
if (b.type !== "paragraph" && b.type !== "list" || !b.text) continue;
|
|
2267
|
+
const text = b.text.trim();
|
|
2268
|
+
if (b.type === "paragraph" && ATTACHMENT_RE.test(text)) {
|
|
2269
|
+
blocks[i] = { ...b, type: "list", listType: "unordered" };
|
|
2270
|
+
continue;
|
|
2598
2271
|
}
|
|
2599
|
-
if (
|
|
2600
|
-
|
|
2272
|
+
if (!validated.has(i)) continue;
|
|
2273
|
+
const label = parseListLabel(text);
|
|
2274
|
+
let depth = familyStack.indexOf(label.family);
|
|
2275
|
+
if (depth < 0) {
|
|
2276
|
+
familyStack.push(label.family);
|
|
2277
|
+
depth = familyStack.length - 1;
|
|
2278
|
+
} else {
|
|
2279
|
+
familyStack = familyStack.slice(0, depth + 1);
|
|
2601
2280
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2281
|
+
const listType = label.family === "arabicDot" ? "ordered" : "unordered";
|
|
2282
|
+
const listBlock = { ...b, type: "list", listType };
|
|
2283
|
+
if (depth === 0) {
|
|
2284
|
+
blocks[i] = listBlock;
|
|
2285
|
+
lastTopLevelList = listBlock;
|
|
2286
|
+
} else if (lastTopLevelList) {
|
|
2287
|
+
if (!lastTopLevelList.children) lastTopLevelList.children = [];
|
|
2288
|
+
lastTopLevelList.children.push(listBlock);
|
|
2289
|
+
toRemove.add(i);
|
|
2604
2290
|
} else {
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
prev[c] = prev[c] ? prev[c] + " " + row[c] : row[c];
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2291
|
+
blocks[i] = listBlock;
|
|
2292
|
+
lastTopLevelList = listBlock;
|
|
2610
2293
|
}
|
|
2611
2294
|
}
|
|
2612
|
-
if (
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
let headerEnd = 0;
|
|
2616
|
-
for (let r = 0; r < merged.length; r++) {
|
|
2617
|
-
const hasDataValues = merged[r].slice(dataColStart).some((c) => c && /\d/.test(c));
|
|
2618
|
-
if (hasDataValues) break;
|
|
2619
|
-
headerEnd = r + 1;
|
|
2295
|
+
if (toRemove.size > 0) {
|
|
2296
|
+
const sorted = [...toRemove].sort((a, b) => b - a);
|
|
2297
|
+
for (const idx of sorted) blocks.splice(idx, 1);
|
|
2620
2298
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2299
|
+
}
|
|
2300
|
+
function detectListBlocks(blocks) {
|
|
2301
|
+
const result = [];
|
|
2302
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
2303
|
+
const block = blocks[i];
|
|
2304
|
+
if (block.type === "paragraph" && block.text) {
|
|
2305
|
+
const text = block.text.trim();
|
|
2306
|
+
if (/^\d+\.\s/.test(text)) {
|
|
2307
|
+
result.push({ ...block, type: "list", listType: "ordered", text: block.text });
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
|
|
2311
|
+
result.push({ ...block, type: "list", listType: "unordered", text: block.text });
|
|
2312
|
+
continue;
|
|
2628
2313
|
}
|
|
2629
2314
|
}
|
|
2630
|
-
|
|
2631
|
-
}
|
|
2632
|
-
for (const row of merged) {
|
|
2633
|
-
for (let c = 0; c < row.length; c++) {
|
|
2634
|
-
if (row[c]) row[c] = collapseEvenSpacing(row[c]);
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
const totalCells = merged.length * numCols;
|
|
2638
|
-
const filledCells = merged.reduce((s, row) => s + row.filter((c) => c).length, 0);
|
|
2639
|
-
if (filledCells < totalCells * 0.35 || merged.length < 2 || merged.length <= 3 && numCols >= 7) {
|
|
2640
|
-
return merged.map((r) => r.filter((c) => c).join(" ")).join("\n");
|
|
2641
|
-
}
|
|
2642
|
-
const md = [];
|
|
2643
|
-
md.push("| " + merged[0].join(" | ") + " |");
|
|
2644
|
-
md.push("| " + merged[0].map(() => "---").join(" | ") + " |");
|
|
2645
|
-
for (let r = 1; r < merged.length; r++) {
|
|
2646
|
-
md.push("| " + merged[r].join(" | ") + " |");
|
|
2315
|
+
result.push(block);
|
|
2647
2316
|
}
|
|
2648
|
-
return
|
|
2317
|
+
return result;
|
|
2649
2318
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
const
|
|
2654
|
-
let
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
result += " ";
|
|
2661
|
-
result += sorted[i].text;
|
|
2662
|
-
continue;
|
|
2663
|
-
}
|
|
2664
|
-
if (isEvenSpaced[i]) {
|
|
2665
|
-
result += sorted[i].text;
|
|
2666
|
-
continue;
|
|
2319
|
+
var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
|
|
2320
|
+
var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
|
|
2321
|
+
function detectSpecialKoreanTables(blocks) {
|
|
2322
|
+
const result = [];
|
|
2323
|
+
let kvLines = [];
|
|
2324
|
+
const flushKvTable = () => {
|
|
2325
|
+
if (kvLines.length < 2) {
|
|
2326
|
+
for (const kv of kvLines) result.push(kv.block);
|
|
2327
|
+
kvLines = [];
|
|
2328
|
+
return;
|
|
2667
2329
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2330
|
+
const cells = kvLines.map((kv) => {
|
|
2331
|
+
if (kv.value) {
|
|
2332
|
+
return [
|
|
2333
|
+
{ text: kv.key, colSpan: 1, rowSpan: 1 },
|
|
2334
|
+
{ text: kv.value, colSpan: 1, rowSpan: 1 }
|
|
2335
|
+
];
|
|
2336
|
+
}
|
|
2337
|
+
return [
|
|
2338
|
+
{ text: kv.key, colSpan: 2, rowSpan: 1 },
|
|
2339
|
+
{ text: "", colSpan: 1, rowSpan: 1 }
|
|
2340
|
+
];
|
|
2341
|
+
});
|
|
2342
|
+
const irTable = {
|
|
2343
|
+
rows: cells.length,
|
|
2344
|
+
cols: 2,
|
|
2345
|
+
cells,
|
|
2346
|
+
hasHeader: true
|
|
2347
|
+
};
|
|
2348
|
+
const firstBlock = kvLines[0].block;
|
|
2349
|
+
result.push({
|
|
2350
|
+
type: "table",
|
|
2351
|
+
table: irTable,
|
|
2352
|
+
pageNumber: firstBlock.pageNumber,
|
|
2353
|
+
bbox: firstBlock.bbox
|
|
2354
|
+
});
|
|
2355
|
+
kvLines = [];
|
|
2356
|
+
};
|
|
2357
|
+
for (const block of blocks) {
|
|
2358
|
+
if (block.type !== "paragraph" || !block.text) {
|
|
2359
|
+
flushKvTable();
|
|
2360
|
+
result.push(block);
|
|
2671
2361
|
continue;
|
|
2672
2362
|
}
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2363
|
+
const text = block.text.trim();
|
|
2364
|
+
if (KOREAN_TABLE_HEADER_RE.test(text)) {
|
|
2365
|
+
const colonIdx = text.indexOf(":");
|
|
2366
|
+
if (colonIdx >= 0) {
|
|
2367
|
+
kvLines.push({
|
|
2368
|
+
key: text.slice(0, colonIdx).trim(),
|
|
2369
|
+
value: text.slice(colonIdx + 1).trim(),
|
|
2370
|
+
block
|
|
2371
|
+
});
|
|
2372
|
+
} else {
|
|
2373
|
+
const spaceIdx = text.search(/\s/);
|
|
2374
|
+
if (spaceIdx > 0) {
|
|
2375
|
+
kvLines.push({
|
|
2376
|
+
key: text.slice(0, spaceIdx).trim(),
|
|
2377
|
+
value: text.slice(spaceIdx + 1).trim(),
|
|
2378
|
+
block
|
|
2379
|
+
});
|
|
2380
|
+
} else {
|
|
2381
|
+
kvLines.push({ key: text, value: "", block });
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2676
2384
|
continue;
|
|
2677
2385
|
}
|
|
2678
|
-
if (
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
if (cell.text) cell.text = stripControlChars(cell.text);
|
|
2386
|
+
if (kvLines.length > 0 && text.includes(":")) {
|
|
2387
|
+
if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
|
|
2388
|
+
const colonIdx = text.indexOf(":");
|
|
2389
|
+
const key = text.slice(0, colonIdx).trim();
|
|
2390
|
+
if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
|
|
2391
|
+
kvLines.push({
|
|
2392
|
+
key,
|
|
2393
|
+
value: text.slice(colonIdx + 1).trim(),
|
|
2394
|
+
block
|
|
2395
|
+
});
|
|
2396
|
+
continue;
|
|
2690
2397
|
}
|
|
2691
2398
|
}
|
|
2692
2399
|
}
|
|
2693
|
-
|
|
2400
|
+
flushKvTable();
|
|
2401
|
+
result.push(block);
|
|
2694
2402
|
}
|
|
2403
|
+
flushKvTable();
|
|
2404
|
+
return result;
|
|
2695
2405
|
}
|
|
2696
|
-
function
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
const capBottom = b.bbox.y;
|
|
2724
|
-
const tblTop = table.bbox.y + table.bbox.height;
|
|
2725
|
-
const tblBottom = table.bbox.y;
|
|
2726
|
-
const gap = capBottom >= tblTop ? capBottom - tblTop : tblBottom - capTop;
|
|
2727
|
-
if (gap > CAPTION_MAX_GAP) return false;
|
|
2728
|
-
const overlap = Math.min(b.bbox.x + b.bbox.width, table.bbox.x + table.bbox.width) - Math.max(b.bbox.x, table.bbox.x);
|
|
2729
|
-
if (overlap < Math.min(b.bbox.width, table.bbox.width) * 0.3) return false;
|
|
2406
|
+
function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
|
|
2407
|
+
const ZONE_RATIO = 0.12;
|
|
2408
|
+
const MIN_REPEAT = 3;
|
|
2409
|
+
const topEntries = [];
|
|
2410
|
+
const bottomEntries = [];
|
|
2411
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2412
|
+
const b = blocks[bi];
|
|
2413
|
+
if (!b.bbox || !b.pageNumber || !_optionalChain([b, 'access', _19 => _19.text, 'optionalAccess', _20 => _20.trim, 'call', _21 => _21()])) continue;
|
|
2414
|
+
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2415
|
+
if (!ph) continue;
|
|
2416
|
+
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2417
|
+
const blockBottom = ph - b.bbox.y;
|
|
2418
|
+
const entry = { blockIdx: bi, page: b.pageNumber, text: b.text.trim() };
|
|
2419
|
+
if (blockBottom <= ph * ZONE_RATIO) bottomEntries.push(entry);
|
|
2420
|
+
else if (blockTop >= ph * (1 - ZONE_RATIO)) topEntries.push(entry);
|
|
2421
|
+
}
|
|
2422
|
+
const removeSet = /* @__PURE__ */ new Set();
|
|
2423
|
+
for (const entries of [topEntries, bottomEntries]) {
|
|
2424
|
+
if (entries.length === 0) continue;
|
|
2425
|
+
const patternCount = /* @__PURE__ */ new Map();
|
|
2426
|
+
const patternPages = /* @__PURE__ */ new Map();
|
|
2427
|
+
for (const e of entries) {
|
|
2428
|
+
const norm = e.text.replace(/\d+/g, "#");
|
|
2429
|
+
patternCount.set(norm, (patternCount.get(norm) || 0) + 1);
|
|
2430
|
+
const pages = patternPages.get(norm) || /* @__PURE__ */ new Set();
|
|
2431
|
+
pages.add(e.page);
|
|
2432
|
+
patternPages.set(norm, pages);
|
|
2730
2433
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
blocks.splice(i + 1, 1);
|
|
2434
|
+
const repeatedPatterns = /* @__PURE__ */ new Set();
|
|
2435
|
+
for (const [p, count] of patternCount) {
|
|
2436
|
+
if (count >= MIN_REPEAT && (_nullishCoalesce(_optionalChain([patternPages, 'access', _22 => _22.get, 'call', _23 => _23(p), 'optionalAccess', _24 => _24.size]), () => ( 0))) >= MIN_REPEAT) {
|
|
2437
|
+
repeatedPatterns.add(p);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
for (const e of entries) {
|
|
2441
|
+
const norm = e.text.replace(/\d+/g, "#");
|
|
2442
|
+
if (repeatedPatterns.has(norm)) {
|
|
2443
|
+
removeSet.add(e.blockIdx);
|
|
2444
|
+
}
|
|
2743
2445
|
}
|
|
2744
2446
|
}
|
|
2447
|
+
if (removeSet.size > 0) {
|
|
2448
|
+
warnings.push({ message: `${removeSet.size}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
2449
|
+
}
|
|
2450
|
+
return [...removeSet].sort((a, b) => a - b);
|
|
2745
2451
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
if (
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2452
|
+
|
|
2453
|
+
// src/pdf/page-blocks.ts
|
|
2454
|
+
function extractPageBlocksWithLines(items, pageNum, opList, pageWidth, pageHeight) {
|
|
2455
|
+
if (items.length === 0) return [];
|
|
2456
|
+
let { horizontals, verticals } = extractLines(opList.fnArray, opList.argsArray);
|
|
2457
|
+
({ horizontals, verticals } = filterPageBorderLines(horizontals, verticals, pageWidth, pageHeight));
|
|
2458
|
+
({ horizontals, verticals } = preprocessLines(horizontals, verticals));
|
|
2459
|
+
markStrikethroughItems(items, horizontals);
|
|
2460
|
+
wrapStrikethroughRuns(items);
|
|
2461
|
+
const grids = buildTableGrids(horizontals, verticals);
|
|
2462
|
+
if (grids.length > 0) {
|
|
2463
|
+
return extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals);
|
|
2754
2464
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2465
|
+
return extractPageBlocksFallback(items, pageNum);
|
|
2466
|
+
}
|
|
2467
|
+
var STRIKE_MAX_THICKNESS = 2;
|
|
2468
|
+
var STRIKE_MAX_THICKNESS_RATIO = 0.25;
|
|
2469
|
+
var STRIKE_CENTER_TOLERANCE = 0.25;
|
|
2470
|
+
var STRIKE_MIN_OVERLAP_RATIO = 0.8;
|
|
2471
|
+
var STRIKE_MAX_LINE_TO_TEXT_RATIO = 1.5;
|
|
2472
|
+
function markStrikethroughItems(items, horizontals) {
|
|
2473
|
+
if (items.length === 0 || horizontals.length === 0) return;
|
|
2474
|
+
for (const line of horizontals) {
|
|
2475
|
+
if (line.lineWidth > STRIKE_MAX_THICKNESS) continue;
|
|
2476
|
+
const matches = [];
|
|
2477
|
+
for (const item of items) {
|
|
2478
|
+
const h = item.h > 0 ? item.h : item.fontSize;
|
|
2479
|
+
if (h <= 0 || item.w <= 0) continue;
|
|
2480
|
+
if (line.lineWidth > h * STRIKE_MAX_THICKNESS_RATIO) continue;
|
|
2481
|
+
const centerY = item.y + h * 0.4;
|
|
2482
|
+
if (Math.abs(line.y1 - centerY) > h * STRIKE_CENTER_TOLERANCE) continue;
|
|
2483
|
+
const overlap = Math.min(line.x2, item.x + item.w) - Math.max(line.x1, item.x);
|
|
2484
|
+
if (overlap / item.w < STRIKE_MIN_OVERLAP_RATIO) continue;
|
|
2485
|
+
matches.push(item);
|
|
2486
|
+
}
|
|
2487
|
+
if (matches.length === 0) continue;
|
|
2488
|
+
let totalW = 0;
|
|
2489
|
+
for (const m of matches) totalW += m.w;
|
|
2490
|
+
if (totalW <= 0 || (line.x2 - line.x1) / totalW > STRIKE_MAX_LINE_TO_TEXT_RATIO) continue;
|
|
2491
|
+
for (const m of matches) m.strike = true;
|
|
2761
2492
|
}
|
|
2762
|
-
m = text.match(/^([①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮])\s*/);
|
|
2763
|
-
if (m) return { family: "circled", ord: m[1].charCodeAt(0) - 9312 + 1 };
|
|
2764
|
-
return null;
|
|
2765
2493
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
const
|
|
2773
|
-
|
|
2494
|
+
function wrapStrikethroughRuns(items) {
|
|
2495
|
+
const struck = items.filter((i) => i.strike);
|
|
2496
|
+
if (struck.length === 0) return;
|
|
2497
|
+
const lines = /* @__PURE__ */ new Map();
|
|
2498
|
+
for (const item of struck) {
|
|
2499
|
+
const key = Math.round(item.y / 3);
|
|
2500
|
+
const arr = lines.get(key) || [];
|
|
2501
|
+
arr.push(item);
|
|
2502
|
+
lines.set(key, arr);
|
|
2774
2503
|
}
|
|
2775
|
-
const
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
arr.push(l);
|
|
2780
|
-
byFamily.set(l.label.family, arr);
|
|
2504
|
+
for (const arr of lines.values()) {
|
|
2505
|
+
arr.sort((a, b) => a.x - b.x);
|
|
2506
|
+
arr[0].text = "~~" + arr[0].text;
|
|
2507
|
+
arr[arr.length - 1].text = arr[arr.length - 1].text + "~~";
|
|
2781
2508
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2509
|
+
}
|
|
2510
|
+
function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
|
|
2511
|
+
const blocks = [];
|
|
2512
|
+
const usedItems = /* @__PURE__ */ new Set();
|
|
2513
|
+
const sortedGrids = [...grids].sort((a, b) => b.bbox.y2 - a.bbox.y2);
|
|
2514
|
+
for (const grid of sortedGrids) {
|
|
2515
|
+
const numGridRows = grid.rowYs.length - 1;
|
|
2516
|
+
const numGridCols = grid.colXs.length - 1;
|
|
2517
|
+
if (numGridRows === 1 && numGridCols >= 2) continue;
|
|
2518
|
+
if (numGridCols === 1 && numGridRows >= 2) continue;
|
|
2519
|
+
const tableItems = [];
|
|
2520
|
+
const pad = 3;
|
|
2521
|
+
const gridW = grid.bbox.x2 - grid.bbox.x1;
|
|
2522
|
+
for (const item of items) {
|
|
2523
|
+
if (usedItems.has(item)) continue;
|
|
2524
|
+
if (item.y < grid.bbox.y1 - pad || item.y > grid.bbox.y2 + pad) continue;
|
|
2525
|
+
if (item.x < grid.bbox.x1 - pad || item.x + item.w > grid.bbox.x2 + pad) continue;
|
|
2526
|
+
if (gridW < 120 && item.x + item.w > grid.bbox.x2 - 2) continue;
|
|
2527
|
+
tableItems.push(item);
|
|
2528
|
+
usedItems.add(item);
|
|
2529
|
+
}
|
|
2530
|
+
const cells = extractCells(grid, horizontals, verticals);
|
|
2531
|
+
if (cells.length === 0) continue;
|
|
2532
|
+
const textItems = tableItems.map((i) => ({
|
|
2533
|
+
text: i.text,
|
|
2534
|
+
x: i.x,
|
|
2535
|
+
y: i.y,
|
|
2536
|
+
w: i.w,
|
|
2537
|
+
h: i.h,
|
|
2538
|
+
fontSize: i.fontSize,
|
|
2539
|
+
fontName: i.fontName,
|
|
2540
|
+
hasSpaceBefore: i.hasSpaceBefore
|
|
2541
|
+
}));
|
|
2542
|
+
const cellTextMap = mapTextToCells(textItems, cells);
|
|
2543
|
+
const numRows = grid.rowYs.length - 1;
|
|
2544
|
+
const numCols = grid.colXs.length - 1;
|
|
2545
|
+
const irGrid = Array.from(
|
|
2546
|
+
{ length: numRows },
|
|
2547
|
+
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
2548
|
+
);
|
|
2549
|
+
for (const cell of cells) {
|
|
2550
|
+
const cellItems = cellTextMap.get(cell) || [];
|
|
2551
|
+
let text = cellTextToString(cellItems);
|
|
2552
|
+
text = text.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
|
|
2553
|
+
text = text.split("\n").map((line) => collapseEvenSpacing(line)).join("\n");
|
|
2554
|
+
irGrid[cell.row][cell.col] = {
|
|
2555
|
+
text,
|
|
2556
|
+
colSpan: cell.colSpan,
|
|
2557
|
+
rowSpan: cell.rowSpan
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
let finalGrid = irGrid;
|
|
2561
|
+
let finalRows = numRows;
|
|
2562
|
+
if (numRows <= 2 && numCols >= 3) {
|
|
2563
|
+
const rebuilt = normalizeUndersegmentedTable(irGrid, grid.colXs, textItems);
|
|
2564
|
+
if (rebuilt) {
|
|
2565
|
+
finalGrid = rebuilt.map((row) => row.map((rawText) => {
|
|
2566
|
+
const cleaned = rawText.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
|
|
2567
|
+
return {
|
|
2568
|
+
text: cleaned.split("\n").map((line) => collapseEvenSpacing(line)).join("\n"),
|
|
2569
|
+
colSpan: 1,
|
|
2570
|
+
rowSpan: 1
|
|
2571
|
+
};
|
|
2572
|
+
}));
|
|
2573
|
+
finalRows = finalGrid.length;
|
|
2791
2574
|
}
|
|
2792
2575
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
const
|
|
2800
|
-
if (
|
|
2801
|
-
|
|
2802
|
-
|
|
2576
|
+
const irTable = {
|
|
2577
|
+
rows: finalRows,
|
|
2578
|
+
cols: numCols,
|
|
2579
|
+
cells: finalGrid,
|
|
2580
|
+
hasHeader: finalRows > 1
|
|
2581
|
+
};
|
|
2582
|
+
const hasContent = finalGrid.some((row) => row.some((cell) => cell.text.trim() !== ""));
|
|
2583
|
+
if (!hasContent) continue;
|
|
2584
|
+
const tableBbox = {
|
|
2585
|
+
page: pageNum,
|
|
2586
|
+
x: grid.bbox.x1,
|
|
2587
|
+
y: grid.bbox.y1,
|
|
2588
|
+
width: grid.bbox.x2 - grid.bbox.x1,
|
|
2589
|
+
height: grid.bbox.y2 - grid.bbox.y1
|
|
2590
|
+
};
|
|
2591
|
+
if (shouldDemoteTable(irTable)) {
|
|
2592
|
+
const demoted = demoteTableToText(irTable);
|
|
2593
|
+
if (demoted) {
|
|
2594
|
+
const text = numGridRows === 1 ? "\n" + demoted + "\n" : demoted;
|
|
2595
|
+
blocks.push({ type: "paragraph", text, pageNumber: pageNum, bbox: tableBbox, style: dominantStyle(tableItems) });
|
|
2596
|
+
}
|
|
2803
2597
|
continue;
|
|
2804
2598
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2599
|
+
blocks.push({ type: "table", table: irTable, pageNumber: pageNum, bbox: tableBbox });
|
|
2600
|
+
}
|
|
2601
|
+
let remaining = items.filter((i) => !usedItems.has(i));
|
|
2602
|
+
if (remaining.length > 0) {
|
|
2603
|
+
remaining.sort((a, b) => b.y - a.y || a.x - b.x);
|
|
2604
|
+
const clusterItems = remaining.map((i) => ({
|
|
2605
|
+
text: i.text,
|
|
2606
|
+
x: i.x,
|
|
2607
|
+
y: i.y,
|
|
2608
|
+
w: i.w,
|
|
2609
|
+
h: i.h,
|
|
2610
|
+
fontSize: i.fontSize,
|
|
2611
|
+
fontName: i.fontName,
|
|
2612
|
+
hasSpaceBefore: i.hasSpaceBefore
|
|
2613
|
+
}));
|
|
2614
|
+
const clusterResults = detectClusterTables(clusterItems, pageNum);
|
|
2615
|
+
if (clusterResults.length > 0) {
|
|
2616
|
+
const ciToIdx = /* @__PURE__ */ new Map();
|
|
2617
|
+
for (let ci = 0; ci < clusterItems.length; ci++) ciToIdx.set(clusterItems[ci], ci);
|
|
2618
|
+
const usedClusterIndices = /* @__PURE__ */ new Set();
|
|
2619
|
+
for (const cr of clusterResults) {
|
|
2620
|
+
for (const ci of cr.usedItems) {
|
|
2621
|
+
const idx = ciToIdx.get(ci);
|
|
2622
|
+
if (idx !== void 0) usedClusterIndices.add(idx);
|
|
2623
|
+
}
|
|
2624
|
+
blocks.push({ type: "table", table: cr.table, pageNumber: pageNum, bbox: cr.bbox });
|
|
2625
|
+
}
|
|
2626
|
+
remaining = remaining.filter((_, idx) => !usedClusterIndices.has(idx));
|
|
2627
|
+
}
|
|
2628
|
+
if (remaining.length > 0) {
|
|
2629
|
+
const allY = remaining.map((i) => i.y);
|
|
2630
|
+
const pageH = _chunkYBPNKFJWcjs.safeMax.call(void 0, allY) - _chunkYBPNKFJWcjs.safeMin.call(void 0, allY);
|
|
2631
|
+
const groups = xyCutOrder(remaining, Math.max(15, pageH * 0.03));
|
|
2632
|
+
const textBlocks = [];
|
|
2633
|
+
for (const group of groups) {
|
|
2634
|
+
if (group.length === 0) continue;
|
|
2635
|
+
const groupBlocks = extractPageBlocksFallback(group, pageNum);
|
|
2636
|
+
for (const b of groupBlocks) textBlocks.push(b);
|
|
2637
|
+
}
|
|
2638
|
+
const finalTextBlocks = detectListBlocks(textBlocks);
|
|
2639
|
+
for (const b of finalTextBlocks) blocks.push(b);
|
|
2640
|
+
}
|
|
2641
|
+
blocks.sort((a, b) => {
|
|
2642
|
+
const ay = a.bbox ? a.bbox.y + a.bbox.height : 0;
|
|
2643
|
+
const by = b.bbox ? b.bbox.y + b.bbox.height : 0;
|
|
2644
|
+
return by - ay;
|
|
2645
|
+
});
|
|
2646
|
+
return mergeAdjacentTableBlocks(blocks);
|
|
2647
|
+
}
|
|
2648
|
+
return mergeAdjacentTableBlocks(blocks);
|
|
2649
|
+
}
|
|
2650
|
+
var NEIGHBOR_TABLE_EPSILON = 0.2;
|
|
2651
|
+
function mergeCrossPageTables(blocks) {
|
|
2652
|
+
for (let i = blocks.length - 2; i >= 0; i--) {
|
|
2653
|
+
const prev = blocks[i];
|
|
2654
|
+
const curr = blocks[i + 1];
|
|
2655
|
+
if (prev.type !== "table" || curr.type !== "table" || !prev.table || !curr.table) continue;
|
|
2656
|
+
if (!prev.pageNumber || !curr.pageNumber || curr.pageNumber !== prev.pageNumber + 1) continue;
|
|
2657
|
+
if (prev.table.cols !== curr.table.cols) continue;
|
|
2658
|
+
if (!prev.bbox || !curr.bbox) continue;
|
|
2659
|
+
const width = Math.max(prev.bbox.width, curr.bbox.width, 1);
|
|
2660
|
+
const leftDiff = Math.abs(prev.bbox.x - curr.bbox.x);
|
|
2661
|
+
const rightDiff = Math.abs(prev.bbox.x + prev.bbox.width - (curr.bbox.x + curr.bbox.width));
|
|
2662
|
+
if (leftDiff > width * NEIGHBOR_TABLE_EPSILON || rightDiff > width * NEIGHBOR_TABLE_EPSILON) continue;
|
|
2663
|
+
let currCells = curr.table.cells;
|
|
2664
|
+
if (currCells.length > 1 && prev.table.cells.length > 0 && rowTextsEqual(prev.table.cells[0], currCells[0])) {
|
|
2665
|
+
currCells = currCells.slice(1);
|
|
2666
|
+
}
|
|
2667
|
+
if (currCells.length === 0) {
|
|
2668
|
+
blocks.splice(i + 1, 1);
|
|
2809
2669
|
continue;
|
|
2810
2670
|
}
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
const listType = label.family === "arabicDot" ? "ordered" : "unordered";
|
|
2821
|
-
const listBlock = { ...b, type: "list", listType };
|
|
2822
|
-
if (depth === 0) {
|
|
2823
|
-
blocks[i] = listBlock;
|
|
2824
|
-
lastTopLevelList = listBlock;
|
|
2825
|
-
} else if (lastTopLevelList) {
|
|
2826
|
-
if (!lastTopLevelList.children) lastTopLevelList.children = [];
|
|
2827
|
-
lastTopLevelList.children.push(listBlock);
|
|
2828
|
-
toRemove.add(i);
|
|
2829
|
-
} else {
|
|
2830
|
-
blocks[i] = listBlock;
|
|
2831
|
-
lastTopLevelList = listBlock;
|
|
2832
|
-
}
|
|
2671
|
+
const merged = {
|
|
2672
|
+
rows: prev.table.rows + currCells.length,
|
|
2673
|
+
cols: prev.table.cols,
|
|
2674
|
+
cells: [...prev.table.cells, ...currCells],
|
|
2675
|
+
hasHeader: prev.table.hasHeader,
|
|
2676
|
+
caption: prev.table.caption
|
|
2677
|
+
};
|
|
2678
|
+
blocks[i] = { ...prev, table: merged };
|
|
2679
|
+
blocks.splice(i + 1, 1);
|
|
2833
2680
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2681
|
+
}
|
|
2682
|
+
function rowTextsEqual(a, b) {
|
|
2683
|
+
if (a.length !== b.length) return false;
|
|
2684
|
+
const norm = (t) => t.replace(/\s+/g, "");
|
|
2685
|
+
for (let i = 0; i < a.length; i++) {
|
|
2686
|
+
if (norm(a[i].text) !== norm(b[i].text)) return false;
|
|
2837
2687
|
}
|
|
2688
|
+
return a.some((c) => c.text.trim() !== "");
|
|
2838
2689
|
}
|
|
2839
|
-
function
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
}
|
|
2690
|
+
function mergeAdjacentTableBlocks(blocks) {
|
|
2691
|
+
if (blocks.length <= 1) return blocks;
|
|
2692
|
+
const result = [blocks[0]];
|
|
2693
|
+
for (let i = 1; i < blocks.length; i++) {
|
|
2694
|
+
const prev = result[result.length - 1];
|
|
2695
|
+
const curr = blocks[i];
|
|
2696
|
+
if (prev.type === "table" && curr.type === "table" && prev.table && curr.table && prev.table.cols === curr.table.cols) {
|
|
2697
|
+
const merged = {
|
|
2698
|
+
rows: prev.table.rows + curr.table.rows,
|
|
2699
|
+
cols: prev.table.cols,
|
|
2700
|
+
cells: [...prev.table.cells, ...curr.table.cells],
|
|
2701
|
+
hasHeader: prev.table.hasHeader
|
|
2702
|
+
};
|
|
2703
|
+
result[result.length - 1] = { ...prev, table: merged };
|
|
2704
|
+
} else {
|
|
2705
|
+
result.push(curr);
|
|
2853
2706
|
}
|
|
2854
|
-
result.push(block);
|
|
2855
2707
|
}
|
|
2856
2708
|
return result;
|
|
2857
2709
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2710
|
+
function extractPageBlocksFallback(items, pageNum) {
|
|
2711
|
+
if (items.length === 0) return [];
|
|
2712
|
+
const blocks = [];
|
|
2713
|
+
const clusterItems = items.map((i) => ({
|
|
2714
|
+
text: i.text,
|
|
2715
|
+
x: i.x,
|
|
2716
|
+
y: i.y,
|
|
2717
|
+
w: i.w,
|
|
2718
|
+
h: i.h,
|
|
2719
|
+
fontSize: i.fontSize,
|
|
2720
|
+
fontName: i.fontName,
|
|
2721
|
+
hasSpaceBefore: i.hasSpaceBefore
|
|
2722
|
+
}));
|
|
2723
|
+
const clusterResults = detectClusterTables(clusterItems, pageNum);
|
|
2724
|
+
if (clusterResults.length > 0) {
|
|
2725
|
+
const ciToIdx = /* @__PURE__ */ new Map();
|
|
2726
|
+
for (let ci = 0; ci < clusterItems.length; ci++) ciToIdx.set(clusterItems[ci], ci);
|
|
2727
|
+
const usedIndices = /* @__PURE__ */ new Set();
|
|
2728
|
+
for (const cr of clusterResults) {
|
|
2729
|
+
for (const ci of cr.usedItems) {
|
|
2730
|
+
const idx = ciToIdx.get(ci);
|
|
2731
|
+
if (idx !== void 0) usedIndices.add(idx);
|
|
2875
2732
|
}
|
|
2876
|
-
|
|
2877
|
-
{ text: kv.key, colSpan: 2, rowSpan: 1 },
|
|
2878
|
-
{ text: "", colSpan: 1, rowSpan: 1 }
|
|
2879
|
-
];
|
|
2880
|
-
});
|
|
2881
|
-
const irTable = {
|
|
2882
|
-
rows: cells.length,
|
|
2883
|
-
cols: 2,
|
|
2884
|
-
cells,
|
|
2885
|
-
hasHeader: true
|
|
2886
|
-
};
|
|
2887
|
-
const firstBlock = kvLines[0].block;
|
|
2888
|
-
result.push({
|
|
2889
|
-
type: "table",
|
|
2890
|
-
table: irTable,
|
|
2891
|
-
pageNumber: firstBlock.pageNumber,
|
|
2892
|
-
bbox: firstBlock.bbox
|
|
2893
|
-
});
|
|
2894
|
-
kvLines = [];
|
|
2895
|
-
};
|
|
2896
|
-
for (const block of blocks) {
|
|
2897
|
-
if (block.type !== "paragraph" || !block.text) {
|
|
2898
|
-
flushKvTable();
|
|
2899
|
-
result.push(block);
|
|
2900
|
-
continue;
|
|
2733
|
+
blocks.push({ type: "table", table: cr.table, pageNumber: pageNum, bbox: cr.bbox });
|
|
2901
2734
|
}
|
|
2902
|
-
const
|
|
2903
|
-
if (
|
|
2904
|
-
const
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
});
|
|
2911
|
-
} else {
|
|
2912
|
-
const spaceIdx = text.search(/\s/);
|
|
2913
|
-
if (spaceIdx > 0) {
|
|
2914
|
-
kvLines.push({
|
|
2915
|
-
key: text.slice(0, spaceIdx).trim(),
|
|
2916
|
-
value: text.slice(spaceIdx + 1).trim(),
|
|
2917
|
-
block
|
|
2918
|
-
});
|
|
2919
|
-
} else {
|
|
2920
|
-
kvLines.push({ key: text, value: "", block });
|
|
2921
|
-
}
|
|
2735
|
+
const remaining = items.filter((_, idx) => !usedIndices.has(idx));
|
|
2736
|
+
if (remaining.length > 0) {
|
|
2737
|
+
const yLines = mergeSuperscriptLines(groupByY(remaining));
|
|
2738
|
+
for (const line of yLines) {
|
|
2739
|
+
const text = mergeLineSimple(line);
|
|
2740
|
+
if (!text.trim()) continue;
|
|
2741
|
+
const bbox = computeBBox(line, pageNum);
|
|
2742
|
+
blocks.push({ type: "paragraph", text, pageNumber: pageNum, bbox, style: dominantStyle(line) });
|
|
2922
2743
|
}
|
|
2923
|
-
continue;
|
|
2924
2744
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2745
|
+
blocks.sort((a, b) => {
|
|
2746
|
+
const ay = a.bbox ? a.bbox.y + a.bbox.height : 0;
|
|
2747
|
+
const by = b.bbox ? b.bbox.y + b.bbox.height : 0;
|
|
2748
|
+
return by - ay;
|
|
2749
|
+
});
|
|
2750
|
+
} else {
|
|
2751
|
+
const allYLines = mergeSuperscriptLines(groupByY(items));
|
|
2752
|
+
const columns = detectColumns(allYLines);
|
|
2753
|
+
if (columns && columns.length >= 3) {
|
|
2754
|
+
const tableText = extractWithColumns(allYLines, columns);
|
|
2755
|
+
const bbox = computeBBox(items, pageNum);
|
|
2756
|
+
blocks.push({ type: "paragraph", text: tableText, pageNumber: pageNum, bbox, style: dominantStyle(items) });
|
|
2757
|
+
} else {
|
|
2758
|
+
const allY = items.map((i) => i.y);
|
|
2759
|
+
const pageHeight = _chunkYBPNKFJWcjs.safeMax.call(void 0, allY) - _chunkYBPNKFJWcjs.safeMin.call(void 0, allY);
|
|
2760
|
+
const gapThreshold = Math.max(15, pageHeight * 0.03);
|
|
2761
|
+
const orderedGroups = xyCutOrder(items, gapThreshold);
|
|
2762
|
+
for (const group of orderedGroups) {
|
|
2763
|
+
if (group.length === 0) continue;
|
|
2764
|
+
const yLines = mergeSuperscriptLines(groupByY(group));
|
|
2765
|
+
const groupColumns = detectColumns(yLines);
|
|
2766
|
+
if (groupColumns && groupColumns.length >= 3) {
|
|
2767
|
+
const tableText = extractWithColumns(yLines, groupColumns);
|
|
2768
|
+
const bbox = computeBBox(group, pageNum);
|
|
2769
|
+
blocks.push({ type: "paragraph", text: tableText, pageNumber: pageNum, bbox, style: dominantStyle(group) });
|
|
2770
|
+
} else {
|
|
2771
|
+
for (const line of yLines) {
|
|
2772
|
+
const text = mergeLineSimple(line);
|
|
2773
|
+
if (!text.trim()) continue;
|
|
2774
|
+
const bbox = computeBBox(line, pageNum);
|
|
2775
|
+
blocks.push({ type: "paragraph", text, pageNumber: pageNum, bbox, style: dominantStyle(line) });
|
|
2776
|
+
}
|
|
2936
2777
|
}
|
|
2937
2778
|
}
|
|
2938
2779
|
}
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
if (!b.bbox || !b.pageNumber || !_optionalChain([b, 'access', _27 => _27.text, 'optionalAccess', _28 => _28.trim, 'call', _29 => _29()])) continue;
|
|
2953
|
-
const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
|
|
2954
|
-
if (!ph) continue;
|
|
2955
|
-
const blockTop = ph - (b.bbox.y + b.bbox.height);
|
|
2956
|
-
const blockBottom = ph - b.bbox.y;
|
|
2957
|
-
const entry = { blockIdx: bi, page: b.pageNumber, text: b.text.trim() };
|
|
2958
|
-
if (blockBottom <= ph * ZONE_RATIO) bottomEntries.push(entry);
|
|
2959
|
-
else if (blockTop >= ph * (1 - ZONE_RATIO)) topEntries.push(entry);
|
|
2960
|
-
}
|
|
2961
|
-
const removeSet = /* @__PURE__ */ new Set();
|
|
2962
|
-
for (const entries of [topEntries, bottomEntries]) {
|
|
2963
|
-
if (entries.length === 0) continue;
|
|
2964
|
-
const patternCount = /* @__PURE__ */ new Map();
|
|
2965
|
-
const patternPages = /* @__PURE__ */ new Map();
|
|
2966
|
-
for (const e of entries) {
|
|
2967
|
-
const norm = e.text.replace(/\d+/g, "#");
|
|
2968
|
-
patternCount.set(norm, (patternCount.get(norm) || 0) + 1);
|
|
2969
|
-
const pages = patternPages.get(norm) || /* @__PURE__ */ new Set();
|
|
2970
|
-
pages.add(e.page);
|
|
2971
|
-
patternPages.set(norm, pages);
|
|
2972
|
-
}
|
|
2973
|
-
const repeatedPatterns = /* @__PURE__ */ new Set();
|
|
2974
|
-
for (const [p, count] of patternCount) {
|
|
2975
|
-
if (count >= MIN_REPEAT && (_nullishCoalesce(_optionalChain([patternPages, 'access', _30 => _30.get, 'call', _31 => _31(p), 'optionalAccess', _32 => _32.size]), () => ( 0))) >= MIN_REPEAT) {
|
|
2976
|
-
repeatedPatterns.add(p);
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
for (const e of entries) {
|
|
2980
|
-
const norm = e.text.replace(/\d+/g, "#");
|
|
2981
|
-
if (repeatedPatterns.has(norm)) {
|
|
2982
|
-
removeSet.add(e.blockIdx);
|
|
2780
|
+
}
|
|
2781
|
+
return detectSpecialKoreanTables(blocks);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// src/pdf/text-clean.ts
|
|
2785
|
+
function sanitizeBlockControlChars(blocks) {
|
|
2786
|
+
for (const b of blocks) {
|
|
2787
|
+
if (b.text) b.text = stripControlChars(b.text);
|
|
2788
|
+
if (b.table) {
|
|
2789
|
+
for (const row of b.table.cells) {
|
|
2790
|
+
for (const cell of row) {
|
|
2791
|
+
if (cell.text) cell.text = stripControlChars(cell.text);
|
|
2792
|
+
}
|
|
2983
2793
|
}
|
|
2984
2794
|
}
|
|
2795
|
+
if (b.children) sanitizeBlockControlChars(b.children);
|
|
2985
2796
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2797
|
+
}
|
|
2798
|
+
function cleanPdfText(text) {
|
|
2799
|
+
return mergeKoreanLines(
|
|
2800
|
+
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, "")
|
|
2801
|
+
).replace(/^(?!\| ---).*$/gm, (line) => {
|
|
2802
|
+
if (/^\s*\${1,2}.+\${1,2}\s*$/.test(line)) return line;
|
|
2803
|
+
return collapseEvenSpacing(line);
|
|
2804
|
+
}).replace(/([□■◆○●▶ㅇ])\s+([가-힣])\s+([가-힣])/g, "$1 $2$3").replace(/\\~\\~/g, "~~").replace(/~~~~/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
2805
|
+
}
|
|
2806
|
+
function startsWithMarker(line) {
|
|
2807
|
+
const t = line.trimStart();
|
|
2808
|
+
return /^[가-힣ㄱ-ㅎ][.)]/.test(t) || /^\d+[.)]/.test(t) || /^\([가-힣ㄱ-ㅎ\d]+\)/.test(t) || /^[○●※▶▷◆◇■□★☆\-·]\s/.test(t) || /^제\d+[조항호장절]/.test(t);
|
|
2809
|
+
}
|
|
2810
|
+
function isStandaloneHeader(line) {
|
|
2811
|
+
return /^제\d+[조항호장절](\([^)]*\))?(\s+\S+){0,7}$/.test(line.trim());
|
|
2990
2812
|
}
|
|
2991
2813
|
function mergeKoreanLines(text) {
|
|
2992
2814
|
if (!text) return "";
|
|
@@ -3017,6 +2839,8 @@ function mergeKoreanLines(text) {
|
|
|
3017
2839
|
}
|
|
3018
2840
|
return result.join("\n");
|
|
3019
2841
|
}
|
|
2842
|
+
|
|
2843
|
+
// src/pdf/formula-ocr.ts
|
|
3020
2844
|
async function applyFormulaOcr(buffer, blocks, pageFilter, effectivePageCount, warnings, _onProgress) {
|
|
3021
2845
|
const formulaMod = await Promise.resolve().then(() => _interopRequireWildcard(require("./formula-5NKVS2LR.cjs")));
|
|
3022
2846
|
const { FormulaPipeline, ensureFormulaModels } = formulaMod;
|
|
@@ -3136,6 +2960,251 @@ function formatMb(bytes) {
|
|
|
3136
2960
|
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
3137
2961
|
}
|
|
3138
2962
|
|
|
2963
|
+
// src/pdf/polyfill.ts
|
|
2964
|
+
var _pdfworkermjs = require('pdfjs-dist/legacy/build/pdf.worker.mjs'); var pdfjsWorker = _interopRequireWildcard(_pdfworkermjs);
|
|
2965
|
+
var g = globalThis;
|
|
2966
|
+
if (typeof g.DOMMatrix === "undefined") {
|
|
2967
|
+
g.DOMMatrix = (_class = class DOMMatrix {
|
|
2968
|
+
__init() {this.m = [1, 0, 0, 1, 0, 0]}
|
|
2969
|
+
constructor(init) {;_class.prototype.__init.call(this);
|
|
2970
|
+
if (init) this.m = init;
|
|
2971
|
+
}
|
|
2972
|
+
}, _class);
|
|
2973
|
+
}
|
|
2974
|
+
if (typeof g.Path2D === "undefined") {
|
|
2975
|
+
g.Path2D = class Path2D {
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
g.pdfjsWorker = pdfjsWorker;
|
|
2979
|
+
|
|
2980
|
+
// src/pdf/parser.ts
|
|
2981
|
+
|
|
2982
|
+
var _module = require('module');
|
|
2983
|
+
var _path = require('path');
|
|
2984
|
+
_pdfmjs.GlobalWorkerOptions.workerSrc = "";
|
|
2985
|
+
var MAX_PAGES = 5e3;
|
|
2986
|
+
var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
|
|
2987
|
+
var PDF_LOAD_TIMEOUT_MS = 3e4;
|
|
2988
|
+
var pdfjsAssets = {};
|
|
2989
|
+
try {
|
|
2990
|
+
const _require = _module.createRequire.call(void 0, _chunkGS7T56RPcjs.importMetaUrl);
|
|
2991
|
+
const pkgDir = _path.dirname.call(void 0, _require.resolve("pdfjs-dist/package.json"));
|
|
2992
|
+
pdfjsAssets.cMapUrl = _path.join.call(void 0, pkgDir, "cmaps") + "/";
|
|
2993
|
+
pdfjsAssets.cMapPacked = true;
|
|
2994
|
+
pdfjsAssets.standardFontDataUrl = _path.join.call(void 0, pkgDir, "standard_fonts") + "/";
|
|
2995
|
+
} catch (e2) {
|
|
2996
|
+
}
|
|
2997
|
+
async function loadPdfWithTimeout(buffer) {
|
|
2998
|
+
const loadingTask = _pdfmjs.getDocument.call(void 0, {
|
|
2999
|
+
data: new Uint8Array(buffer),
|
|
3000
|
+
useSystemFonts: true,
|
|
3001
|
+
disableFontFace: true,
|
|
3002
|
+
isEvalSupported: false,
|
|
3003
|
+
...pdfjsAssets
|
|
3004
|
+
});
|
|
3005
|
+
let timer;
|
|
3006
|
+
try {
|
|
3007
|
+
return await Promise.race([
|
|
3008
|
+
loadingTask.promise,
|
|
3009
|
+
new Promise((_, reject) => {
|
|
3010
|
+
timer = setTimeout(() => {
|
|
3011
|
+
loadingTask.destroy();
|
|
3012
|
+
reject(new (0, _chunkYBPNKFJWcjs.KordocError)("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
|
|
3013
|
+
}, PDF_LOAD_TIMEOUT_MS);
|
|
3014
|
+
})
|
|
3015
|
+
]);
|
|
3016
|
+
} finally {
|
|
3017
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function parsePdfDocument(buffer, options) {
|
|
3021
|
+
const formulaBuffer = _optionalChain([options, 'optionalAccess', _25 => _25.formulaOcr]) ? buffer.slice(0) : null;
|
|
3022
|
+
const doc = await loadPdfWithTimeout(buffer);
|
|
3023
|
+
try {
|
|
3024
|
+
const pageCount = doc.numPages;
|
|
3025
|
+
if (pageCount === 0) throw new (0, _chunkYBPNKFJWcjs.KordocError)("PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3026
|
+
const metadata = { pageCount };
|
|
3027
|
+
await extractPdfMetadata(doc, metadata);
|
|
3028
|
+
const blocks = [];
|
|
3029
|
+
const warnings = [];
|
|
3030
|
+
const pageQuality = [];
|
|
3031
|
+
let totalChars = 0;
|
|
3032
|
+
let totalTextBytes = 0;
|
|
3033
|
+
const effectivePageCount = Math.min(pageCount, MAX_PAGES);
|
|
3034
|
+
const pageFilter = _optionalChain([options, 'optionalAccess', _26 => _26.pages]) ? _chunkDCZVOIEOcjs.parsePageRange.call(void 0, options.pages, effectivePageCount) : null;
|
|
3035
|
+
const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
|
|
3036
|
+
const fontSizeFreq = /* @__PURE__ */ new Map();
|
|
3037
|
+
const pageHeights = /* @__PURE__ */ new Map();
|
|
3038
|
+
const pagesWithLargeImage = /* @__PURE__ */ new Set();
|
|
3039
|
+
const skippedImagePages = /* @__PURE__ */ new Map();
|
|
3040
|
+
let parsedPages = 0;
|
|
3041
|
+
for (let i = 1; i <= effectivePageCount; i++) {
|
|
3042
|
+
if (pageFilter && !pageFilter.has(i)) continue;
|
|
3043
|
+
try {
|
|
3044
|
+
const page = await doc.getPage(i);
|
|
3045
|
+
const tc = await page.getTextContent();
|
|
3046
|
+
const viewport = page.getViewport({ scale: 1 });
|
|
3047
|
+
pageHeights.set(i, viewport.height);
|
|
3048
|
+
const rawItems = tc.items;
|
|
3049
|
+
const items = normalizeItems(rawItems);
|
|
3050
|
+
const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
|
|
3051
|
+
if (hiddenCount > 0) {
|
|
3052
|
+
warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
|
|
3053
|
+
}
|
|
3054
|
+
for (const item of visible) {
|
|
3055
|
+
if (item.fontSize > 0) fontSizeFreq.set(item.fontSize, (fontSizeFreq.get(item.fontSize) || 0) + 1);
|
|
3056
|
+
}
|
|
3057
|
+
const opList = await page.getOperatorList();
|
|
3058
|
+
const pageArea = viewport.width * viewport.height;
|
|
3059
|
+
if (pageArea > 0) {
|
|
3060
|
+
const imageRegions = extractImageRegions(opList.fnArray, opList.argsArray);
|
|
3061
|
+
let uncovered = 0;
|
|
3062
|
+
for (const r of imageRegions) {
|
|
3063
|
+
const area = (r.x2 - r.x1) * (r.y2 - r.y1);
|
|
3064
|
+
if (area < pageArea * 0.05) continue;
|
|
3065
|
+
pagesWithLargeImage.add(i);
|
|
3066
|
+
const hasText = visible.some((it) => {
|
|
3067
|
+
const cx = it.x + it.w / 2;
|
|
3068
|
+
const cy = it.y + (it.h || it.fontSize) / 2;
|
|
3069
|
+
return cx >= r.x1 && cx <= r.x2 && cy >= r.y1 && cy <= r.y2;
|
|
3070
|
+
});
|
|
3071
|
+
if (!hasText) uncovered++;
|
|
3072
|
+
}
|
|
3073
|
+
if (uncovered > 0) skippedImagePages.set(i, uncovered);
|
|
3074
|
+
}
|
|
3075
|
+
const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
|
|
3076
|
+
for (const b of pageBlocks) blocks.push(b);
|
|
3077
|
+
let pageText = "";
|
|
3078
|
+
for (const b of pageBlocks) {
|
|
3079
|
+
const t = b.text || "";
|
|
3080
|
+
totalChars += t.replace(/\s/g, "").length;
|
|
3081
|
+
totalTextBytes += t.length * 2;
|
|
3082
|
+
pageText += pageText ? "\n" + t : t;
|
|
3083
|
+
}
|
|
3084
|
+
pageQuality.push(computePageQuality(i, pageText));
|
|
3085
|
+
if (totalTextBytes > MAX_TOTAL_TEXT) throw new (0, _chunkYBPNKFJWcjs.KordocError)("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
|
|
3086
|
+
parsedPages++;
|
|
3087
|
+
_optionalChain([options, 'optionalAccess', _27 => _27.onProgress, 'optionalCall', _28 => _28(parsedPages, totalTarget)]);
|
|
3088
|
+
} catch (pageErr) {
|
|
3089
|
+
if (pageErr instanceof _chunkYBPNKFJWcjs.KordocError) throw pageErr;
|
|
3090
|
+
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" });
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
|
|
3094
|
+
let isImageBased = false;
|
|
3095
|
+
if (totalChars / Math.max(parsedPageCount, 1) < 10) {
|
|
3096
|
+
if (_optionalChain([options, 'optionalAccess', _29 => _29.ocr])) {
|
|
3097
|
+
try {
|
|
3098
|
+
const { ocrPages } = await Promise.resolve().then(() => _interopRequireWildcard(require("./provider-G4C2V2PD.cjs")));
|
|
3099
|
+
const ocrBlocks = await ocrPages(doc, options.ocr, pageFilter, effectivePageCount);
|
|
3100
|
+
if (ocrBlocks.length > 0) {
|
|
3101
|
+
const ocrMarkdown = ocrBlocks.map((b) => b.text || "").filter(Boolean).join("\n\n");
|
|
3102
|
+
return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true, pageQuality, qualitySummary: summarizeDocumentQuality(pageQuality) };
|
|
3103
|
+
}
|
|
3104
|
+
} catch (e3) {
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
isImageBased = true;
|
|
3108
|
+
warnings.push({
|
|
3109
|
+
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`,
|
|
3110
|
+
code: "NEEDS_OCR"
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
if (!isImageBased) {
|
|
3114
|
+
const OCR_REASON_MESSAGES = {
|
|
3115
|
+
low_text: "\uD14D\uC2A4\uD2B8\uAC00 \uAC70\uC758 \uC5C6\uB294 \uD398\uC774\uC9C0 (\uC2A4\uCE94/\uC774\uBBF8\uC9C0 \uCD94\uC815)",
|
|
3116
|
+
high_pua: "\uAE00\uAF34 \uB9E4\uD551 \uC2E4\uD328 (PUA \uBE44\uC728 \uB192\uC74C) \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
3117
|
+
high_control: "\uC81C\uC5B4\uBB38\uC790 \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00",
|
|
3118
|
+
high_replacement: "\uB300\uCCB4\uBB38\uC790(U+FFFD) \uBE44\uC728 \uB192\uC74C \u2014 \uCD94\uCD9C \uD14D\uC2A4\uD2B8 \uC2E0\uB8B0 \uBD88\uAC00"
|
|
3119
|
+
};
|
|
3120
|
+
for (const pq of pageQuality) {
|
|
3121
|
+
if (!pq.needsOcr || !pq.ocrReason) continue;
|
|
3122
|
+
if (pq.ocrReason === "low_text" && !pagesWithLargeImage.has(pq.page)) continue;
|
|
3123
|
+
warnings.push({ page: pq.page, message: `${OCR_REASON_MESSAGES[pq.ocrReason]} \u2014 OCR \uAC80\uD1A0 \uD544\uC694`, code: "NEEDS_OCR" });
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
if (!isImageBased) {
|
|
3127
|
+
for (const [page, count] of [...skippedImagePages.entries()].sort((a, b) => a[0] - b[0])) {
|
|
3128
|
+
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" });
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
if (_optionalChain([options, 'optionalAccess', _30 => _30.removeHeaderFooter]) !== false && parsedPageCount >= 3) {
|
|
3132
|
+
const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
|
|
3133
|
+
for (let ri = removed.length - 1; ri >= 0; ri--) {
|
|
3134
|
+
blocks.splice(removed[ri], 1);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
mergeCrossPageTables(blocks);
|
|
3138
|
+
if (_optionalChain([options, 'optionalAccess', _31 => _31.formulaOcr]) && formulaBuffer) {
|
|
3139
|
+
try {
|
|
3140
|
+
await applyFormulaOcr(formulaBuffer, blocks, pageFilter, effectivePageCount, warnings, options.onProgress);
|
|
3141
|
+
} catch (e) {
|
|
3142
|
+
warnings.push({
|
|
3143
|
+
message: `\uC218\uC2DD OCR \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`,
|
|
3144
|
+
code: "PARTIAL_PARSE"
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
const medianFontSize = computeMedianFontSizeFromFreq(fontSizeFreq);
|
|
3149
|
+
if (medianFontSize > 0) {
|
|
3150
|
+
detectHeadings(blocks, medianFontSize);
|
|
3151
|
+
}
|
|
3152
|
+
detectMarkerHeadings(blocks);
|
|
3153
|
+
detectTableCaptions(blocks);
|
|
3154
|
+
detectKoreanListBlocks(blocks);
|
|
3155
|
+
const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
|
|
3156
|
+
sanitizeBlockControlChars(blocks);
|
|
3157
|
+
let markdown = cleanPdfText(_chunkYBPNKFJWcjs.blocksToMarkdown.call(void 0, blocks));
|
|
3158
|
+
return {
|
|
3159
|
+
markdown,
|
|
3160
|
+
blocks,
|
|
3161
|
+
metadata,
|
|
3162
|
+
outline: outline.length > 0 ? outline : void 0,
|
|
3163
|
+
warnings: warnings.length > 0 ? warnings : void 0,
|
|
3164
|
+
isImageBased: isImageBased || void 0,
|
|
3165
|
+
pageQuality,
|
|
3166
|
+
qualitySummary: summarizeDocumentQuality(pageQuality)
|
|
3167
|
+
};
|
|
3168
|
+
} finally {
|
|
3169
|
+
await doc.destroy().catch(() => {
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
async function extractPdfMetadata(doc, metadata) {
|
|
3174
|
+
try {
|
|
3175
|
+
const result = await doc.getMetadata();
|
|
3176
|
+
if (!_optionalChain([result, 'optionalAccess', _32 => _32.info])) return;
|
|
3177
|
+
const info = result.info;
|
|
3178
|
+
if (typeof info.Title === "string" && info.Title.trim()) metadata.title = info.Title.trim();
|
|
3179
|
+
if (typeof info.Author === "string" && info.Author.trim()) metadata.author = info.Author.trim();
|
|
3180
|
+
if (typeof info.Creator === "string" && info.Creator.trim()) metadata.creator = info.Creator.trim();
|
|
3181
|
+
if (typeof info.Subject === "string" && info.Subject.trim()) metadata.description = info.Subject.trim();
|
|
3182
|
+
if (typeof info.Keywords === "string" && info.Keywords.trim()) {
|
|
3183
|
+
metadata.keywords = info.Keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
|
|
3184
|
+
}
|
|
3185
|
+
if (typeof info.CreationDate === "string") metadata.createdAt = parsePdfDate(info.CreationDate);
|
|
3186
|
+
if (typeof info.ModDate === "string") metadata.modifiedAt = parsePdfDate(info.ModDate);
|
|
3187
|
+
} catch (e4) {
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
function parsePdfDate(dateStr) {
|
|
3191
|
+
const m = dateStr.match(/D:(\d{4})(\d{2})?(\d{2})?(\d{2})?(\d{2})?(\d{2})?/);
|
|
3192
|
+
if (!m) return void 0;
|
|
3193
|
+
const [, year, month = "01", day = "01", hour = "00", min = "00", sec = "00"] = m;
|
|
3194
|
+
return `${year}-${month}-${day}T${hour}:${min}:${sec}`;
|
|
3195
|
+
}
|
|
3196
|
+
async function extractPdfMetadataOnly(buffer) {
|
|
3197
|
+
const doc = await loadPdfWithTimeout(buffer);
|
|
3198
|
+
try {
|
|
3199
|
+
const metadata = { pageCount: doc.numPages };
|
|
3200
|
+
await extractPdfMetadata(doc, metadata);
|
|
3201
|
+
return metadata;
|
|
3202
|
+
} finally {
|
|
3203
|
+
await doc.destroy().catch(() => {
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3139
3208
|
|
|
3140
3209
|
|
|
3141
3210
|
|
|
@@ -3144,4 +3213,4 @@ function formatMb(bytes) {
|
|
|
3144
3213
|
|
|
3145
3214
|
|
|
3146
3215
|
exports.cleanPdfText = cleanPdfText; exports.detectKoreanListBlocks = detectKoreanListBlocks; exports.detectTableCaptions = detectTableCaptions; exports.extractPdfMetadataOnly = extractPdfMetadataOnly; exports.mergeCrossPageTables = mergeCrossPageTables; exports.parsePdfDocument = parsePdfDocument; exports.removeHeaderFooterBlocks = removeHeaderFooterBlocks;
|
|
3147
|
-
//# sourceMappingURL=parser-
|
|
3216
|
+
//# sourceMappingURL=parser-NS4ZPD7B.cjs.map
|