kordoc 4.14.2 → 4.14.4
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 +51 -6
- package/dist/{-AQKXLFIM.js → -Z4TJ3QGK.js} +13 -11
- package/dist/{chunk-QOMNEWCQ.cjs → chunk-5LAZQCUJ.cjs} +305 -31
- package/dist/chunk-5LAZQCUJ.cjs.map +1 -0
- package/dist/{chunk-7IAQ4HOK.js → chunk-6BWU6SQX.js} +1 -1
- package/dist/chunk-6BWU6SQX.js.map +1 -0
- package/dist/{chunk-FOJZNMMK.js → chunk-6CUIXJOC.js} +1 -1
- package/dist/chunk-6CUIXJOC.js.map +1 -0
- package/dist/{chunk-ZWD734EY.js → chunk-7SE4EZ45.js} +2 -2
- package/dist/{chunk-TSFS5VKD.js → chunk-D4U2TXDC.js} +332 -141
- package/dist/chunk-D4U2TXDC.js.map +1 -0
- package/dist/chunk-DPWWXUOG.js +2222 -0
- package/dist/chunk-DPWWXUOG.js.map +1 -0
- package/dist/{chunk-GDW4YP7K.js → chunk-GLSBEEZJ.js} +2 -2
- package/dist/{chunk-JQEFP45L.js → chunk-GUJ4Q73I.js} +2 -2
- package/dist/{chunk-ZWOFQRIB.js → chunk-HIJUKZHM.js} +3 -3
- package/dist/chunk-IMAAFSAX.cjs +1352 -0
- package/dist/chunk-IMAAFSAX.cjs.map +1 -0
- package/dist/{chunk-TNCKOL2O.js → chunk-LCW7SEPG.js} +2 -2
- package/dist/chunk-LFSQAMSM.js +1352 -0
- package/dist/chunk-LFSQAMSM.js.map +1 -0
- package/dist/chunk-MG6DJEX4.js +1299 -0
- package/dist/chunk-MG6DJEX4.js.map +1 -0
- package/dist/{chunk-QC5T6D4J.js → chunk-NLU6LBLY.js} +299 -25
- package/dist/chunk-NLU6LBLY.js.map +1 -0
- package/dist/{chunk-EQZI77PP.cjs → chunk-QEJUY6SK.cjs} +3 -3
- package/dist/{chunk-EQZI77PP.cjs.map → chunk-QEJUY6SK.cjs.map} +1 -1
- package/dist/{chunk-VQ5W4MYO.js → chunk-QIF2VXBQ.js} +298 -24
- package/dist/chunk-QIF2VXBQ.js.map +1 -0
- package/dist/{chunk-UET462HB.js → chunk-TITDJWVZ.js} +2 -2
- package/dist/{chunk-247ANM72.cjs → chunk-USHQCF5T.cjs} +870 -147
- package/dist/chunk-USHQCF5T.cjs.map +1 -0
- package/dist/{chunk-C7FQBSON.js → chunk-V3DX4DUF.js} +2 -2
- package/dist/{chunk-DOGPOY4B.js → chunk-VDN7LMPL.js} +2 -2
- package/dist/{chunk-EDNJLUSE.js → chunk-VJIL6PIL.js} +2 -2
- package/dist/{chunk-L7DMSMAG.cjs → chunk-VWUYVEFN.cjs} +1 -1
- package/dist/chunk-VWUYVEFN.cjs.map +1 -0
- package/dist/{chunk-HR2XNERB.cjs → chunk-WJHAU73B.cjs} +2 -2
- package/dist/{chunk-HR2XNERB.cjs.map → chunk-WJHAU73B.cjs.map} +1 -1
- package/dist/{chunk-NS55SM7A.js → chunk-WJJGW5PL.js} +2 -2
- package/dist/{chunk-EISO3POT.js → chunk-XVXHXQIO.js} +788 -194
- package/dist/chunk-XVXHXQIO.js.map +1 -0
- package/dist/{chunk-ILU76UF3.js → chunk-YAKC46HZ.js} +2 -2
- package/dist/chunk-YAKC46HZ.js.map +1 -0
- package/dist/{chunk-3DUADAY2.js → chunk-YAVHVFRY.js} +851 -128
- package/dist/chunk-YAVHVFRY.js.map +1 -0
- package/dist/{chunk-A7QC2ZVP.js → chunk-YT3GC62E.js} +851 -128
- package/dist/chunk-YT3GC62E.js.map +1 -0
- package/dist/chunks-CCMRL4MJ.js +10 -0
- package/dist/cli.js +228 -97
- package/dist/cli.js.map +1 -1
- package/dist/{formula-PJJ6S3Z2.cjs → formula-5LOVHAKE.cjs} +9 -9
- package/dist/{formula-PJJ6S3Z2.cjs.map → formula-5LOVHAKE.cjs.map} +1 -1
- package/dist/{formula-JGFTXWYT.js → formula-7RANCW5L.js} +3 -3
- package/dist/{formula-5S7WWXGH.js → formula-7UYJOVTB.js} +4 -4
- package/dist/{image-ocr-WCAYXLOA.js → image-ocr-I6JOCATS.js} +34 -16
- package/dist/image-ocr-I6JOCATS.js.map +1 -0
- package/dist/{image-ocr-ZTYAGV3W.cjs → image-ocr-NYBJEOH2.cjs} +36 -18
- package/dist/image-ocr-NYBJEOH2.cjs.map +1 -0
- package/dist/{image-ocr-FLIL7MGN.js → image-ocr-ZV6PDBUQ.js} +32 -14
- package/dist/image-ocr-ZV6PDBUQ.js.map +1 -0
- package/dist/index.cjs +4048 -1216
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -28
- package/dist/index.d.ts +66 -28
- package/dist/index.js +3487 -655
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +74 -53
- package/dist/mcp.js.map +1 -1
- package/dist/{model-bundle-FSPZZ2EM.js → model-bundle-2C5IYDLE.js} +5 -5
- package/dist/{models-CJPAQK5R.js → models-SEQZHZFT.js} +5 -5
- package/dist/{parser-SCUTZAIJ.js → parser-3FNXEP6T.js} +708 -250
- package/dist/parser-3FNXEP6T.js.map +1 -0
- package/dist/{parser-A4TMPJAM.js → parser-FJBJXRJK.js} +708 -250
- package/dist/parser-FJBJXRJK.js.map +1 -0
- package/dist/{parser-VCXE4I5A.cjs → parser-HVEKF73B.cjs} +733 -275
- package/dist/parser-HVEKF73B.cjs.map +1 -0
- package/dist/pdf-ocr-JRDBJ6TA.cjs +14 -0
- package/dist/{pdf-ocr-MBZZ57LD.cjs.map → pdf-ocr-JRDBJ6TA.cjs.map} +1 -1
- package/dist/pdf-ocr-UUBWBEHL.js +16 -0
- package/dist/pdf-ocr-VJSF2SQC.js +13 -0
- package/dist/{profile-io-LBM263K7.js → profile-io-RUFZOZO6.js} +3 -3
- package/dist/{rasterize-46RG4ZF6.js → rasterize-IOYXR5SI.js} +3 -3
- package/dist/redact-NMIKVHMP.js +18 -0
- package/dist/redact-doc-QBHNJ2BU.js +1284 -0
- package/dist/redact-doc-QBHNJ2BU.js.map +1 -0
- package/dist/{render-NGP5T24S.js → render-YZ2TEC3N.js} +7 -7
- package/dist/seal-ECV44TUH.js +10 -0
- package/dist/{setup-AU4OCHNH.js → setup-MPIEP6XT.js} +3 -3
- package/dist/visual-ICMOWUCR.js +19 -0
- package/dist/{watch-MKPUCPXM.js → watch-RYO7MN5B.js} +12 -12
- package/package.json +2 -2
- package/dist/chunk-247ANM72.cjs.map +0 -1
- package/dist/chunk-3DUADAY2.js.map +0 -1
- package/dist/chunk-422427XJ.cjs +0 -571
- package/dist/chunk-422427XJ.cjs.map +0 -1
- package/dist/chunk-7IAQ4HOK.js.map +0 -1
- package/dist/chunk-A7QC2ZVP.js.map +0 -1
- package/dist/chunk-DZIXKL7E.js +0 -145
- package/dist/chunk-DZIXKL7E.js.map +0 -1
- package/dist/chunk-EISO3POT.js.map +0 -1
- package/dist/chunk-EP7KOI2V.js +0 -571
- package/dist/chunk-EP7KOI2V.js.map +0 -1
- package/dist/chunk-FOJZNMMK.js.map +0 -1
- package/dist/chunk-ILU76UF3.js.map +0 -1
- package/dist/chunk-KVWV4ULP.js +0 -516
- package/dist/chunk-KVWV4ULP.js.map +0 -1
- package/dist/chunk-L7DMSMAG.cjs.map +0 -1
- package/dist/chunk-QC5T6D4J.js.map +0 -1
- package/dist/chunk-QOMNEWCQ.cjs.map +0 -1
- package/dist/chunk-TSFS5VKD.js.map +0 -1
- package/dist/chunk-VQ5W4MYO.js.map +0 -1
- package/dist/chunks-SX77P5CV.js +0 -10
- package/dist/image-ocr-FLIL7MGN.js.map +0 -1
- package/dist/image-ocr-WCAYXLOA.js.map +0 -1
- package/dist/image-ocr-ZTYAGV3W.cjs.map +0 -1
- package/dist/parser-A4TMPJAM.js.map +0 -1
- package/dist/parser-SCUTZAIJ.js.map +0 -1
- package/dist/parser-VCXE4I5A.cjs.map +0 -1
- package/dist/pdf-ocr-76SAUXMZ.js +0 -13
- package/dist/pdf-ocr-CPU55CVF.js +0 -16
- package/dist/pdf-ocr-MBZZ57LD.cjs +0 -14
- package/dist/redact-7ILEAUTS.js +0 -12
- package/dist/seal-RGUPOLXD.js +0 -10
- package/dist/visual-NZLYIYEM.js +0 -19
- /package/dist/{-AQKXLFIM.js.map → -Z4TJ3QGK.js.map} +0 -0
- /package/dist/{chunk-ZWD734EY.js.map → chunk-7SE4EZ45.js.map} +0 -0
- /package/dist/{chunk-GDW4YP7K.js.map → chunk-GLSBEEZJ.js.map} +0 -0
- /package/dist/{chunk-JQEFP45L.js.map → chunk-GUJ4Q73I.js.map} +0 -0
- /package/dist/{chunk-ZWOFQRIB.js.map → chunk-HIJUKZHM.js.map} +0 -0
- /package/dist/{chunk-TNCKOL2O.js.map → chunk-LCW7SEPG.js.map} +0 -0
- /package/dist/{chunk-UET462HB.js.map → chunk-TITDJWVZ.js.map} +0 -0
- /package/dist/{chunk-C7FQBSON.js.map → chunk-V3DX4DUF.js.map} +0 -0
- /package/dist/{chunk-DOGPOY4B.js.map → chunk-VDN7LMPL.js.map} +0 -0
- /package/dist/{chunk-EDNJLUSE.js.map → chunk-VJIL6PIL.js.map} +0 -0
- /package/dist/{chunk-NS55SM7A.js.map → chunk-WJJGW5PL.js.map} +0 -0
- /package/dist/{chunks-SX77P5CV.js.map → chunks-CCMRL4MJ.js.map} +0 -0
- /package/dist/{formula-JGFTXWYT.js.map → formula-7RANCW5L.js.map} +0 -0
- /package/dist/{formula-5S7WWXGH.js.map → formula-7UYJOVTB.js.map} +0 -0
- /package/dist/{model-bundle-FSPZZ2EM.js.map → model-bundle-2C5IYDLE.js.map} +0 -0
- /package/dist/{models-CJPAQK5R.js.map → models-SEQZHZFT.js.map} +0 -0
- /package/dist/{pdf-ocr-76SAUXMZ.js.map → pdf-ocr-UUBWBEHL.js.map} +0 -0
- /package/dist/{pdf-ocr-CPU55CVF.js.map → pdf-ocr-VJSF2SQC.js.map} +0 -0
- /package/dist/{profile-io-LBM263K7.js.map → profile-io-RUFZOZO6.js.map} +0 -0
- /package/dist/{rasterize-46RG4ZF6.js.map → rasterize-IOYXR5SI.js.map} +0 -0
- /package/dist/{redact-7ILEAUTS.js.map → redact-NMIKVHMP.js.map} +0 -0
- /package/dist/{render-NGP5T24S.js.map → render-YZ2TEC3N.js.map} +0 -0
- /package/dist/{seal-RGUPOLXD.js.map → seal-ECV44TUH.js.map} +0 -0
- /package/dist/{setup-AU4OCHNH.js.map → setup-MPIEP6XT.js.map} +0 -0
- /package/dist/{visual-NZLYIYEM.js.map → visual-ICMOWUCR.js.map} +0 -0
- /package/dist/{watch-MKPUCPXM.js.map → watch-RYO7MN5B.js.map} +0 -0
|
@@ -0,0 +1,1352 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } 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
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkQEJUY6SKcjs = require('./chunk-QEJUY6SK.cjs');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkUSHQCF5Tcjs = require('./chunk-USHQCF5T.cjs');
|
|
8
|
+
|
|
9
|
+
// src/ocr/ruling-lines.ts
|
|
10
|
+
var INK_LUMA_MAX = 205;
|
|
11
|
+
var MIN_LINE_LENGTH_PT = 20;
|
|
12
|
+
var MAX_LINE_WIDTH_PT = 2.5;
|
|
13
|
+
var GAP_TOL_PX = 2;
|
|
14
|
+
var BAND_OVERLAP_RATIO = 0.5;
|
|
15
|
+
var SURROUND_INK_RATIO = 0.35;
|
|
16
|
+
var DOT_MAX_GAP_PT = 1.3;
|
|
17
|
+
var DOT_MAX_DASH_PT = 4;
|
|
18
|
+
var DOT_MAX_PERIOD_PT = 2.2;
|
|
19
|
+
var DOT_MIN_COUNT = 8;
|
|
20
|
+
var DOT_TOUCH_PT = 1.5;
|
|
21
|
+
var FILL_DELTA = 10;
|
|
22
|
+
var FILL_MIN_LENGTH_PT = 10;
|
|
23
|
+
var FILL_MIN_DEPTH_PT = 4;
|
|
24
|
+
var FILL_FLAT_TOL = 12;
|
|
25
|
+
var FILL_FLAT_RATIO = 0.8;
|
|
26
|
+
function detectRulingLines(rgba, width, height, scale) {
|
|
27
|
+
const minLenPx = Math.round(MIN_LINE_LENGTH_PT * scale);
|
|
28
|
+
const maxThickPx = Math.max(1, Math.floor(MAX_LINE_WIDTH_PT * scale));
|
|
29
|
+
const ink = new Uint8Array(width * height);
|
|
30
|
+
for (let p = 0, i = 0; p < ink.length; p++, i += 4) {
|
|
31
|
+
const luma = rgba[i] * 77 + rgba[i + 1] * 150 + rgba[i + 2] * 29 >> 8;
|
|
32
|
+
if (luma <= INK_LUMA_MAX && rgba[i + 3] >= 128) ink[p] = 1;
|
|
33
|
+
}
|
|
34
|
+
const solid = (at, line, span) => solidRuns(at, line, span, minLenPx);
|
|
35
|
+
const dot = {
|
|
36
|
+
maxGap: Math.max(2, Math.round(DOT_MAX_GAP_PT * scale)),
|
|
37
|
+
maxDash: Math.max(2, Math.round(DOT_MAX_DASH_PT * scale)),
|
|
38
|
+
maxPeriod: DOT_MAX_PERIOD_PT * scale
|
|
39
|
+
};
|
|
40
|
+
const dotted = (at, line, span) => dottedRuns(at, line, span, minLenPx, dot.maxGap, dot.maxDash, dot.maxPeriod);
|
|
41
|
+
const horizontals = detectBands(ink, width, height, maxThickPx, false, solid);
|
|
42
|
+
const verticals = detectBands(ink, width, height, maxThickPx, true, solid);
|
|
43
|
+
const dotH = detectBands(ink, width, height, maxThickPx, false, dotted);
|
|
44
|
+
const dotV = detectBands(ink, width, height, maxThickPx, true, dotted);
|
|
45
|
+
const tol = Math.max(2, Math.round(DOT_TOUCH_PT * scale));
|
|
46
|
+
const allH = [...horizontals, ...dotH], allV = [...verticals, ...dotV];
|
|
47
|
+
const touchesV = (a) => allV.some((p) => p.x1 >= a.x1 - tol && p.x1 <= a.x2 + tol && a.y1 >= p.y1 - tol && a.y1 <= p.y2 + tol);
|
|
48
|
+
const touchesH = (a) => allH.some((p) => p.y1 >= a.y1 - tol && p.y1 <= a.y2 + tol && a.x1 >= p.x1 - tol && a.x1 <= p.x2 + tol);
|
|
49
|
+
horizontals.push(...dotH.filter(touchesV));
|
|
50
|
+
verticals.push(...dotV.filter(touchesH));
|
|
51
|
+
const inkV = [...verticals];
|
|
52
|
+
const ruleH = horizontals.filter((p) => inkV.some((v) => v.x1 >= p.x1 - tol && v.x1 <= p.x2 + tol && p.y1 >= v.y1 - tol && p.y1 <= v.y2 + tol));
|
|
53
|
+
const fill = fillEdges(rgba, width, height, scale);
|
|
54
|
+
const fillH = fill.horizontals.filter((a) => ruleH.some((p) => Math.abs(p.x1 - a.x1) <= tol && Math.abs(p.x2 - a.x2) <= tol));
|
|
55
|
+
horizontals.push(...fillH);
|
|
56
|
+
const edgeH = [...ruleH, ...fillH];
|
|
57
|
+
verticals.push(...fill.verticals.filter((a) => inkV.some((p) => Math.abs(p.x1 - a.x1) <= tol && p.y1 <= a.y2 + tol && p.y2 >= a.y1 - tol) || edgeH.some((p) => (Math.abs(p.x1 - a.x1) <= tol || Math.abs(p.x2 - a.x1) <= tol) && p.y1 >= a.y1 - tol && p.y1 <= a.y2 + tol)));
|
|
58
|
+
return { horizontals, verticals };
|
|
59
|
+
}
|
|
60
|
+
function fillEdges(rgba, width, height, scale) {
|
|
61
|
+
const k = Math.max(1, Math.round(scale));
|
|
62
|
+
const w = Math.floor(width / k), h = Math.floor(height / k), n = w * h;
|
|
63
|
+
const luma = new Uint8Array(n);
|
|
64
|
+
const hist = new Uint32Array(256);
|
|
65
|
+
for (let y = 0; y < h; y++) {
|
|
66
|
+
for (let x = 0; x < w; x++) {
|
|
67
|
+
let mx = 0;
|
|
68
|
+
for (let by = 0; by < k; by++) {
|
|
69
|
+
let i = ((y * k + by) * width + x * k) * 4;
|
|
70
|
+
for (let bx = 0; bx < k; bx++, i += 4) {
|
|
71
|
+
const l = rgba[i] * 77 + rgba[i + 1] * 150 + rgba[i + 2] * 29 >> 8;
|
|
72
|
+
if (l > mx) mx = l;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
luma[y * w + x] = mx;
|
|
76
|
+
hist[mx]++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let bg = 255;
|
|
80
|
+
for (let v = 0; v < 256; v++) if (hist[v] > hist[bg]) bg = v;
|
|
81
|
+
const tint = new Uint8Array(n);
|
|
82
|
+
for (let p = 0; p < n; p++) if (luma[p] < bg - FILL_DELTA) tint[p] = 1;
|
|
83
|
+
const depth = Math.max(2, Math.round(FILL_MIN_DEPTH_PT * scale / k));
|
|
84
|
+
const minLen = Math.round(FILL_MIN_LENGTH_PT * scale / k);
|
|
85
|
+
const solid = (at, line, span) => solidRuns(at, line, span, minLen);
|
|
86
|
+
const run = new Uint16Array(n);
|
|
87
|
+
const mask = new Uint8Array(n);
|
|
88
|
+
const out = { horizontals: [], verticals: [] };
|
|
89
|
+
for (const [dx, dy] of [[0, 1], [0, -1], [1, 0], [-1, 0]]) {
|
|
90
|
+
const step = dy * w + dx;
|
|
91
|
+
const ys = dy > 0 ? [h - 1, -1, -1] : [0, h, 1];
|
|
92
|
+
const xs = dx > 0 ? [w - 1, -1, -1] : [0, w, 1];
|
|
93
|
+
for (let y = ys[0]; y !== ys[1]; y += ys[2]) {
|
|
94
|
+
for (let x = xs[0]; x !== xs[1]; x += xs[2]) {
|
|
95
|
+
const p = y * w + x;
|
|
96
|
+
const nx = x + dx, ny = y + dy;
|
|
97
|
+
run[p] = tint[p] ? nx >= 0 && nx < w && ny >= 0 && ny < h ? Math.min(65535, run[p + step] + 1) : 1 : 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
mask.fill(0);
|
|
101
|
+
for (let y = 0; y < h; y++) {
|
|
102
|
+
for (let x = 0; x < w; x++) {
|
|
103
|
+
const p = y * w + x;
|
|
104
|
+
if (run[p] < depth) continue;
|
|
105
|
+
const ox = x - dx, oy = y - dy;
|
|
106
|
+
if (ox < 0 || oy < 0 || ox >= w || oy >= h || !tint[p - step]) mask[p] = 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const vertical = dx !== 0;
|
|
110
|
+
const flat = (s) => {
|
|
111
|
+
const d = depth >> 1;
|
|
112
|
+
const vals = [];
|
|
113
|
+
if (vertical) {
|
|
114
|
+
const x = Math.round(s.x1) + dx * d;
|
|
115
|
+
for (let y = Math.round(s.y1); y <= s.y2; y++) if (x >= 0 && x < w) vals.push(luma[y * w + x]);
|
|
116
|
+
} else {
|
|
117
|
+
const y = Math.round(s.y1) + dy * d;
|
|
118
|
+
for (let x = Math.round(s.x1); x <= s.x2; x++) if (y >= 0 && y < h) vals.push(luma[y * w + x]);
|
|
119
|
+
}
|
|
120
|
+
if (!vals.length) return false;
|
|
121
|
+
const med = [...vals].sort((a, b) => a - b)[vals.length >> 1];
|
|
122
|
+
return vals.filter((v) => Math.abs(v - med) <= FILL_FLAT_TOL).length >= vals.length * FILL_FLAT_RATIO;
|
|
123
|
+
};
|
|
124
|
+
for (const s of detectBands(mask, w, h, 1, vertical, solid)) {
|
|
125
|
+
if (!flat(s)) continue;
|
|
126
|
+
const c = (v) => v * k + (k - 1) / 2;
|
|
127
|
+
(vertical ? out.verticals : out.horizontals).push({ x1: c(s.x1), y1: c(s.y1), x2: c(s.x2), y2: c(s.y2), thicknessPx: 1 });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function solidRuns(at, line, span, minLenPx) {
|
|
133
|
+
const runs = [];
|
|
134
|
+
let runStart = -1;
|
|
135
|
+
let gap = 0;
|
|
136
|
+
for (let pos = 0; pos <= span; pos++) {
|
|
137
|
+
const on = pos < span && at(line, pos) === 1;
|
|
138
|
+
if (on) {
|
|
139
|
+
if (runStart < 0) runStart = pos;
|
|
140
|
+
gap = 0;
|
|
141
|
+
} else if (runStart >= 0) {
|
|
142
|
+
if (++gap > GAP_TOL_PX || pos >= span) {
|
|
143
|
+
const hi = pos - gap;
|
|
144
|
+
if (hi - runStart + 1 >= minLenPx) runs.push({ lo: runStart, hi });
|
|
145
|
+
runStart = -1;
|
|
146
|
+
gap = 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return runs;
|
|
151
|
+
}
|
|
152
|
+
function dottedRuns(at, line, span, minLenPx, maxGap, maxDash, maxPeriod) {
|
|
153
|
+
const runs = [];
|
|
154
|
+
const chain = [];
|
|
155
|
+
const median = (xs) => xs.sort((a, b) => a - b)[xs.length >> 1];
|
|
156
|
+
const flush = () => {
|
|
157
|
+
const cnt = chain.length >> 1;
|
|
158
|
+
if (cnt >= DOT_MIN_COUNT && chain[chain.length - 1] - chain[0] + 1 >= minLenPx) {
|
|
159
|
+
const periods = [], dashes = [], gaps = [];
|
|
160
|
+
for (let k = 0; k < cnt; k++) {
|
|
161
|
+
dashes.push(chain[2 * k + 1] - chain[2 * k] + 1);
|
|
162
|
+
if (k) {
|
|
163
|
+
periods.push(chain[2 * k] - chain[2 * k - 2]);
|
|
164
|
+
gaps.push(chain[2 * k] - chain[2 * k - 1] - 1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (median(periods) <= maxPeriod || median(dashes) >= 1.5 * median(gaps)) runs.push({ lo: chain[0], hi: chain[chain.length - 1] });
|
|
168
|
+
}
|
|
169
|
+
chain.length = 0;
|
|
170
|
+
};
|
|
171
|
+
let s = -1;
|
|
172
|
+
for (let pos = 0; pos <= span; pos++) {
|
|
173
|
+
const on = pos < span && at(line, pos) === 1;
|
|
174
|
+
if (on) {
|
|
175
|
+
if (s < 0) s = pos;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (s < 0) continue;
|
|
179
|
+
const lo = s, hi = pos - 1;
|
|
180
|
+
s = -1;
|
|
181
|
+
if (hi - lo + 1 > maxDash) {
|
|
182
|
+
flush();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (chain.length && lo - chain[chain.length - 1] - 1 > maxGap) flush();
|
|
186
|
+
chain.push(lo, hi);
|
|
187
|
+
}
|
|
188
|
+
flush();
|
|
189
|
+
return runs;
|
|
190
|
+
}
|
|
191
|
+
function detectBands(ink, width, height, maxThickPx, transpose, readRuns) {
|
|
192
|
+
const lines = transpose ? width : height;
|
|
193
|
+
const span = transpose ? height : width;
|
|
194
|
+
const at = transpose ? (line, pos) => ink[pos * width + line] : (line, pos) => ink[line * width + pos];
|
|
195
|
+
const out = [];
|
|
196
|
+
let open = [];
|
|
197
|
+
const surroundInkRatio = (b, side) => {
|
|
198
|
+
if (side < 0 || side >= lines) return 0;
|
|
199
|
+
let dark = 0, total = 0;
|
|
200
|
+
for (let pos = b.lo; pos <= b.hi; pos += 3) {
|
|
201
|
+
total++;
|
|
202
|
+
if (at(side, pos) === 1) dark++;
|
|
203
|
+
}
|
|
204
|
+
return total > 0 ? dark / total : 0;
|
|
205
|
+
};
|
|
206
|
+
const emit = (b) => {
|
|
207
|
+
const thick = b.last - b.first + 1;
|
|
208
|
+
if (thick > maxThickPx) return;
|
|
209
|
+
if (surroundInkRatio(b, b.first - 2) >= SURROUND_INK_RATIO && surroundInkRatio(b, b.last + 2) >= SURROUND_INK_RATIO) return;
|
|
210
|
+
const center = (b.first + b.last) / 2;
|
|
211
|
+
out.push(
|
|
212
|
+
transpose ? { x1: center, y1: b.lo, x2: center, y2: b.hi, thicknessPx: thick } : { x1: b.lo, y1: center, x2: b.hi, y2: center, thicknessPx: thick }
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
for (let line = 0; line < lines; line++) {
|
|
216
|
+
const runs = readRuns(at, line, span);
|
|
217
|
+
const next = [];
|
|
218
|
+
const used = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const band of open) {
|
|
220
|
+
if (band.last !== line - 1) {
|
|
221
|
+
emit(band);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
let merged = false;
|
|
225
|
+
for (let r = 0; r < runs.length; r++) {
|
|
226
|
+
if (used.has(r)) continue;
|
|
227
|
+
const run = runs[r];
|
|
228
|
+
const overlap = Math.min(band.hi, run.hi) - Math.max(band.lo, run.lo) + 1;
|
|
229
|
+
const shorter = Math.min(band.hi - band.lo, run.hi - run.lo) + 1;
|
|
230
|
+
if (overlap >= shorter * BAND_OVERLAP_RATIO) {
|
|
231
|
+
band.lo = Math.min(band.lo, run.lo);
|
|
232
|
+
band.hi = Math.max(band.hi, run.hi);
|
|
233
|
+
band.last = line;
|
|
234
|
+
next.push(band);
|
|
235
|
+
used.add(r);
|
|
236
|
+
merged = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!merged) emit(band);
|
|
241
|
+
}
|
|
242
|
+
for (let r = 0; r < runs.length; r++) {
|
|
243
|
+
if (!used.has(r)) next.push({ lo: runs[r].lo, hi: runs[r].hi, first: line, last: line });
|
|
244
|
+
}
|
|
245
|
+
open = next;
|
|
246
|
+
}
|
|
247
|
+
for (const band of open) emit(band);
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
250
|
+
function rulingToPdfLines(ruling, scale, pdfHeight) {
|
|
251
|
+
const horizontals = ruling.horizontals.map((s) => {
|
|
252
|
+
const y = pdfHeight - (s.y1 + s.y2) / 2 / scale;
|
|
253
|
+
return {
|
|
254
|
+
x1: Math.min(s.x1, s.x2) / scale,
|
|
255
|
+
y1: y,
|
|
256
|
+
x2: Math.max(s.x1, s.x2) / scale,
|
|
257
|
+
y2: y,
|
|
258
|
+
lineWidth: s.thicknessPx / scale
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
const verticals = ruling.verticals.map((s) => {
|
|
262
|
+
const x = (s.x1 + s.x2) / 2 / scale;
|
|
263
|
+
const yA = pdfHeight - Math.max(s.y1, s.y2) / scale;
|
|
264
|
+
const yB = pdfHeight - Math.min(s.y1, s.y2) / scale;
|
|
265
|
+
return { x1: x, y1: yA, x2: x, y2: yB, lineWidth: s.thicknessPx / scale };
|
|
266
|
+
});
|
|
267
|
+
return { horizontals, verticals };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/ocr/engine.ts
|
|
271
|
+
var _promises = require('fs/promises');
|
|
272
|
+
var _path = require('path');
|
|
273
|
+
|
|
274
|
+
// src/ocr/models.ts
|
|
275
|
+
var OCR_DET_MODEL = {
|
|
276
|
+
name: "PP-OCRv5 mobile det",
|
|
277
|
+
filename: "det.onnx",
|
|
278
|
+
url: "https://huggingface.co/PaddlePaddle/PP-OCRv5_mobile_det_onnx/resolve/main/inference.onnx",
|
|
279
|
+
sha256: "a431985659dc921974177a95adcfbb90fd9e51989a5e04d70d0b75f597b6e61d",
|
|
280
|
+
sizeMb: 5
|
|
281
|
+
};
|
|
282
|
+
var OCR_REC_MODEL = {
|
|
283
|
+
name: "PP-OCRv5 korean rec",
|
|
284
|
+
filename: "rec_korean.onnx",
|
|
285
|
+
url: "https://huggingface.co/PaddlePaddle/korean_PP-OCRv5_mobile_rec_onnx/resolve/main/inference.onnx",
|
|
286
|
+
sha256: "92f0b7785e64fc9090106a241cf4c1eb97472824558272751b88a2a4476d3a08",
|
|
287
|
+
sizeMb: 13
|
|
288
|
+
};
|
|
289
|
+
var OCR_REC_DICT = {
|
|
290
|
+
name: "PP-OCRv5 korean dict",
|
|
291
|
+
filename: "rec_korean.yml",
|
|
292
|
+
url: "https://huggingface.co/PaddlePaddle/korean_PP-OCRv5_mobile_rec_onnx/resolve/main/inference.yml",
|
|
293
|
+
sha256: "f757fa1c40e99edcf27e9cce879b93eb2a51fa46f5ef39095689b8c37dd75998",
|
|
294
|
+
sizeMb: 1
|
|
295
|
+
};
|
|
296
|
+
var ALL_OCR_MODELS = [OCR_DET_MODEL, OCR_REC_MODEL, OCR_REC_DICT];
|
|
297
|
+
function getOcrModelsDir() {
|
|
298
|
+
return _chunkQEJUY6SKcjs.getModelsDir.call(void 0, "ppocr");
|
|
299
|
+
}
|
|
300
|
+
async function ensureOcrModels(onProgress) {
|
|
301
|
+
return _chunkQEJUY6SKcjs.ensureModelsIn.call(void 0, getOcrModelsDir(), ALL_OCR_MODELS, onProgress);
|
|
302
|
+
}
|
|
303
|
+
function parseCharacterDict(yml) {
|
|
304
|
+
const lines = yml.split("\n");
|
|
305
|
+
const chars = [];
|
|
306
|
+
let inDict = false;
|
|
307
|
+
let dictIndent = -1;
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
if (!inDict) {
|
|
310
|
+
const m2 = /^(\s*)character_dict:\s*$/.exec(line);
|
|
311
|
+
if (m2) {
|
|
312
|
+
inDict = true;
|
|
313
|
+
dictIndent = m2[1].length;
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const m = /^(\s*)- (.*)$/.exec(line);
|
|
318
|
+
if (m && m[1].length >= dictIndent) {
|
|
319
|
+
let v = m[2];
|
|
320
|
+
if (v.length >= 2 && (v.startsWith("'") && v.endsWith("'") || v.startsWith('"') && v.endsWith('"'))) {
|
|
321
|
+
v = v.slice(1, -1).replace(/''/g, "'");
|
|
322
|
+
}
|
|
323
|
+
chars.push(v);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (line.trim() !== "") break;
|
|
327
|
+
}
|
|
328
|
+
return chars;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/ocr/line-split.ts
|
|
332
|
+
function grayCrop(rgba, pageW, box) {
|
|
333
|
+
const out = new Uint8Array(box.w * box.h);
|
|
334
|
+
for (let y = 0; y < box.h; y++) {
|
|
335
|
+
let si = ((box.y + y) * pageW + box.x) * 4;
|
|
336
|
+
let di = y * box.w;
|
|
337
|
+
for (let x = 0; x < box.w; x++, si += 4, di++) {
|
|
338
|
+
out[di] = rgba[si] * 77 + rgba[si + 1] * 150 + rgba[si + 2] * 29 >> 8;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function inkStats(gray) {
|
|
344
|
+
const hist = new Uint32Array(256);
|
|
345
|
+
for (let i = 0; i < gray.length; i++) hist[gray[i]]++;
|
|
346
|
+
const total = gray.length;
|
|
347
|
+
let sumAll = 0;
|
|
348
|
+
for (let v = 0; v < 256; v++) sumAll += v * hist[v];
|
|
349
|
+
let wB = 0, sumB = 0, best = -1, thr = 127;
|
|
350
|
+
for (let t = 0; t < 256; t++) {
|
|
351
|
+
wB += hist[t];
|
|
352
|
+
if (wB === 0) continue;
|
|
353
|
+
const wF = total - wB;
|
|
354
|
+
if (wF === 0) break;
|
|
355
|
+
sumB += t * hist[t];
|
|
356
|
+
const mB = sumB / wB;
|
|
357
|
+
const mF = (sumAll - sumB) / wF;
|
|
358
|
+
const between = wB * wF * (mB - mF) * (mB - mF);
|
|
359
|
+
if (between > best) {
|
|
360
|
+
best = between;
|
|
361
|
+
thr = t;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
let nDark = 0, sDark = 0;
|
|
365
|
+
for (let v = 0; v <= thr; v++) {
|
|
366
|
+
nDark += hist[v];
|
|
367
|
+
sDark += v * hist[v];
|
|
368
|
+
}
|
|
369
|
+
const nLight = total - nDark;
|
|
370
|
+
const sLight = sumAll - sDark;
|
|
371
|
+
if (nDark === 0 || nLight === 0) return { threshold: thr, darkInk: true, contrast: 0, inkRatio: 0 };
|
|
372
|
+
const darkInk = nDark <= nLight;
|
|
373
|
+
return {
|
|
374
|
+
threshold: thr,
|
|
375
|
+
darkInk,
|
|
376
|
+
contrast: sLight / nLight - sDark / nDark,
|
|
377
|
+
inkRatio: (darkInk ? nDark : nLight) / total
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function splitRowBands(gray, w, h, ink, minBandRatio) {
|
|
381
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
382
|
+
const colInk = new Uint32Array(w);
|
|
383
|
+
for (let y = 0; y < h; y++) {
|
|
384
|
+
const off = y * w;
|
|
385
|
+
for (let x = 0; x < w; x++) if (isInk(gray[off + x])) colInk[x]++;
|
|
386
|
+
}
|
|
387
|
+
const ruleCol = new Uint8Array(w);
|
|
388
|
+
let liveW = 0;
|
|
389
|
+
for (let x = 0; x < w; x++) {
|
|
390
|
+
if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
|
|
391
|
+
else liveW++;
|
|
392
|
+
}
|
|
393
|
+
const rowInk = new Uint32Array(h);
|
|
394
|
+
for (let y = 0; y < h; y++) {
|
|
395
|
+
let n = 0;
|
|
396
|
+
const off = y * w;
|
|
397
|
+
for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) n++;
|
|
398
|
+
rowInk[y] = n >= liveW * 0.85 ? 0 : n;
|
|
399
|
+
}
|
|
400
|
+
const blank = Math.max(1, Math.floor(w * 0.02));
|
|
401
|
+
let bands = [];
|
|
402
|
+
let start = -1;
|
|
403
|
+
for (let y = 0; y <= h; y++) {
|
|
404
|
+
const on = y < h && rowInk[y] > blank;
|
|
405
|
+
if (on && start < 0) start = y;
|
|
406
|
+
else if (!on && start >= 0) {
|
|
407
|
+
bands.push([start, y]);
|
|
408
|
+
start = -1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const inkCols = (y0, y1) => {
|
|
412
|
+
let x0 = w, x1 = -1;
|
|
413
|
+
for (let y = y0; y < y1; y++) {
|
|
414
|
+
const off = y * w;
|
|
415
|
+
for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) {
|
|
416
|
+
if (x < x0) x0 = x;
|
|
417
|
+
if (x > x1) x1 = x;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return x1 >= x0 ? [x0, x1 + 1] : [0, w];
|
|
421
|
+
};
|
|
422
|
+
if (bands.length <= 1) return [{ y0: _nullishCoalesce(_optionalChain([bands, 'access', _2 => _2[0], 'optionalAccess', _3 => _3[0]]), () => ( 0)), y1: _nullishCoalesce(_optionalChain([bands, 'access', _4 => _4[0], 'optionalAccess', _5 => _5[1]]), () => ( h)), x0: 0, x1: w }];
|
|
423
|
+
const widths = bands.map(([y0, y1]) => {
|
|
424
|
+
const [a, b] = inkCols(y0, y1);
|
|
425
|
+
return b - a;
|
|
426
|
+
}).sort((a, b) => a - b);
|
|
427
|
+
const charSize = widths[widths.length >> 1];
|
|
428
|
+
const minBand = Math.max(3, minBandRatio * charSize);
|
|
429
|
+
for (let i = 0; i + 1 < bands.length; ) {
|
|
430
|
+
const gap = bands[i + 1][0] - bands[i][1];
|
|
431
|
+
if (gap <= Math.max(2, charSize * 0.15) && bands[i + 1][1] - bands[i][0] <= charSize * 1.3) {
|
|
432
|
+
bands.splice(i, 2, [bands[i][0], bands[i + 1][1]]);
|
|
433
|
+
} else i++;
|
|
434
|
+
}
|
|
435
|
+
while (bands.length > 1 && bands[0][0] <= 1 && bands[0][1] - bands[0][0] < minBand) bands.shift();
|
|
436
|
+
while (bands.length > 1 && bands[bands.length - 1][1] >= h - 1 && bands[bands.length - 1][1] - bands[bands.length - 1][0] < minBand) bands.pop();
|
|
437
|
+
for (; ; ) {
|
|
438
|
+
let idx = -1, minH = Infinity;
|
|
439
|
+
for (let i = 0; i < bands.length; i++) {
|
|
440
|
+
const bh = bands[i][1] - bands[i][0];
|
|
441
|
+
if (bh < minBand && bh < minH) {
|
|
442
|
+
minH = bh;
|
|
443
|
+
idx = i;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (idx < 0 || bands.length === 1) break;
|
|
447
|
+
const gapPrev = idx > 0 ? bands[idx][0] - bands[idx - 1][1] : Infinity;
|
|
448
|
+
const gapNext = idx < bands.length - 1 ? bands[idx + 1][0] - bands[idx][1] : Infinity;
|
|
449
|
+
const j = gapPrev <= gapNext ? idx - 1 : idx + 1;
|
|
450
|
+
const a = Math.min(idx, j), b = Math.max(idx, j);
|
|
451
|
+
bands = [...bands.slice(0, a), [bands[a][0], bands[b][1]], ...bands.slice(b + 1)];
|
|
452
|
+
}
|
|
453
|
+
if (bands.length === 1) return [{ y0: bands[0][0], y1: bands[0][1], x0: 0, x1: w }];
|
|
454
|
+
return bands.map(([y0, y1]) => {
|
|
455
|
+
const [x0, x1] = inkCols(y0, y1);
|
|
456
|
+
return { y0, y1, x0, x1 };
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function components(gray, w, h, ink) {
|
|
460
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
461
|
+
const comps = [];
|
|
462
|
+
const label = new Int32Array(w * h);
|
|
463
|
+
const stack = [];
|
|
464
|
+
for (let p0 = 0; p0 < w * h; p0++) {
|
|
465
|
+
if (label[p0] || !isInk(gray[p0])) continue;
|
|
466
|
+
const c = { x0: w, x1: 0, y0: h, y1: 0, id: comps.length + 1 };
|
|
467
|
+
label[p0] = c.id;
|
|
468
|
+
stack.push(p0);
|
|
469
|
+
while (stack.length) {
|
|
470
|
+
const p = stack.pop();
|
|
471
|
+
const x = p % w, y = p / w | 0;
|
|
472
|
+
if (x < c.x0) c.x0 = x;
|
|
473
|
+
if (x + 1 > c.x1) c.x1 = x + 1;
|
|
474
|
+
if (y < c.y0) c.y0 = y;
|
|
475
|
+
if (y + 1 > c.y1) c.y1 = y + 1;
|
|
476
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
477
|
+
const nx = x + dx, ny = y + dy;
|
|
478
|
+
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
|
|
479
|
+
const q = ny * w + nx;
|
|
480
|
+
if (!label[q] && isInk(gray[q])) {
|
|
481
|
+
label[q] = c.id;
|
|
482
|
+
stack.push(q);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (c.y1 - c.y0 < h * 0.85 && c.x1 - c.x0 < w * 0.85) comps.push(c);
|
|
487
|
+
}
|
|
488
|
+
return { comps, label };
|
|
489
|
+
}
|
|
490
|
+
function leadingTriangle(gray, w, h, ink) {
|
|
491
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
492
|
+
let charH = 0;
|
|
493
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
494
|
+
const first = comps.filter((c) => c.y1 - c.y0 >= charH * 0.4).sort((a, b) => a.x0 - b.x0)[0];
|
|
495
|
+
if (!first || charH < 8) return null;
|
|
496
|
+
const cw = first.x1 - first.x0, ch = first.y1 - first.y0;
|
|
497
|
+
if (cw < ch * 0.8 || cw > ch * 1.8) return null;
|
|
498
|
+
const rowSpan = (y) => {
|
|
499
|
+
let n = 0, lo = -1, hi = -1, runs = 0, prev = false;
|
|
500
|
+
for (let x = first.x0; x < first.x1; x++) {
|
|
501
|
+
const on = label[y * w + x] === first.id;
|
|
502
|
+
if (on) {
|
|
503
|
+
n++;
|
|
504
|
+
if (lo < 0) lo = x - first.x0;
|
|
505
|
+
hi = x - first.x0;
|
|
506
|
+
if (!prev) runs++;
|
|
507
|
+
}
|
|
508
|
+
prev = on;
|
|
509
|
+
}
|
|
510
|
+
return { n, lo, hi, runs };
|
|
511
|
+
};
|
|
512
|
+
const base = Math.max(rowSpan(first.y1 - 1).n, rowSpan(first.y1 - 2).n);
|
|
513
|
+
if (base < cw * 0.7) return null;
|
|
514
|
+
for (let y = first.y0; y < first.y0 + Math.max(1, Math.round(ch * 0.2)); y++) {
|
|
515
|
+
const r = rowSpan(y);
|
|
516
|
+
if (r.n === 0) continue;
|
|
517
|
+
if (r.hi - r.lo + 1 > cw * 0.4 || (r.lo + r.hi) / 2 < cw * 0.25 || (r.lo + r.hi) / 2 > cw * 0.75) return null;
|
|
518
|
+
}
|
|
519
|
+
const mid = rowSpan(first.y0 + Math.round(ch * 0.6));
|
|
520
|
+
if (mid.lo < 0 || mid.lo > cw * 0.3 || mid.hi < cw * 0.7) return null;
|
|
521
|
+
return mid.runs >= 2 ? "\u25B3" : "\u25B2";
|
|
522
|
+
}
|
|
523
|
+
function leaderRuns(gray, w, h, ink, minDots) {
|
|
524
|
+
const comps = components(gray, w, h, ink).comps.sort((a, b) => a.x0 - b.x0);
|
|
525
|
+
const blobs = [];
|
|
526
|
+
for (const c of comps) {
|
|
527
|
+
const last = blobs[blobs.length - 1];
|
|
528
|
+
if (last && c.x0 < last.x1) {
|
|
529
|
+
last.x1 = Math.max(last.x1, c.x1);
|
|
530
|
+
last.y0 = Math.min(last.y0, c.y0);
|
|
531
|
+
last.y1 = Math.max(last.y1, c.y1);
|
|
532
|
+
} else blobs.push({ x0: c.x0, x1: c.x1, y0: c.y0, y1: c.y1 });
|
|
533
|
+
}
|
|
534
|
+
let charH = 0;
|
|
535
|
+
for (const b of blobs) charH = Math.max(charH, b.y1 - b.y0);
|
|
536
|
+
if (charH < 8) return [];
|
|
537
|
+
const small = (c) => c.x1 - c.x0 <= charH * 0.3 && c.y1 - c.y0 <= charH * 0.3;
|
|
538
|
+
let bandTop = h, bandBot = 0;
|
|
539
|
+
for (const c of comps) if (!small(c)) {
|
|
540
|
+
bandTop = Math.min(bandTop, c.y0);
|
|
541
|
+
bandBot = Math.max(bandBot, c.y1);
|
|
542
|
+
}
|
|
543
|
+
const cy = (c) => (c.y0 + c.y1) / 2;
|
|
544
|
+
const isDot = (b) => small(b) && cy(b) >= bandTop && cy(b) <= bandBot;
|
|
545
|
+
const runs = [];
|
|
546
|
+
for (let i = 0; i < blobs.length; ) {
|
|
547
|
+
if (!isDot(blobs[i])) {
|
|
548
|
+
i++;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
let j = i;
|
|
552
|
+
while (j + 1 < blobs.length && isDot(blobs[j + 1]) && blobs[j + 1].x0 - blobs[j].x1 <= charH && Math.abs(cy(blobs[j + 1]) - cy(blobs[i])) <= charH * 0.2) j++;
|
|
553
|
+
const x0 = blobs[i].x0, x1 = blobs[j].x1, y = cy(blobs[i]);
|
|
554
|
+
const onLine = comps.some((c) => !small(c) && c.y0 <= y && c.y1 >= y && (c.x1 <= x0 + 2 && c.x1 >= x0 - 2 * charH || c.x0 >= x1 - 2 && c.x0 <= x1 + 2 * charH));
|
|
555
|
+
const run = blobs.slice(i, j + 1).filter((b) => b.x0 > 0 && b.x1 < w);
|
|
556
|
+
const spread = (xs) => {
|
|
557
|
+
const v = [...xs].sort((a, b) => a - b).slice(xs.length >= 5 ? 1 : 0, xs.length >= 5 ? -1 : void 0);
|
|
558
|
+
return v[v.length - 1] / Math.max(1, v[0]);
|
|
559
|
+
};
|
|
560
|
+
const even = run.length >= 2 && spread(run.map((b) => b.x1 - b.x0)) <= 2.5 && spread(run.map((b) => b.y1 - b.y0)) <= 2.5 && spread(run.slice(1).map((b, k) => b.x0 - run[k].x0)) <= 1.8;
|
|
561
|
+
if (j - i + 1 >= minDots && onLine && even) runs.push([x0, x1]);
|
|
562
|
+
i = j + 1;
|
|
563
|
+
}
|
|
564
|
+
return runs;
|
|
565
|
+
}
|
|
566
|
+
function inkBounds(gray, w, h, ink) {
|
|
567
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
568
|
+
const colInk = new Uint32Array(w);
|
|
569
|
+
const rowInk = new Uint32Array(h);
|
|
570
|
+
for (let y = 0; y < h; y++) {
|
|
571
|
+
const off = y * w;
|
|
572
|
+
for (let x = 0; x < w; x++) if (isInk(gray[off + x])) {
|
|
573
|
+
colInk[x]++;
|
|
574
|
+
rowInk[y]++;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const ruleCol = new Uint8Array(w), ruleRow = new Uint8Array(h);
|
|
578
|
+
for (let x = 0; x < w; x++) if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
|
|
579
|
+
for (let y = 0; y < h; y++) if (rowInk[y] >= w * 0.85) ruleRow[y] = 1;
|
|
580
|
+
let x0 = w, x1 = -1, y0 = h, y1 = -1;
|
|
581
|
+
for (let y = 0; y < h; y++) {
|
|
582
|
+
if (ruleRow[y]) continue;
|
|
583
|
+
const off = y * w;
|
|
584
|
+
for (let x = 0; x < w; x++) {
|
|
585
|
+
if (ruleCol[x] || !isInk(gray[off + x])) continue;
|
|
586
|
+
if (x < x0) x0 = x;
|
|
587
|
+
if (x > x1) x1 = x;
|
|
588
|
+
if (y < y0) y0 = y;
|
|
589
|
+
if (y > y1) y1 = y;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (x1 < x0 || y1 < y0) return { x0: 0, y0: 0, x1: w, y1: h };
|
|
593
|
+
return { x0, y0, x1: x1 + 1, y1: y1 + 1 };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/ocr/postprocess.ts
|
|
597
|
+
var HANGUL_START = /^[\uac00-\ud7a3]/;
|
|
598
|
+
function restoreSymbols(text) {
|
|
599
|
+
let s = text;
|
|
600
|
+
s = s.replace(/^([Oo])(\s?)(?=[\uac00-\ud7a3])/, "\u25CB$2");
|
|
601
|
+
s = s.replace(/(?<![A-Za-z])O{2,}(?![A-Za-z])/g, (m, i) => /[\uac00-\ud7a3]$/.test(s.slice(Math.max(0, i - 2), i).trimEnd()) || /^\s?[\uac00-\ud7a3]/.test(s.slice(i + m.length)) ? "\u25CB".repeat(m.length) : m);
|
|
602
|
+
s = s.replace(/[\u2206\u0394]/g, "\u25B3");
|
|
603
|
+
s = s.replace(/\(cid:\d*\)/g, "");
|
|
604
|
+
return smartQuotes(joinDigitGroups(s));
|
|
605
|
+
}
|
|
606
|
+
function joinDigitGroups(s) {
|
|
607
|
+
return s.replace(/(?<![\d,])\d{1,3}(?:, ?\d{3})+(?![\d])/g, (m) => m.replace(/, /g, ","));
|
|
608
|
+
}
|
|
609
|
+
function smartQuotes(s) {
|
|
610
|
+
if (!/['"]/.test(s)) return s;
|
|
611
|
+
const chars = [...s];
|
|
612
|
+
for (const q of ["'", '"']) {
|
|
613
|
+
const [open, close] = q === "'" ? ["\u2018", "\u2019"] : ["\u201C", "\u201D"];
|
|
614
|
+
const pos = [];
|
|
615
|
+
for (let i = 0; i < chars.length; i++) {
|
|
616
|
+
if (chars[i] !== q) continue;
|
|
617
|
+
const prev2 = i > 0 ? chars[i - 1] : "";
|
|
618
|
+
if (q === "'" && !/[\p{L}\p{N}]/u.test(prev2) && /^\d{2}(?!\d)/.test(chars.slice(i + 1, i + 4).join(""))) {
|
|
619
|
+
chars[i] = close;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
pos.push(i);
|
|
623
|
+
}
|
|
624
|
+
if (pos.length === 0) continue;
|
|
625
|
+
const first = pos[0];
|
|
626
|
+
const prev = first > 0 ? chars[first - 1] : "";
|
|
627
|
+
const atOpen = prev === "" || /[\s(\[{<\u300c\u300e\u3010\u3008\u300a\u2018\u201c·,:]/.test(prev);
|
|
628
|
+
let isOpen = pos.length % 2 === 0 || atOpen;
|
|
629
|
+
for (const i of pos) {
|
|
630
|
+
chars[i] = isOpen ? open : close;
|
|
631
|
+
isOpen = !isOpen;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return chars.join("");
|
|
635
|
+
}
|
|
636
|
+
function isDotFragment(text) {
|
|
637
|
+
return /^[\s.\u00b7\u2024\u2025\u2026\u2027\u2219\u22c5\u318d]+$/.test(text);
|
|
638
|
+
}
|
|
639
|
+
function joinLeaderItems(items, ends) {
|
|
640
|
+
const gone = /* @__PURE__ */ new Set();
|
|
641
|
+
const sameLine = (a, b) => Math.abs(a.y + a.h / 2 - (b.y + b.h / 2)) <= Math.max(a.h, b.h) / 2;
|
|
642
|
+
const merge = (left, right) => {
|
|
643
|
+
const x2 = Math.max(left.x + left.w, right.x + right.w), y2 = Math.max(left.y + left.h, right.y + right.h);
|
|
644
|
+
left.text = left.text.replace(/\s*\u2026$/, "") + " \u2026 " + right.text.replace(/^\u2026\s*/, "");
|
|
645
|
+
left.x = Math.min(left.x, right.x);
|
|
646
|
+
left.y = Math.min(left.y, right.y);
|
|
647
|
+
left.w = x2 - left.x;
|
|
648
|
+
left.h = y2 - left.y;
|
|
649
|
+
left.confidence = Math.min(left.confidence, right.confidence);
|
|
650
|
+
gone.add(right);
|
|
651
|
+
};
|
|
652
|
+
const neighbor = (it, toLeft) => {
|
|
653
|
+
let best = null;
|
|
654
|
+
for (const o of items) {
|
|
655
|
+
if (o === it || gone.has(o) || !sameLine(o, it)) continue;
|
|
656
|
+
if (toLeft ? o.x + o.w > it.x + 2 : o.x < it.x + it.w - 2) continue;
|
|
657
|
+
if (!best || (toLeft ? o.x + o.w > best.x + best.w : o.x < best.x)) best = o;
|
|
658
|
+
}
|
|
659
|
+
return best;
|
|
660
|
+
};
|
|
661
|
+
for (const it of [...items].sort((a, b) => b.x - a.x)) {
|
|
662
|
+
if (gone.has(it) || !_optionalChain([ends, 'access', _6 => _6.get, 'call', _7 => _7(it), 'optionalAccess', _8 => _8.lead])) continue;
|
|
663
|
+
const left = neighbor(it, true);
|
|
664
|
+
if (left) merge(left, it);
|
|
665
|
+
}
|
|
666
|
+
for (const it of [...items].sort((a, b) => a.x - b.x)) {
|
|
667
|
+
if (gone.has(it) || !_optionalChain([ends, 'access', _9 => _9.get, 'call', _10 => _10(it), 'optionalAccess', _11 => _11.trail])) continue;
|
|
668
|
+
const right = neighbor(it, false);
|
|
669
|
+
if (right) merge(it, right);
|
|
670
|
+
}
|
|
671
|
+
const toc = items.filter((it) => !gone.has(it) && /\s\u2026\s\d{1,4}$/.test(it.text));
|
|
672
|
+
if (toc.length >= 2) {
|
|
673
|
+
for (const it of items) {
|
|
674
|
+
if (gone.has(it) || toc.includes(it) || !/^[\s.:\u00b7\u2026]*\d{1,4}$/.test(it.text)) continue;
|
|
675
|
+
const x2 = it.x + it.w;
|
|
676
|
+
if (toc.filter((t) => Math.abs(t.x + t.w - x2) <= Math.max(t.h, it.h)).length < 2) continue;
|
|
677
|
+
const left = neighbor(it, true);
|
|
678
|
+
if (!left || toc.includes(left)) continue;
|
|
679
|
+
left.text = left.text.replace(/[\s.:\u00b7\u2026]+$/, "");
|
|
680
|
+
it.text = it.text.replace(/^[\s.:\u00b7\u2026]+/, "");
|
|
681
|
+
merge(left, it);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return items.filter((it) => !gone.has(it));
|
|
685
|
+
}
|
|
686
|
+
function restoreBulletItems(items) {
|
|
687
|
+
for (const it of items) {
|
|
688
|
+
if (it.text !== "O" && it.text !== "o") continue;
|
|
689
|
+
const cy = it.y + it.h / 2;
|
|
690
|
+
let next = null;
|
|
691
|
+
for (const o of items) {
|
|
692
|
+
if (o === it || o.x < it.x + it.w * 0.5) continue;
|
|
693
|
+
if (Math.abs(o.y + o.h / 2 - cy) > Math.max(it.h, o.h) / 2) continue;
|
|
694
|
+
if (o.x - (it.x + it.w) > it.h * 3) continue;
|
|
695
|
+
if (!next || o.x < next.x) next = o;
|
|
696
|
+
}
|
|
697
|
+
if (next && HANGUL_START.test(next.text.trimStart())) it.text = "\u25CB";
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/ocr/crop.ts
|
|
702
|
+
var REC_HEIGHT = 48;
|
|
703
|
+
var REC_MAX_WIDTH = 3200;
|
|
704
|
+
var MIN_SIZE = 3;
|
|
705
|
+
function bandBoxes(b, bands, pageH) {
|
|
706
|
+
const out = [];
|
|
707
|
+
for (let i = 0; i < bands.length; i++) {
|
|
708
|
+
const { y0, y1, x0, x1 } = bands[i];
|
|
709
|
+
const bh = y1 - y0;
|
|
710
|
+
const upGap = i > 0 ? (y0 - bands[i - 1].y1) / 2 : y0;
|
|
711
|
+
const downGap = i < bands.length - 1 ? (bands[i + 1].y0 - y1) / 2 : b.h - y1;
|
|
712
|
+
const top = Math.max(0, Math.round(b.y + y0 - Math.min(bh * 0.2, upGap)));
|
|
713
|
+
const bot = Math.min(pageH, Math.round(b.y + y1 + Math.min(bh * 0.2, downGap)));
|
|
714
|
+
const left = Math.max(b.x, Math.round(b.x + x0 - bh * 0.2));
|
|
715
|
+
const right = Math.min(b.x + b.w, Math.round(b.x + x1 + bh * 0.2));
|
|
716
|
+
if (bot - top >= MIN_SIZE && right - left >= MIN_SIZE) out.push({ x: left, y: top, w: right - left, h: bot - top });
|
|
717
|
+
}
|
|
718
|
+
return out;
|
|
719
|
+
}
|
|
720
|
+
function lineCrop(rgba, pageW, box, rot) {
|
|
721
|
+
const srcW = rot === 0 ? box.w : box.h;
|
|
722
|
+
const srcH = rot === 0 ? box.h : box.w;
|
|
723
|
+
const rw = Math.min(REC_MAX_WIDTH, Math.max(16, Math.round(srcW * REC_HEIGHT / srcH)));
|
|
724
|
+
const rgb = new Uint8Array(rw * REC_HEIGHT * 3);
|
|
725
|
+
const at = (u, v) => {
|
|
726
|
+
let px, py;
|
|
727
|
+
if (rot === 0) {
|
|
728
|
+
px = u;
|
|
729
|
+
py = v;
|
|
730
|
+
} else if (rot === 90) {
|
|
731
|
+
px = box.w - 1 - v;
|
|
732
|
+
py = u;
|
|
733
|
+
} else {
|
|
734
|
+
px = v;
|
|
735
|
+
py = box.h - 1 - u;
|
|
736
|
+
}
|
|
737
|
+
return ((box.y + py) * pageW + (box.x + px)) * 4;
|
|
738
|
+
};
|
|
739
|
+
const fx = srcW / rw;
|
|
740
|
+
const fy = srcH / REC_HEIGHT;
|
|
741
|
+
for (let dy = 0; dy < REC_HEIGHT; dy++) {
|
|
742
|
+
let sy = (dy + 0.5) * fy - 0.5;
|
|
743
|
+
if (sy < 0) sy = 0;
|
|
744
|
+
const y0 = Math.min(srcH - 1, Math.floor(sy));
|
|
745
|
+
const y1 = Math.min(srcH - 1, y0 + 1);
|
|
746
|
+
const wy = sy - y0;
|
|
747
|
+
for (let dx = 0; dx < rw; dx++) {
|
|
748
|
+
let sx = (dx + 0.5) * fx - 0.5;
|
|
749
|
+
if (sx < 0) sx = 0;
|
|
750
|
+
const x0 = Math.min(srcW - 1, Math.floor(sx));
|
|
751
|
+
const x1 = Math.min(srcW - 1, x0 + 1);
|
|
752
|
+
const wx = sx - x0;
|
|
753
|
+
const i00 = at(x0, y0), i01 = at(x1, y0), i10 = at(x0, y1), i11 = at(x1, y1);
|
|
754
|
+
const o = (dy * rw + dx) * 3;
|
|
755
|
+
for (let c = 0; c < 3; c++) {
|
|
756
|
+
const top = rgba[i00 + c] * (1 - wx) + rgba[i01 + c] * wx;
|
|
757
|
+
const bottom = rgba[i10 + c] * (1 - wx) + rgba[i11 + c] * wx;
|
|
758
|
+
rgb[o + c] = Math.round(top * (1 - wy) + bottom * wy);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return { rgb, w: rw };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/ocr/engine.ts
|
|
766
|
+
var DEFAULT_OCR_TUNING = Object.freeze({
|
|
767
|
+
detLongSide: 960,
|
|
768
|
+
detThresh: 0.3,
|
|
769
|
+
detBoxThresh: 0.6,
|
|
770
|
+
detUnclip: 1.5,
|
|
771
|
+
textScore: 0.5,
|
|
772
|
+
recBatch: 1,
|
|
773
|
+
splitTall: true,
|
|
774
|
+
minInkContrast: 35,
|
|
775
|
+
postprocess: true,
|
|
776
|
+
tightBoxes: true,
|
|
777
|
+
splitLeaders: true
|
|
778
|
+
});
|
|
779
|
+
var DET_MIN_SIZE = 3;
|
|
780
|
+
var DET_MAX_BOXES = 1e3;
|
|
781
|
+
var REC_MIN_WIDTH = 320;
|
|
782
|
+
var REC_BATCH_MAX_PIXELS = 48 * 16e3;
|
|
783
|
+
var TALL_RATIO = 1.5;
|
|
784
|
+
var ROTATE_RATIO = 3;
|
|
785
|
+
var LEADER_MIN_DOTS = 4;
|
|
786
|
+
var LEADER_CONTEXT = 0.5;
|
|
787
|
+
var MIN_INK_RATIO = 0.01;
|
|
788
|
+
var DET_MEAN = [0.485, 0.456, 0.406];
|
|
789
|
+
var DET_STD = [0.229, 0.224, 0.225];
|
|
790
|
+
var OcrEngine = class _OcrEngine {
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
constructor(parts) {
|
|
797
|
+
this.det = parts.det;
|
|
798
|
+
this.rec = parts.rec;
|
|
799
|
+
this.dict = parts.dict;
|
|
800
|
+
this.ort = parts.ort;
|
|
801
|
+
this.sharp = parts.sharp;
|
|
802
|
+
}
|
|
803
|
+
static async create() {
|
|
804
|
+
const [ortMod, sharpModRaw] = await Promise.all([
|
|
805
|
+
tryImport("onnxruntime-node", () => Promise.resolve().then(() => _interopRequireWildcard(require("onnxruntime-node")))),
|
|
806
|
+
tryImport(
|
|
807
|
+
"sharp",
|
|
808
|
+
() => Promise.resolve().then(() => _interopRequireWildcard(require("sharp")))
|
|
809
|
+
)
|
|
810
|
+
]);
|
|
811
|
+
const sharpAny = sharpModRaw;
|
|
812
|
+
const sharpMod = typeof sharpAny === "function" ? sharpAny : _nullishCoalesce(sharpAny.default, () => ( sharpAny));
|
|
813
|
+
const dir = getOcrModelsDir();
|
|
814
|
+
const sessionOpts = {
|
|
815
|
+
graphOptimizationLevel: "all",
|
|
816
|
+
executionProviders: ["cpu"],
|
|
817
|
+
logSeverityLevel: 3
|
|
818
|
+
// paddle2onnx 변환 잔여물 W 로그 폭주 억제
|
|
819
|
+
};
|
|
820
|
+
const [det, rec, dictYml] = await Promise.all([
|
|
821
|
+
ortMod.InferenceSession.create(_path.join.call(void 0, dir, OCR_DET_MODEL.filename), sessionOpts),
|
|
822
|
+
ortMod.InferenceSession.create(_path.join.call(void 0, dir, OCR_REC_MODEL.filename), sessionOpts),
|
|
823
|
+
_promises.readFile.call(void 0, _path.join.call(void 0, dir, OCR_REC_DICT.filename), "utf-8")
|
|
824
|
+
]);
|
|
825
|
+
const dict = parseCharacterDict(dictYml);
|
|
826
|
+
if (dict.length === 0) throw new Error("OCR \uC0AC\uC804 \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uBAA8\uB378 \uCE90\uC2DC\uB97C \uC0AD\uC81C \uD6C4 \uC7AC\uB2E4\uC6B4\uB85C\uB4DC\uD558\uC138\uC694");
|
|
827
|
+
return new _OcrEngine({ det, rec, dict, ort: ortMod, sharp: sharpMod });
|
|
828
|
+
}
|
|
829
|
+
/** onnxruntime-node 1.14+ InferenceSession.release() — 구버전은 무시 */
|
|
830
|
+
async destroy() {
|
|
831
|
+
for (const s of [this.det, this.rec]) {
|
|
832
|
+
const rel = s.release;
|
|
833
|
+
if (typeof rel === "function") {
|
|
834
|
+
try {
|
|
835
|
+
await rel.call(s);
|
|
836
|
+
} catch (e2) {
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* 페이지 RGBA 픽셀 → 텍스트 라인 인식.
|
|
843
|
+
* 반환 좌표는 입력 픽셀 기준. 라인은 위→아래, 좌→우 정렬.
|
|
844
|
+
* @param stats 저신뢰(conf<0.5) 폐기 라인 카운트 출력 — 종전엔 무음 폐기라 관측 불가
|
|
845
|
+
*/
|
|
846
|
+
async recognizePage(rgba, width, height, stats, tuning = DEFAULT_OCR_TUNING) {
|
|
847
|
+
if (width < DET_MIN_SIZE || height < DET_MIN_SIZE) return [];
|
|
848
|
+
const boxes = await this.detect(rgba, width, height, tuning);
|
|
849
|
+
const jobs = [];
|
|
850
|
+
const joins = [];
|
|
851
|
+
let group = 0;
|
|
852
|
+
for (const b of boxes) {
|
|
853
|
+
const gray = grayCrop(rgba, width, b);
|
|
854
|
+
const ink = inkStats(gray);
|
|
855
|
+
if (tuning.minInkContrast > 0 && ink.contrast < tuning.minInkContrast) continue;
|
|
856
|
+
if (tuning.minInkContrast > 0 && ink.inkRatio < MIN_INK_RATIO) continue;
|
|
857
|
+
if (tuning.splitTall && b.h >= b.w * TALL_RATIO) {
|
|
858
|
+
const bands = splitRowBands(gray, b.w, b.h, ink, 0.45);
|
|
859
|
+
if (bands.length >= 2) {
|
|
860
|
+
for (const sub of bandBoxes(b, bands, height)) jobs.push({ box: sub, rot: 0, group: group++ });
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (b.h >= b.w * ROTATE_RATIO) {
|
|
864
|
+
for (const rot of [0, 90, 270]) jobs.push({ box: b, rot, group });
|
|
865
|
+
group++;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const leaders = tuning.splitLeaders ? leaderRuns(gray, b.w, b.h, ink, LEADER_MIN_DOTS) : [];
|
|
870
|
+
if (leaders.length) {
|
|
871
|
+
const join2 = joins.length;
|
|
872
|
+
joins.push({ box: b, parts: [], trailDots: false });
|
|
873
|
+
let x0 = 0, dots = false;
|
|
874
|
+
for (const [a, c] of [...leaders, [b.w, b.w]]) {
|
|
875
|
+
const bare = { x: b.x + x0, y: b.y, w: a - x0, h: b.h };
|
|
876
|
+
const end = c > a ? Math.min(c, a + Math.round(b.h * LEADER_CONTEXT)) : a;
|
|
877
|
+
x0 = c;
|
|
878
|
+
if (bare.w >= DET_MIN_SIZE && inkStats(grayCrop(rgba, width, bare)).contrast >= Math.max(1, tuning.minInkContrast)) {
|
|
879
|
+
jobs.push({ box: { ...bare, w: end - (bare.x - b.x) }, rot: 0, group: group++, join: join2, dotsBefore: dots, trimDots: end > a });
|
|
880
|
+
dots = false;
|
|
881
|
+
}
|
|
882
|
+
if (c > a) dots = true;
|
|
883
|
+
}
|
|
884
|
+
joins[join2].trailDots = dots;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
jobs.push({ box: b, rot: 0, group: group++ });
|
|
888
|
+
}
|
|
889
|
+
const results = await this.recognizeJobs(rgba, width, jobs, tuning.recBatch);
|
|
890
|
+
const best = /* @__PURE__ */ new Map();
|
|
891
|
+
jobs.forEach((job, i) => {
|
|
892
|
+
const r = results[i];
|
|
893
|
+
if (!r) return;
|
|
894
|
+
const cur = best.get(job.group);
|
|
895
|
+
if (!cur || r.confidence > cur.confidence) best.set(job.group, { job, ...r });
|
|
896
|
+
});
|
|
897
|
+
let items = [];
|
|
898
|
+
for (const { job, text: raw, confidence } of best.values()) {
|
|
899
|
+
let text = tuning.postprocess ? restoreSymbols(raw.trim()) : raw;
|
|
900
|
+
if (!text.trim()) continue;
|
|
901
|
+
if (tuning.postprocess && /^\d/.test(text) && job.rot === 0) {
|
|
902
|
+
const g = grayCrop(rgba, width, job.box);
|
|
903
|
+
const tri = leadingTriangle(g, job.box.w, job.box.h, inkStats(g));
|
|
904
|
+
if (tri) text = tri + text;
|
|
905
|
+
}
|
|
906
|
+
if (tuning.postprocess && isDotFragment(text)) continue;
|
|
907
|
+
if (confidence < tuning.textScore) {
|
|
908
|
+
if (stats) stats.droppedLowConf++;
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (job.join !== void 0) {
|
|
912
|
+
if (job.trimDots) text = text.replace(/[\s.:\u00b7\u2022\u2024\u2025\u2026\u2219\u22c5\u318d]+$/u, "");
|
|
913
|
+
if (text) joins[job.join].parts.push({ x: job.box.x, text, dotsBefore: job.dotsBefore === true, confidence });
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
items.push({ text, ...this.itemBox(rgba, width, job.box, tuning), confidence });
|
|
917
|
+
}
|
|
918
|
+
const leaderEnds = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const j of joins) {
|
|
920
|
+
if (!j.parts.length) continue;
|
|
921
|
+
j.parts.sort((a, b) => a.x - b.x);
|
|
922
|
+
let text = "";
|
|
923
|
+
for (const p of j.parts) text += (p.dotsBefore ? text ? " \u2026 " : "\u2026" : text ? " " : "") + p.text;
|
|
924
|
+
if (j.trailDots) text += " \u2026";
|
|
925
|
+
const item = { text, ...this.itemBox(rgba, width, j.box, tuning), confidence: Math.min(...j.parts.map((p) => p.confidence)) };
|
|
926
|
+
items.push(item);
|
|
927
|
+
leaderEnds.set(item, { lead: j.parts[0].dotsBefore, trail: j.trailDots });
|
|
928
|
+
}
|
|
929
|
+
if (leaderEnds.size) items = joinLeaderItems(items, leaderEnds);
|
|
930
|
+
if (tuning.postprocess) restoreBulletItems(items);
|
|
931
|
+
items.sort((a, b) => a.y - b.y || a.x - b.x);
|
|
932
|
+
return items;
|
|
933
|
+
}
|
|
934
|
+
/** 결과 좌표 — det 박스 그대로 또는 박스 안 잉크 외곽 (tightBoxes) */
|
|
935
|
+
itemBox(rgba, width, b, tuning) {
|
|
936
|
+
if (!tuning.tightBoxes) return b;
|
|
937
|
+
const gray = grayCrop(rgba, width, b);
|
|
938
|
+
const t = inkBounds(gray, b.w, b.h, inkStats(gray));
|
|
939
|
+
return { x: b.x + t.x0, y: b.y + t.y0, w: t.x1 - t.x0, h: t.y1 - t.y0 };
|
|
940
|
+
}
|
|
941
|
+
// ─── det ─────────────────────────────────────────────
|
|
942
|
+
async detect(rgba, width, height, tuning) {
|
|
943
|
+
const ratio = tuning.detLongSide / Math.max(width, height);
|
|
944
|
+
const dw = Math.max(32, Math.round(width * ratio / 32) * 32);
|
|
945
|
+
const dh = Math.max(32, Math.round(height * ratio / 32) * 32);
|
|
946
|
+
const rgb = await this.sharp(rgba, { raw: { width, height, channels: 4 } }).resize(dw, dh, { fit: "fill" }).removeAlpha().raw().toBuffer();
|
|
947
|
+
const plane = dw * dh;
|
|
948
|
+
const input = new Float32Array(3 * plane);
|
|
949
|
+
for (let i = 0; i < plane; i++) {
|
|
950
|
+
const r = rgb[i * 3] / 255;
|
|
951
|
+
const g = rgb[i * 3 + 1] / 255;
|
|
952
|
+
const b = rgb[i * 3 + 2] / 255;
|
|
953
|
+
input[i] = (b - DET_MEAN[0]) / DET_STD[0];
|
|
954
|
+
input[plane + i] = (g - DET_MEAN[1]) / DET_STD[1];
|
|
955
|
+
input[2 * plane + i] = (r - DET_MEAN[2]) / DET_STD[2];
|
|
956
|
+
}
|
|
957
|
+
const tensor = new this.ort.Tensor("float32", input, [1, 3, dh, dw]);
|
|
958
|
+
const out = await this.det.run({ [this.det.inputNames[0]]: tensor });
|
|
959
|
+
const probMap = out[this.det.outputNames[0]].data;
|
|
960
|
+
const rawBoxes = componentBoxes(probMap, dw, dh, tuning.detThresh, tuning.detBoxThresh);
|
|
961
|
+
const sx = width / dw;
|
|
962
|
+
const sy = height / dh;
|
|
963
|
+
const boxes = [];
|
|
964
|
+
for (const rb of rawBoxes.slice(0, DET_MAX_BOXES)) {
|
|
965
|
+
const bw = rb.x2 - rb.x1 + 1;
|
|
966
|
+
const bh = rb.y2 - rb.y1 + 1;
|
|
967
|
+
const delta = bw * bh * tuning.detUnclip / (2 * (bw + bh));
|
|
968
|
+
const x1 = Math.max(0, Math.floor((rb.x1 - delta) * sx));
|
|
969
|
+
const y1 = Math.max(0, Math.floor((rb.y1 - delta) * sy));
|
|
970
|
+
const x2 = Math.min(width, Math.ceil((rb.x2 + 1 + delta) * sx));
|
|
971
|
+
const y2 = Math.min(height, Math.ceil((rb.y2 + 1 + delta) * sy));
|
|
972
|
+
if (x2 - x1 < DET_MIN_SIZE || y2 - y1 < DET_MIN_SIZE) continue;
|
|
973
|
+
boxes.push({ x: x1, y: y1, w: x2 - x1, h: y2 - y1 });
|
|
974
|
+
}
|
|
975
|
+
return boxes;
|
|
976
|
+
}
|
|
977
|
+
// ─── rec ─────────────────────────────────────────────
|
|
978
|
+
/** 라인 작업들을 폭 비율 순으로 배치 인식 — 결과는 jobs 순서 */
|
|
979
|
+
async recognizeJobs(rgba, pageW, jobs, batchSize) {
|
|
980
|
+
const crops = jobs.map((j) => lineCrop(rgba, pageW, j.box, j.rot));
|
|
981
|
+
const order = crops.map((_, i) => i).sort((a, b) => crops[a].w - crops[b].w);
|
|
982
|
+
const results = new Array(jobs.length).fill(null);
|
|
983
|
+
const plane = REC_HEIGHT;
|
|
984
|
+
for (let s = 0; s < order.length; ) {
|
|
985
|
+
let e = s + 1;
|
|
986
|
+
while (e < order.length && e - s < Math.max(1, batchSize) && (e - s + 1) * Math.max(REC_MIN_WIDTH, crops[order[e]].w) * plane <= REC_BATCH_MAX_PIXELS) e++;
|
|
987
|
+
const idx = order.slice(s, e);
|
|
988
|
+
const bw = Math.max(REC_MIN_WIDTH, crops[idx[idx.length - 1]].w);
|
|
989
|
+
const n = idx.length;
|
|
990
|
+
const chw = 3 * REC_HEIGHT * bw;
|
|
991
|
+
const input = new Float32Array(n * chw);
|
|
992
|
+
idx.forEach((ci, k) => {
|
|
993
|
+
const c = crops[ci];
|
|
994
|
+
const base = k * chw;
|
|
995
|
+
const p = REC_HEIGHT * bw;
|
|
996
|
+
for (let y = 0; y < REC_HEIGHT; y++) {
|
|
997
|
+
for (let x = 0; x < c.w; x++) {
|
|
998
|
+
const src = (y * c.w + x) * 3;
|
|
999
|
+
const dst = base + y * bw + x;
|
|
1000
|
+
input[dst] = c.rgb[src + 2] / 127.5 - 1;
|
|
1001
|
+
input[dst + p] = c.rgb[src + 1] / 127.5 - 1;
|
|
1002
|
+
input[dst + 2 * p] = c.rgb[src] / 127.5 - 1;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
const tensor = new this.ort.Tensor("float32", input, [n, 3, REC_HEIGHT, bw]);
|
|
1007
|
+
const out = await this.rec.run({ [this.rec.inputNames[0]]: tensor });
|
|
1008
|
+
const logits = out[this.rec.outputNames[0]];
|
|
1009
|
+
const [, T, C] = logits.dims;
|
|
1010
|
+
const data = logits.data;
|
|
1011
|
+
idx.forEach((ci, k) => {
|
|
1012
|
+
results[ci] = ctcDecode(data.subarray(k * T * C, (k + 1) * T * C), T, C, this.dict);
|
|
1013
|
+
});
|
|
1014
|
+
s = e;
|
|
1015
|
+
}
|
|
1016
|
+
return results;
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
function ctcDecode(data, T, C, dict) {
|
|
1020
|
+
let text = "";
|
|
1021
|
+
let confSum = 0;
|
|
1022
|
+
let confCount = 0;
|
|
1023
|
+
let prev = -1;
|
|
1024
|
+
for (let t = 0; t < T; t++) {
|
|
1025
|
+
const off = t * C;
|
|
1026
|
+
let best = 0;
|
|
1027
|
+
let bestV = data[off];
|
|
1028
|
+
for (let c = 1; c < C; c++) {
|
|
1029
|
+
const v = data[off + c];
|
|
1030
|
+
if (v > bestV) {
|
|
1031
|
+
bestV = v;
|
|
1032
|
+
best = c;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const repeat = best === prev;
|
|
1036
|
+
prev = best;
|
|
1037
|
+
if (best === 0 || repeat) continue;
|
|
1038
|
+
let p = bestV;
|
|
1039
|
+
if (p > 1.0001 || p < 0) {
|
|
1040
|
+
let denom = 0;
|
|
1041
|
+
for (let c = 0; c < C; c++) denom += Math.exp(data[off + c] - bestV);
|
|
1042
|
+
p = 1 / denom;
|
|
1043
|
+
}
|
|
1044
|
+
confSum += p;
|
|
1045
|
+
confCount++;
|
|
1046
|
+
if (best >= 1 && best <= dict.length) text += dict[best - 1];
|
|
1047
|
+
else if (best === dict.length + 1) text += " ";
|
|
1048
|
+
}
|
|
1049
|
+
if (!text) return null;
|
|
1050
|
+
return { text, confidence: confCount > 0 ? confSum / confCount : 0 };
|
|
1051
|
+
}
|
|
1052
|
+
function componentBoxes(prob, w, h, thresh = DEFAULT_OCR_TUNING.detThresh, boxThresh = DEFAULT_OCR_TUNING.detBoxThresh) {
|
|
1053
|
+
const visited = new Uint8Array(w * h);
|
|
1054
|
+
const boxes = [];
|
|
1055
|
+
const stack = [];
|
|
1056
|
+
for (let start = 0; start < w * h; start++) {
|
|
1057
|
+
if (visited[start] || prob[start] <= thresh) continue;
|
|
1058
|
+
let x1 = start % w, x2 = x1, y1 = start / w | 0, y2 = y1;
|
|
1059
|
+
let sum = 0;
|
|
1060
|
+
let count = 0;
|
|
1061
|
+
stack.length = 0;
|
|
1062
|
+
stack.push(start);
|
|
1063
|
+
visited[start] = 1;
|
|
1064
|
+
while (stack.length) {
|
|
1065
|
+
const p = stack.pop();
|
|
1066
|
+
const px = p % w;
|
|
1067
|
+
const py = p / w | 0;
|
|
1068
|
+
sum += prob[p];
|
|
1069
|
+
count++;
|
|
1070
|
+
if (px < x1) x1 = px;
|
|
1071
|
+
if (px > x2) x2 = px;
|
|
1072
|
+
if (py < y1) y1 = py;
|
|
1073
|
+
if (py > y2) y2 = py;
|
|
1074
|
+
if (px > 0 && !visited[p - 1] && prob[p - 1] > thresh) {
|
|
1075
|
+
visited[p - 1] = 1;
|
|
1076
|
+
stack.push(p - 1);
|
|
1077
|
+
}
|
|
1078
|
+
if (px < w - 1 && !visited[p + 1] && prob[p + 1] > thresh) {
|
|
1079
|
+
visited[p + 1] = 1;
|
|
1080
|
+
stack.push(p + 1);
|
|
1081
|
+
}
|
|
1082
|
+
if (py > 0 && !visited[p - w] && prob[p - w] > thresh) {
|
|
1083
|
+
visited[p - w] = 1;
|
|
1084
|
+
stack.push(p - w);
|
|
1085
|
+
}
|
|
1086
|
+
if (py < h - 1 && !visited[p + w] && prob[p + w] > thresh) {
|
|
1087
|
+
visited[p + w] = 1;
|
|
1088
|
+
stack.push(p + w);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (x2 - x1 + 1 < DET_MIN_SIZE && y2 - y1 + 1 < DET_MIN_SIZE) continue;
|
|
1092
|
+
boxes.push({ x1, y1, x2, y2, score: sum / count });
|
|
1093
|
+
}
|
|
1094
|
+
return boxes.filter((b) => b.score >= boxThresh).sort((a, b) => a.y1 - b.y1 || a.x1 - b.x1);
|
|
1095
|
+
}
|
|
1096
|
+
async function tryImport(name, loader) {
|
|
1097
|
+
try {
|
|
1098
|
+
return await loader();
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`\uB0B4\uC7A5 OCR \uC744 \uC0AC\uC6A9\uD558\uB824\uBA74 optional dependency '${name}' \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. \`npm install ${name}\` \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694. \uC6D0\uC778: ${e.message}`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
var enginePromise = null;
|
|
1106
|
+
function getOcrEngine() {
|
|
1107
|
+
if (!enginePromise) {
|
|
1108
|
+
enginePromise = OcrEngine.create().catch((err) => {
|
|
1109
|
+
enginePromise = null;
|
|
1110
|
+
throw err;
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
return enginePromise;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/ocr/deskew.ts
|
|
1117
|
+
var MAX_SKEW_DEG = 5;
|
|
1118
|
+
var MIN_APPLY_DEG = 0.25;
|
|
1119
|
+
var MIN_GAIN = 1.03;
|
|
1120
|
+
var TARGET_LONG = 1e3;
|
|
1121
|
+
function estimateSkew(rgba, width, height) {
|
|
1122
|
+
const step = Math.max(1, Math.ceil(Math.max(width, height) / TARGET_LONG));
|
|
1123
|
+
const sw = Math.floor(width / step), sh = Math.floor(height / step);
|
|
1124
|
+
const xs = [], ys = [];
|
|
1125
|
+
for (let y = 0; y < sh; y++) {
|
|
1126
|
+
for (let x = 0; x < sw; x++) {
|
|
1127
|
+
const i = (y * step * width + x * step) * 4;
|
|
1128
|
+
const l = rgba[i] * 77 + rgba[i + 1] * 150 + rgba[i + 2] * 29 >> 8;
|
|
1129
|
+
if (l < 128) {
|
|
1130
|
+
xs.push(x);
|
|
1131
|
+
ys.push(y);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (xs.length < 200) return { angle: 0, gain: 1 };
|
|
1136
|
+
const diag = Math.ceil(Math.hypot(sw, sh));
|
|
1137
|
+
const hist = new Float64Array(2 * diag + 2);
|
|
1138
|
+
const score = (deg) => {
|
|
1139
|
+
const t = deg * Math.PI / 180;
|
|
1140
|
+
const c = Math.cos(t), s = Math.sin(t);
|
|
1141
|
+
hist.fill(0);
|
|
1142
|
+
for (let k = 0; k < xs.length; k++) hist[Math.round(ys[k] * c + xs[k] * s) + diag]++;
|
|
1143
|
+
let sum = 0;
|
|
1144
|
+
for (let i = 0; i < hist.length; i++) sum += hist[i] * hist[i];
|
|
1145
|
+
return sum;
|
|
1146
|
+
};
|
|
1147
|
+
const s0 = score(0);
|
|
1148
|
+
let best = 0, bestS = s0;
|
|
1149
|
+
for (let d = -MAX_SKEW_DEG; d <= MAX_SKEW_DEG + 1e-9; d += 0.2) {
|
|
1150
|
+
const v = score(d);
|
|
1151
|
+
if (v > bestS) {
|
|
1152
|
+
bestS = v;
|
|
1153
|
+
best = d;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const coarse = best;
|
|
1157
|
+
for (let d = coarse - 0.2; d <= coarse + 0.2 + 1e-9; d += 0.02) {
|
|
1158
|
+
const v = score(d);
|
|
1159
|
+
if (v > bestS) {
|
|
1160
|
+
bestS = v;
|
|
1161
|
+
best = d;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return { angle: +(-best).toFixed(2), gain: s0 > 0 ? bestS / s0 : 1 };
|
|
1165
|
+
}
|
|
1166
|
+
function deskewPage(rgba, width, height) {
|
|
1167
|
+
const est = estimateSkew(rgba, width, height);
|
|
1168
|
+
if (Math.abs(est.angle) < MIN_APPLY_DEG || est.gain < MIN_GAIN) return { rgba, angle: 0 };
|
|
1169
|
+
return { rgba: rotateRgba(rgba, width, height, est.angle), angle: est.angle };
|
|
1170
|
+
}
|
|
1171
|
+
function rotateRgba(rgba, width, height, deg) {
|
|
1172
|
+
const out = new Uint8Array(rgba.length);
|
|
1173
|
+
const t = deg * Math.PI / 180;
|
|
1174
|
+
const c = Math.cos(t), s = Math.sin(t);
|
|
1175
|
+
const cx = (width - 1) / 2, cy = (height - 1) / 2;
|
|
1176
|
+
for (let y = 0; y < height; y++) {
|
|
1177
|
+
const dy = y - cy;
|
|
1178
|
+
for (let x = 0; x < width; x++) {
|
|
1179
|
+
const dx = x - cx;
|
|
1180
|
+
const sx = c * dx - s * dy + cx;
|
|
1181
|
+
const sy = s * dx + c * dy + cy;
|
|
1182
|
+
const o = (y * width + x) * 4;
|
|
1183
|
+
const x0 = Math.floor(sx), y0 = Math.floor(sy);
|
|
1184
|
+
if (x0 < 0 || y0 < 0 || x0 >= width - 1 || y0 >= height - 1) {
|
|
1185
|
+
out[o] = 255;
|
|
1186
|
+
out[o + 1] = 255;
|
|
1187
|
+
out[o + 2] = 255;
|
|
1188
|
+
out[o + 3] = 255;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const wx = sx - x0, wy = sy - y0;
|
|
1192
|
+
const i00 = (y0 * width + x0) * 4, i01 = i00 + 4, i10 = i00 + width * 4, i11 = i10 + 4;
|
|
1193
|
+
for (let ch = 0; ch < 4; ch++) {
|
|
1194
|
+
const top = rgba[i00 + ch] * (1 - wx) + rgba[i01 + ch] * wx;
|
|
1195
|
+
const bot = rgba[i10 + ch] * (1 - wx) + rgba[i11 + ch] * wx;
|
|
1196
|
+
out[o + ch] = Math.round(top * (1 - wy) + bot * wy);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return out;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/ocr/pdf-ocr.ts
|
|
1204
|
+
var OCR_RENDER_SCALE = 3;
|
|
1205
|
+
var PAGE_TIMEOUT_MS = 12e4;
|
|
1206
|
+
async function runPdfOcr(buffer, targets, mode, warnings, onProgress, detectTables = true, vectorOps) {
|
|
1207
|
+
const result = /* @__PURE__ */ new Map();
|
|
1208
|
+
if (targets.size === 0) return result;
|
|
1209
|
+
const pdfiumMod = await tryImport2(
|
|
1210
|
+
"@hyzyla/pdfium",
|
|
1211
|
+
() => Promise.resolve().then(() => _interopRequireWildcard(require("@hyzyla/pdfium")))
|
|
1212
|
+
);
|
|
1213
|
+
if (mode === "builtin") {
|
|
1214
|
+
await ensureOcrModels((p) => {
|
|
1215
|
+
if (p.phase === "download" && p.downloaded === 0) {
|
|
1216
|
+
process.stderr.write(`[kordoc-ocr] ${p.spec.name} \uB2E4\uC6B4\uB85C\uB4DC \uC911 (~${p.spec.sizeMb}MB)...
|
|
1217
|
+
`);
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
const engine = mode === "builtin" ? await getOcrEngine() : null;
|
|
1222
|
+
const pdfium = await pdfiumMod.PDFiumLibrary.init();
|
|
1223
|
+
const doc = await pdfium.loadDocument(new Uint8Array(buffer));
|
|
1224
|
+
try {
|
|
1225
|
+
let done = 0;
|
|
1226
|
+
for (const page of doc.pages()) {
|
|
1227
|
+
const pageNo = page.number + 1;
|
|
1228
|
+
if (!targets.has(pageNo)) continue;
|
|
1229
|
+
_optionalChain([onProgress, 'optionalCall', _12 => _12(++done, targets.size)]);
|
|
1230
|
+
try {
|
|
1231
|
+
const blocks = await withTimeout(
|
|
1232
|
+
ocrOnePage(page, pageNo, mode, engine, warnings, detectTables, _optionalChain([vectorOps, 'optionalAccess', _13 => _13.get, 'call', _14 => _14(pageNo)])),
|
|
1233
|
+
PAGE_TIMEOUT_MS,
|
|
1234
|
+
`OCR \uD398\uC774\uC9C0 ${pageNo} \uD0C0\uC784\uC544\uC6C3 (${PAGE_TIMEOUT_MS / 1e3}\uCD08)`
|
|
1235
|
+
);
|
|
1236
|
+
result.set(pageNo, blocks);
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
warnings.push({
|
|
1239
|
+
page: pageNo,
|
|
1240
|
+
message: `\uD398\uC774\uC9C0 ${pageNo} OCR \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`,
|
|
1241
|
+
code: "OCR_FAILED"
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
} finally {
|
|
1246
|
+
doc.destroy();
|
|
1247
|
+
pdfium.destroy();
|
|
1248
|
+
}
|
|
1249
|
+
return result;
|
|
1250
|
+
}
|
|
1251
|
+
async function ocrOnePage(page, pageNo, mode, engine, warnings, detectTables, vectorOps) {
|
|
1252
|
+
const { originalWidth: pdfW, originalHeight: pdfH } = page.getOriginalSize();
|
|
1253
|
+
const rendered = await page.render({
|
|
1254
|
+
scale: OCR_RENDER_SCALE,
|
|
1255
|
+
render: async ({ data }) => data
|
|
1256
|
+
});
|
|
1257
|
+
const { data: bgra, width: rw, height: rh } = rendered;
|
|
1258
|
+
const rgba = bgraToRgba(bgra);
|
|
1259
|
+
if (mode === "builtin") {
|
|
1260
|
+
const upright = vectorOps ? rgba : deskewPage(rgba, rw, rh).rgba;
|
|
1261
|
+
const stats = { droppedLowConf: 0 };
|
|
1262
|
+
const items = await engine.recognizePage(upright, rw, rh, stats);
|
|
1263
|
+
if (stats.droppedLowConf > 0) {
|
|
1264
|
+
warnings.push({
|
|
1265
|
+
page: pageNo,
|
|
1266
|
+
message: `\uD398\uC774\uC9C0 ${pageNo}: \uC800\uC2E0\uB8B0 OCR \uB77C\uC778 ${stats.droppedLowConf}\uAC1C \uD3D0\uAE30 (\uC778\uC2DD \uACB0\uC190 \uAC00\uB2A5)`,
|
|
1267
|
+
code: "OCR_LOW_CONF"
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
const scale = rh / pdfH;
|
|
1271
|
+
if (vectorOps) return ocrItemsToBlocks(items, pageNo, pdfW, pdfH, scale, void 0, detectTables, vectorOps);
|
|
1272
|
+
const ruling = detectRulingLines(upright, rw, rh, scale);
|
|
1273
|
+
const extraLines = rulingToPdfLines(ruling, scale, pdfH);
|
|
1274
|
+
return ocrItemsToBlocks(items, pageNo, pdfW, pdfH, scale, extraLines, detectTables);
|
|
1275
|
+
}
|
|
1276
|
+
const sharpModRaw = await tryImport2(
|
|
1277
|
+
"sharp",
|
|
1278
|
+
() => Promise.resolve().then(() => _interopRequireWildcard(require("sharp")))
|
|
1279
|
+
);
|
|
1280
|
+
const sharpAny = sharpModRaw;
|
|
1281
|
+
const sharp = typeof sharpAny === "function" ? sharpAny : _nullishCoalesce(sharpAny.default, () => ( sharpAny));
|
|
1282
|
+
const png = await sharp(rgba, { raw: { width: rw, height: rh, channels: 4 } }).png().toBuffer();
|
|
1283
|
+
const text = await mode(new Uint8Array(png), pageNo, "image/png");
|
|
1284
|
+
if (!text.trim()) {
|
|
1285
|
+
warnings.push({ page: pageNo, message: `\uD398\uC774\uC9C0 ${pageNo} OCR \uACB0\uACFC \uC5C6\uC74C`, code: "OCR_FAILED" });
|
|
1286
|
+
return [];
|
|
1287
|
+
}
|
|
1288
|
+
return [{ type: "paragraph", text: text.trim(), pageNumber: pageNo }];
|
|
1289
|
+
}
|
|
1290
|
+
function ocrItemsToBlocks(items, pageNumber, pdfW, pdfH, scale, extraLines, detectTables = true, opList = { fnArray: [], argsArray: [] }) {
|
|
1291
|
+
const norm = items.map((it) => {
|
|
1292
|
+
const h = it.h / scale;
|
|
1293
|
+
return {
|
|
1294
|
+
text: it.text,
|
|
1295
|
+
x: Math.round(it.x / scale),
|
|
1296
|
+
// NormItem.y 는 pdfjs transform[5] = 베이스라인 (bottom-up) — 잉크 하단으로 근사
|
|
1297
|
+
y: Math.round(pdfH - (it.y + it.h) / scale),
|
|
1298
|
+
w: Math.round(it.w / scale),
|
|
1299
|
+
h: Math.round(h),
|
|
1300
|
+
// 박스는 잉크 외곽(engine tightBoxes) — 한글 줄 잉크 높이 ≈ 0.9em
|
|
1301
|
+
fontSize: Math.max(1, Math.round(h / 0.9)),
|
|
1302
|
+
fontName: "ocr",
|
|
1303
|
+
isHidden: false
|
|
1304
|
+
};
|
|
1305
|
+
});
|
|
1306
|
+
return _chunkUSHQCF5Tcjs.extractPageBlocksWithLines.call(void 0, norm, pageNumber, opList, pdfW, pdfH, extraLines, detectTables);
|
|
1307
|
+
}
|
|
1308
|
+
async function tryImport2(name, loader) {
|
|
1309
|
+
try {
|
|
1310
|
+
return await loader();
|
|
1311
|
+
} catch (e) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`OCR \uC744 \uC0AC\uC6A9\uD558\uB824\uBA74 optional dependency '${name}' \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. \`npm install ${name}\` \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694. \uC6D0\uC778: ${e.message}`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async function withTimeout(promise, ms, msg) {
|
|
1318
|
+
promise.catch(() => {
|
|
1319
|
+
});
|
|
1320
|
+
let timer;
|
|
1321
|
+
try {
|
|
1322
|
+
return await Promise.race([
|
|
1323
|
+
promise,
|
|
1324
|
+
new Promise((_, reject) => {
|
|
1325
|
+
timer = setTimeout(() => reject(new Error(msg)), ms);
|
|
1326
|
+
})
|
|
1327
|
+
]);
|
|
1328
|
+
} finally {
|
|
1329
|
+
if (timer) clearTimeout(timer);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function bgraToRgba(bgra) {
|
|
1333
|
+
const out = new Uint8Array(bgra.length);
|
|
1334
|
+
for (let i = 0; i < bgra.length; i += 4) {
|
|
1335
|
+
out[i] = bgra[i + 2];
|
|
1336
|
+
out[i + 1] = bgra[i + 1];
|
|
1337
|
+
out[i + 2] = bgra[i];
|
|
1338
|
+
out[i + 3] = bgra[i + 3];
|
|
1339
|
+
}
|
|
1340
|
+
return out;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
exports.ensureOcrModels = ensureOcrModels; exports.getOcrEngine = getOcrEngine; exports.detectRulingLines = detectRulingLines; exports.rulingToPdfLines = rulingToPdfLines; exports.deskewPage = deskewPage; exports.runPdfOcr = runPdfOcr; exports.ocrItemsToBlocks = ocrItemsToBlocks;
|
|
1352
|
+
//# sourceMappingURL=chunk-IMAAFSAX.cjs.map
|