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