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