kordoc 4.14.2 → 4.14.3

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.
Files changed (151) hide show
  1. package/README.md +38 -5
  2. package/dist/{-AQKXLFIM.js → -7XBPXCXN.js} +13 -11
  3. package/dist/{chunk-FOJZNMMK.js → chunk-2LQGSDZ6.js} +1 -1
  4. package/dist/chunk-2LQGSDZ6.js.map +1 -0
  5. package/dist/chunk-4R67INKT.js +1002 -0
  6. package/dist/chunk-4R67INKT.js.map +1 -0
  7. package/dist/{chunk-ILU76UF3.js → chunk-4RJ5D7MT.js} +2 -2
  8. package/dist/chunk-4RJ5D7MT.js.map +1 -0
  9. package/dist/{chunk-EQZI77PP.cjs → chunk-5VVS2QIJ.cjs} +3 -3
  10. package/dist/{chunk-EQZI77PP.cjs.map → chunk-5VVS2QIJ.cjs.map} +1 -1
  11. package/dist/{chunk-3DUADAY2.js → chunk-A275ZQLX.js} +209 -87
  12. package/dist/chunk-A275ZQLX.js.map +1 -0
  13. package/dist/{chunk-EISO3POT.js → chunk-CVEERPMP.js} +788 -194
  14. package/dist/chunk-CVEERPMP.js.map +1 -0
  15. package/dist/{chunk-ZWD734EY.js → chunk-D6KQKMU3.js} +2 -2
  16. package/dist/{chunk-QOMNEWCQ.cjs → chunk-EBZUZC6F.cjs} +305 -31
  17. package/dist/chunk-EBZUZC6F.cjs.map +1 -0
  18. package/dist/{chunk-HR2XNERB.cjs → chunk-EMGF6EEV.cjs} +2 -2
  19. package/dist/{chunk-HR2XNERB.cjs.map → chunk-EMGF6EEV.cjs.map} +1 -1
  20. package/dist/{chunk-GDW4YP7K.js → chunk-FAZR34CA.js} +2 -2
  21. package/dist/{chunk-TNCKOL2O.js → chunk-GTINVM5K.js} +2 -2
  22. package/dist/{chunk-L7DMSMAG.cjs → chunk-HX4UUF7Y.cjs} +1 -1
  23. package/dist/chunk-HX4UUF7Y.cjs.map +1 -0
  24. package/dist/{chunk-UET462HB.js → chunk-I4NNYWIR.js} +2 -2
  25. package/dist/chunk-IH56ZISC.js +620 -0
  26. package/dist/chunk-IH56ZISC.js.map +1 -0
  27. package/dist/{chunk-EDNJLUSE.js → chunk-IHZEFW4L.js} +2 -2
  28. package/dist/chunk-IW2CB755.cjs +1002 -0
  29. package/dist/chunk-IW2CB755.cjs.map +1 -0
  30. package/dist/{chunk-ZWOFQRIB.js → chunk-JCVIOLBF.js} +3 -3
  31. package/dist/{chunk-JQEFP45L.js → chunk-L5EH2NVO.js} +2 -2
  32. package/dist/{chunk-247ANM72.cjs → chunk-NOAQRCV6.cjs} +219 -97
  33. package/dist/chunk-NOAQRCV6.cjs.map +1 -0
  34. package/dist/{chunk-7IAQ4HOK.js → chunk-NQCUAGLR.js} +1 -1
  35. package/dist/chunk-NQCUAGLR.js.map +1 -0
  36. package/dist/{chunk-NS55SM7A.js → chunk-O2PGQNYE.js} +2 -2
  37. package/dist/{chunk-QC5T6D4J.js → chunk-QGW7NMTX.js} +299 -25
  38. package/dist/chunk-QGW7NMTX.js.map +1 -0
  39. package/dist/{chunk-A7QC2ZVP.js → chunk-UVVEBI22.js} +209 -87
  40. package/dist/chunk-UVVEBI22.js.map +1 -0
  41. package/dist/chunk-VJHAUMCF.js +949 -0
  42. package/dist/chunk-VJHAUMCF.js.map +1 -0
  43. package/dist/{chunk-C7FQBSON.js → chunk-VNFJPTU4.js} +2 -2
  44. package/dist/{chunk-VQ5W4MYO.js → chunk-VYOGXNQ3.js} +298 -24
  45. package/dist/chunk-VYOGXNQ3.js.map +1 -0
  46. package/dist/{chunk-DOGPOY4B.js → chunk-WB5LW52Z.js} +2 -2
  47. package/dist/{chunk-TSFS5VKD.js → chunk-YGBZABI4.js} +332 -141
  48. package/dist/chunk-YGBZABI4.js.map +1 -0
  49. package/dist/chunks-I7TEMJXH.js +10 -0
  50. package/dist/cli.js +228 -97
  51. package/dist/cli.js.map +1 -1
  52. package/dist/{formula-PJJ6S3Z2.cjs → formula-CPO2TZGB.cjs} +9 -9
  53. package/dist/{formula-PJJ6S3Z2.cjs.map → formula-CPO2TZGB.cjs.map} +1 -1
  54. package/dist/{formula-5S7WWXGH.js → formula-NOIDZEBA.js} +4 -4
  55. package/dist/{formula-JGFTXWYT.js → formula-X44GV7TN.js} +3 -3
  56. package/dist/{image-ocr-WCAYXLOA.js → image-ocr-72RAR4WR.js} +34 -16
  57. package/dist/image-ocr-72RAR4WR.js.map +1 -0
  58. package/dist/{image-ocr-FLIL7MGN.js → image-ocr-R6W6A75P.js} +32 -14
  59. package/dist/image-ocr-R6W6A75P.js.map +1 -0
  60. package/dist/{image-ocr-ZTYAGV3W.cjs → image-ocr-SUGT64ZM.cjs} +36 -18
  61. package/dist/image-ocr-SUGT64ZM.cjs.map +1 -0
  62. package/dist/index.cjs +2416 -1184
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +62 -25
  65. package/dist/index.d.ts +62 -25
  66. package/dist/index.js +1853 -621
  67. package/dist/index.js.map +1 -1
  68. package/dist/mcp.js +74 -53
  69. package/dist/mcp.js.map +1 -1
  70. package/dist/{model-bundle-FSPZZ2EM.js → model-bundle-57CLEBCO.js} +5 -5
  71. package/dist/{models-CJPAQK5R.js → models-7ELIFTD7.js} +5 -5
  72. package/dist/{parser-SCUTZAIJ.js → parser-4NKAYWAX.js} +261 -241
  73. package/dist/parser-4NKAYWAX.js.map +1 -0
  74. package/dist/{parser-A4TMPJAM.js → parser-HWFQRSUQ.js} +261 -241
  75. package/dist/parser-HWFQRSUQ.js.map +1 -0
  76. package/dist/{parser-VCXE4I5A.cjs → parser-RANGXGBD.cjs} +289 -269
  77. package/dist/parser-RANGXGBD.cjs.map +1 -0
  78. package/dist/pdf-ocr-BNC34MNU.js +13 -0
  79. package/dist/pdf-ocr-QX2GB33Q.js +16 -0
  80. package/dist/pdf-ocr-VHSFACH7.cjs +14 -0
  81. package/dist/{pdf-ocr-MBZZ57LD.cjs.map → pdf-ocr-VHSFACH7.cjs.map} +1 -1
  82. package/dist/{profile-io-LBM263K7.js → profile-io-V52PL5ZO.js} +3 -3
  83. package/dist/{rasterize-46RG4ZF6.js → rasterize-ZIGLJDOG.js} +3 -3
  84. package/dist/redact-TIQEQI3M.js +18 -0
  85. package/dist/redact-doc-KXZ3TZAG.js +1216 -0
  86. package/dist/redact-doc-KXZ3TZAG.js.map +1 -0
  87. package/dist/{render-NGP5T24S.js → render-U5PE6V2E.js} +7 -7
  88. package/dist/seal-DNXJBILR.js +10 -0
  89. package/dist/{setup-AU4OCHNH.js → setup-YRDM4ODP.js} +3 -3
  90. package/dist/visual-5XWMA4EV.js +19 -0
  91. package/dist/{watch-MKPUCPXM.js → watch-5IOLBATT.js} +12 -12
  92. package/package.json +2 -2
  93. package/dist/chunk-247ANM72.cjs.map +0 -1
  94. package/dist/chunk-3DUADAY2.js.map +0 -1
  95. package/dist/chunk-422427XJ.cjs +0 -571
  96. package/dist/chunk-422427XJ.cjs.map +0 -1
  97. package/dist/chunk-7IAQ4HOK.js.map +0 -1
  98. package/dist/chunk-A7QC2ZVP.js.map +0 -1
  99. package/dist/chunk-DZIXKL7E.js +0 -145
  100. package/dist/chunk-DZIXKL7E.js.map +0 -1
  101. package/dist/chunk-EISO3POT.js.map +0 -1
  102. package/dist/chunk-EP7KOI2V.js +0 -571
  103. package/dist/chunk-EP7KOI2V.js.map +0 -1
  104. package/dist/chunk-FOJZNMMK.js.map +0 -1
  105. package/dist/chunk-ILU76UF3.js.map +0 -1
  106. package/dist/chunk-KVWV4ULP.js +0 -516
  107. package/dist/chunk-KVWV4ULP.js.map +0 -1
  108. package/dist/chunk-L7DMSMAG.cjs.map +0 -1
  109. package/dist/chunk-QC5T6D4J.js.map +0 -1
  110. package/dist/chunk-QOMNEWCQ.cjs.map +0 -1
  111. package/dist/chunk-TSFS5VKD.js.map +0 -1
  112. package/dist/chunk-VQ5W4MYO.js.map +0 -1
  113. package/dist/chunks-SX77P5CV.js +0 -10
  114. package/dist/image-ocr-FLIL7MGN.js.map +0 -1
  115. package/dist/image-ocr-WCAYXLOA.js.map +0 -1
  116. package/dist/image-ocr-ZTYAGV3W.cjs.map +0 -1
  117. package/dist/parser-A4TMPJAM.js.map +0 -1
  118. package/dist/parser-SCUTZAIJ.js.map +0 -1
  119. package/dist/parser-VCXE4I5A.cjs.map +0 -1
  120. package/dist/pdf-ocr-76SAUXMZ.js +0 -13
  121. package/dist/pdf-ocr-CPU55CVF.js +0 -16
  122. package/dist/pdf-ocr-MBZZ57LD.cjs +0 -14
  123. package/dist/redact-7ILEAUTS.js +0 -12
  124. package/dist/seal-RGUPOLXD.js +0 -10
  125. package/dist/visual-NZLYIYEM.js +0 -19
  126. /package/dist/{-AQKXLFIM.js.map → -7XBPXCXN.js.map} +0 -0
  127. /package/dist/{chunk-ZWD734EY.js.map → chunk-D6KQKMU3.js.map} +0 -0
  128. /package/dist/{chunk-GDW4YP7K.js.map → chunk-FAZR34CA.js.map} +0 -0
  129. /package/dist/{chunk-TNCKOL2O.js.map → chunk-GTINVM5K.js.map} +0 -0
  130. /package/dist/{chunk-UET462HB.js.map → chunk-I4NNYWIR.js.map} +0 -0
  131. /package/dist/{chunk-EDNJLUSE.js.map → chunk-IHZEFW4L.js.map} +0 -0
  132. /package/dist/{chunk-ZWOFQRIB.js.map → chunk-JCVIOLBF.js.map} +0 -0
  133. /package/dist/{chunk-JQEFP45L.js.map → chunk-L5EH2NVO.js.map} +0 -0
  134. /package/dist/{chunk-NS55SM7A.js.map → chunk-O2PGQNYE.js.map} +0 -0
  135. /package/dist/{chunk-C7FQBSON.js.map → chunk-VNFJPTU4.js.map} +0 -0
  136. /package/dist/{chunk-DOGPOY4B.js.map → chunk-WB5LW52Z.js.map} +0 -0
  137. /package/dist/{chunks-SX77P5CV.js.map → chunks-I7TEMJXH.js.map} +0 -0
  138. /package/dist/{formula-5S7WWXGH.js.map → formula-NOIDZEBA.js.map} +0 -0
  139. /package/dist/{formula-JGFTXWYT.js.map → formula-X44GV7TN.js.map} +0 -0
  140. /package/dist/{model-bundle-FSPZZ2EM.js.map → model-bundle-57CLEBCO.js.map} +0 -0
  141. /package/dist/{models-CJPAQK5R.js.map → models-7ELIFTD7.js.map} +0 -0
  142. /package/dist/{pdf-ocr-76SAUXMZ.js.map → pdf-ocr-BNC34MNU.js.map} +0 -0
  143. /package/dist/{pdf-ocr-CPU55CVF.js.map → pdf-ocr-QX2GB33Q.js.map} +0 -0
  144. /package/dist/{profile-io-LBM263K7.js.map → profile-io-V52PL5ZO.js.map} +0 -0
  145. /package/dist/{rasterize-46RG4ZF6.js.map → rasterize-ZIGLJDOG.js.map} +0 -0
  146. /package/dist/{redact-7ILEAUTS.js.map → redact-TIQEQI3M.js.map} +0 -0
  147. /package/dist/{render-NGP5T24S.js.map → render-U5PE6V2E.js.map} +0 -0
  148. /package/dist/{seal-RGUPOLXD.js.map → seal-DNXJBILR.js.map} +0 -0
  149. /package/dist/{setup-AU4OCHNH.js.map → setup-YRDM4ODP.js.map} +0 -0
  150. /package/dist/{visual-NZLYIYEM.js.map → visual-5XWMA4EV.js.map} +0 -0
  151. /package/dist/{watch-MKPUCPXM.js.map → watch-5IOLBATT.js.map} +0 -0
@@ -0,0 +1,949 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ OCR_DET_MODEL,
4
+ OCR_REC_DICT,
5
+ OCR_REC_MODEL,
6
+ ensureOcrModels,
7
+ getOcrModelsDir,
8
+ parseCharacterDict
9
+ } from "./chunk-D6KQKMU3.js";
10
+ import {
11
+ extractPageBlocksWithLines
12
+ } from "./chunk-A275ZQLX.js";
13
+
14
+ // src/ocr/ruling-lines.ts
15
+ var INK_LUMA_MAX = 190;
16
+ var MIN_LINE_LENGTH_PT = 20;
17
+ var MAX_LINE_WIDTH_PT = 2.5;
18
+ var GAP_TOL_PX = 2;
19
+ var BAND_OVERLAP_RATIO = 0.5;
20
+ var SURROUND_INK_RATIO = 0.35;
21
+ function detectRulingLines(rgba, width, height, scale) {
22
+ const minLenPx = Math.round(MIN_LINE_LENGTH_PT * scale);
23
+ const maxThickPx = Math.max(1, Math.floor(MAX_LINE_WIDTH_PT * scale));
24
+ const ink = new Uint8Array(width * height);
25
+ for (let p = 0, i = 0; p < ink.length; p++, i += 4) {
26
+ const luma = rgba[i] * 77 + rgba[i + 1] * 150 + rgba[i + 2] * 29 >> 8;
27
+ if (luma <= INK_LUMA_MAX && rgba[i + 3] >= 128) ink[p] = 1;
28
+ }
29
+ const horizontals = detectBands(ink, width, height, minLenPx, maxThickPx, false);
30
+ const verticals = detectBands(ink, width, height, minLenPx, maxThickPx, true);
31
+ return { horizontals, verticals };
32
+ }
33
+ function detectBands(ink, width, height, minLenPx, maxThickPx, transpose) {
34
+ const lines = transpose ? width : height;
35
+ const span = transpose ? height : width;
36
+ const at = transpose ? (line, pos) => ink[pos * width + line] : (line, pos) => ink[line * width + pos];
37
+ const out = [];
38
+ let open = [];
39
+ const surroundInkRatio = (b, side) => {
40
+ if (side < 0 || side >= lines) return 0;
41
+ let dark = 0, total = 0;
42
+ for (let pos = b.lo; pos <= b.hi; pos += 3) {
43
+ total++;
44
+ if (at(side, pos) === 1) dark++;
45
+ }
46
+ return total > 0 ? dark / total : 0;
47
+ };
48
+ const emit = (b) => {
49
+ const thick = b.last - b.first + 1;
50
+ if (thick > maxThickPx) return;
51
+ if (surroundInkRatio(b, b.first - 2) >= SURROUND_INK_RATIO && surroundInkRatio(b, b.last + 2) >= SURROUND_INK_RATIO) return;
52
+ const center = (b.first + b.last) / 2;
53
+ out.push(
54
+ 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 }
55
+ );
56
+ };
57
+ for (let line = 0; line < lines; line++) {
58
+ const runs = [];
59
+ let runStart = -1;
60
+ let gap = 0;
61
+ for (let pos = 0; pos <= span; pos++) {
62
+ const on = pos < span && at(line, pos) === 1;
63
+ if (on) {
64
+ if (runStart < 0) runStart = pos;
65
+ gap = 0;
66
+ } else if (runStart >= 0) {
67
+ if (++gap > GAP_TOL_PX || pos >= span) {
68
+ const hi = pos - gap;
69
+ if (hi - runStart + 1 >= minLenPx) runs.push({ lo: runStart, hi });
70
+ runStart = -1;
71
+ gap = 0;
72
+ }
73
+ }
74
+ }
75
+ const next = [];
76
+ const used = /* @__PURE__ */ new Set();
77
+ for (const band of open) {
78
+ if (band.last !== line - 1) {
79
+ emit(band);
80
+ continue;
81
+ }
82
+ let merged = false;
83
+ for (let r = 0; r < runs.length; r++) {
84
+ if (used.has(r)) continue;
85
+ const run = runs[r];
86
+ const overlap = Math.min(band.hi, run.hi) - Math.max(band.lo, run.lo) + 1;
87
+ const shorter = Math.min(band.hi - band.lo, run.hi - run.lo) + 1;
88
+ if (overlap >= shorter * BAND_OVERLAP_RATIO) {
89
+ band.lo = Math.min(band.lo, run.lo);
90
+ band.hi = Math.max(band.hi, run.hi);
91
+ band.last = line;
92
+ next.push(band);
93
+ used.add(r);
94
+ merged = true;
95
+ break;
96
+ }
97
+ }
98
+ if (!merged) emit(band);
99
+ }
100
+ for (let r = 0; r < runs.length; r++) {
101
+ if (!used.has(r)) next.push({ lo: runs[r].lo, hi: runs[r].hi, first: line, last: line });
102
+ }
103
+ open = next;
104
+ }
105
+ for (const band of open) emit(band);
106
+ return out;
107
+ }
108
+ function rulingToPdfLines(ruling, scale, pdfHeight) {
109
+ const horizontals = ruling.horizontals.map((s) => {
110
+ const y = pdfHeight - (s.y1 + s.y2) / 2 / scale;
111
+ return {
112
+ x1: Math.min(s.x1, s.x2) / scale,
113
+ y1: y,
114
+ x2: Math.max(s.x1, s.x2) / scale,
115
+ y2: y,
116
+ lineWidth: s.thicknessPx / scale
117
+ };
118
+ });
119
+ const verticals = ruling.verticals.map((s) => {
120
+ const x = (s.x1 + s.x2) / 2 / scale;
121
+ const yA = pdfHeight - Math.max(s.y1, s.y2) / scale;
122
+ const yB = pdfHeight - Math.min(s.y1, s.y2) / scale;
123
+ return { x1: x, y1: yA, x2: x, y2: yB, lineWidth: s.thicknessPx / scale };
124
+ });
125
+ return { horizontals, verticals };
126
+ }
127
+
128
+ // src/ocr/engine.ts
129
+ import { readFile } from "fs/promises";
130
+ import { join } from "path";
131
+
132
+ // src/ocr/line-split.ts
133
+ function grayCrop(rgba, pageW, box) {
134
+ const out = new Uint8Array(box.w * box.h);
135
+ for (let y = 0; y < box.h; y++) {
136
+ let si = ((box.y + y) * pageW + box.x) * 4;
137
+ let di = y * box.w;
138
+ for (let x = 0; x < box.w; x++, si += 4, di++) {
139
+ out[di] = rgba[si] * 77 + rgba[si + 1] * 150 + rgba[si + 2] * 29 >> 8;
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+ function inkStats(gray) {
145
+ const hist = new Uint32Array(256);
146
+ for (let i = 0; i < gray.length; i++) hist[gray[i]]++;
147
+ const total = gray.length;
148
+ let sumAll = 0;
149
+ for (let v = 0; v < 256; v++) sumAll += v * hist[v];
150
+ let wB = 0, sumB = 0, best = -1, thr = 127;
151
+ for (let t = 0; t < 256; t++) {
152
+ wB += hist[t];
153
+ if (wB === 0) continue;
154
+ const wF = total - wB;
155
+ if (wF === 0) break;
156
+ sumB += t * hist[t];
157
+ const mB = sumB / wB;
158
+ const mF = (sumAll - sumB) / wF;
159
+ const between = wB * wF * (mB - mF) * (mB - mF);
160
+ if (between > best) {
161
+ best = between;
162
+ thr = t;
163
+ }
164
+ }
165
+ let nDark = 0, sDark = 0;
166
+ for (let v = 0; v <= thr; v++) {
167
+ nDark += hist[v];
168
+ sDark += v * hist[v];
169
+ }
170
+ const nLight = total - nDark;
171
+ const sLight = sumAll - sDark;
172
+ if (nDark === 0 || nLight === 0) return { threshold: thr, darkInk: true, contrast: 0, inkRatio: 0 };
173
+ const darkInk = nDark <= nLight;
174
+ return {
175
+ threshold: thr,
176
+ darkInk,
177
+ contrast: sLight / nLight - sDark / nDark,
178
+ inkRatio: (darkInk ? nDark : nLight) / total
179
+ };
180
+ }
181
+ function splitRowBands(gray, w, h, ink, minBandRatio) {
182
+ const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
183
+ const colInk = new Uint32Array(w);
184
+ for (let y = 0; y < h; y++) {
185
+ const off = y * w;
186
+ for (let x = 0; x < w; x++) if (isInk(gray[off + x])) colInk[x]++;
187
+ }
188
+ const ruleCol = new Uint8Array(w);
189
+ let liveW = 0;
190
+ for (let x = 0; x < w; x++) {
191
+ if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
192
+ else liveW++;
193
+ }
194
+ const rowInk = new Uint32Array(h);
195
+ for (let y = 0; y < h; y++) {
196
+ let n = 0;
197
+ const off = y * w;
198
+ for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) n++;
199
+ rowInk[y] = n >= liveW * 0.85 ? 0 : n;
200
+ }
201
+ const blank = Math.max(1, Math.floor(w * 0.02));
202
+ let bands = [];
203
+ let start = -1;
204
+ for (let y = 0; y <= h; y++) {
205
+ const on = y < h && rowInk[y] > blank;
206
+ if (on && start < 0) start = y;
207
+ else if (!on && start >= 0) {
208
+ bands.push([start, y]);
209
+ start = -1;
210
+ }
211
+ }
212
+ const inkCols = (y0, y1) => {
213
+ let x0 = w, x1 = -1;
214
+ for (let y = y0; y < y1; y++) {
215
+ const off = y * w;
216
+ for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) {
217
+ if (x < x0) x0 = x;
218
+ if (x > x1) x1 = x;
219
+ }
220
+ }
221
+ return x1 >= x0 ? [x0, x1 + 1] : [0, w];
222
+ };
223
+ if (bands.length <= 1) return [{ y0: bands[0]?.[0] ?? 0, y1: bands[0]?.[1] ?? h, x0: 0, x1: w }];
224
+ const widths = bands.map(([y0, y1]) => {
225
+ const [a, b] = inkCols(y0, y1);
226
+ return b - a;
227
+ }).sort((a, b) => a - b);
228
+ const charSize = widths[widths.length >> 1];
229
+ const minBand = Math.max(3, minBandRatio * charSize);
230
+ for (let i = 0; i + 1 < bands.length; ) {
231
+ const gap = bands[i + 1][0] - bands[i][1];
232
+ if (gap <= Math.max(2, charSize * 0.15) && bands[i + 1][1] - bands[i][0] <= charSize * 1.3) {
233
+ bands.splice(i, 2, [bands[i][0], bands[i + 1][1]]);
234
+ } else i++;
235
+ }
236
+ while (bands.length > 1 && bands[0][0] <= 1 && bands[0][1] - bands[0][0] < minBand) bands.shift();
237
+ while (bands.length > 1 && bands[bands.length - 1][1] >= h - 1 && bands[bands.length - 1][1] - bands[bands.length - 1][0] < minBand) bands.pop();
238
+ for (; ; ) {
239
+ let idx = -1, minH = Infinity;
240
+ for (let i = 0; i < bands.length; i++) {
241
+ const bh = bands[i][1] - bands[i][0];
242
+ if (bh < minBand && bh < minH) {
243
+ minH = bh;
244
+ idx = i;
245
+ }
246
+ }
247
+ if (idx < 0 || bands.length === 1) break;
248
+ const gapPrev = idx > 0 ? bands[idx][0] - bands[idx - 1][1] : Infinity;
249
+ const gapNext = idx < bands.length - 1 ? bands[idx + 1][0] - bands[idx][1] : Infinity;
250
+ const j = gapPrev <= gapNext ? idx - 1 : idx + 1;
251
+ const a = Math.min(idx, j), b = Math.max(idx, j);
252
+ bands = [...bands.slice(0, a), [bands[a][0], bands[b][1]], ...bands.slice(b + 1)];
253
+ }
254
+ if (bands.length === 1) return [{ y0: bands[0][0], y1: bands[0][1], x0: 0, x1: w }];
255
+ return bands.map(([y0, y1]) => {
256
+ const [x0, x1] = inkCols(y0, y1);
257
+ return { y0, y1, x0, x1 };
258
+ });
259
+ }
260
+ function inkBounds(gray, w, h, ink) {
261
+ const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
262
+ const colInk = new Uint32Array(w);
263
+ const rowInk = new Uint32Array(h);
264
+ for (let y = 0; y < h; y++) {
265
+ const off = y * w;
266
+ for (let x = 0; x < w; x++) if (isInk(gray[off + x])) {
267
+ colInk[x]++;
268
+ rowInk[y]++;
269
+ }
270
+ }
271
+ const ruleCol = new Uint8Array(w), ruleRow = new Uint8Array(h);
272
+ for (let x = 0; x < w; x++) if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
273
+ for (let y = 0; y < h; y++) if (rowInk[y] >= w * 0.85) ruleRow[y] = 1;
274
+ let x0 = w, x1 = -1, y0 = h, y1 = -1;
275
+ for (let y = 0; y < h; y++) {
276
+ if (ruleRow[y]) continue;
277
+ const off = y * w;
278
+ for (let x = 0; x < w; x++) {
279
+ if (ruleCol[x] || !isInk(gray[off + x])) continue;
280
+ if (x < x0) x0 = x;
281
+ if (x > x1) x1 = x;
282
+ if (y < y0) y0 = y;
283
+ if (y > y1) y1 = y;
284
+ }
285
+ }
286
+ if (x1 < x0 || y1 < y0) return { x0: 0, y0: 0, x1: w, y1: h };
287
+ return { x0, y0, x1: x1 + 1, y1: y1 + 1 };
288
+ }
289
+
290
+ // src/ocr/postprocess.ts
291
+ var HANGUL_START = /^[\uac00-\ud7a3]/;
292
+ function restoreSymbols(text) {
293
+ let s = text;
294
+ s = s.replace(/^([Oo])(\s?)(?=[\uac00-\ud7a3])/, "\u25CB$2");
295
+ 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);
296
+ s = s.replace(/[\u2206\u0394]/g, "\u25B3");
297
+ return smartQuotes(joinDigitGroups(s));
298
+ }
299
+ function joinDigitGroups(s) {
300
+ return s.replace(/(?<![\d,])\d{1,3}(?:, ?\d{3})+(?![\d])/g, (m) => m.replace(/, /g, ","));
301
+ }
302
+ function smartQuotes(s) {
303
+ if (!/['"]/.test(s)) return s;
304
+ const chars = [...s];
305
+ for (const q of ["'", '"']) {
306
+ const [open, close] = q === "'" ? ["\u2018", "\u2019"] : ["\u201C", "\u201D"];
307
+ const pos = [];
308
+ for (let i = 0; i < chars.length; i++) {
309
+ if (chars[i] !== q) continue;
310
+ const prev2 = i > 0 ? chars[i - 1] : "";
311
+ if (q === "'" && !/[\p{L}\p{N}]/u.test(prev2) && /^\d{2}(?!\d)/.test(chars.slice(i + 1, i + 4).join(""))) {
312
+ chars[i] = close;
313
+ continue;
314
+ }
315
+ pos.push(i);
316
+ }
317
+ if (pos.length === 0) continue;
318
+ const first = pos[0];
319
+ const prev = first > 0 ? chars[first - 1] : "";
320
+ const atOpen = prev === "" || /[\s(\[{<\u300c\u300e\u3010\u3008\u300a\u2018\u201c·,:]/.test(prev);
321
+ let isOpen = pos.length % 2 === 0 || atOpen;
322
+ for (const i of pos) {
323
+ chars[i] = isOpen ? open : close;
324
+ isOpen = !isOpen;
325
+ }
326
+ }
327
+ return chars.join("");
328
+ }
329
+ function isDotFragment(text) {
330
+ return /^[\s.\u00b7\u2024\u2025\u2026\u2027\u2219\u22c5\u318d]+$/.test(text);
331
+ }
332
+ function restoreBulletItems(items) {
333
+ for (const it of items) {
334
+ if (it.text !== "O" && it.text !== "o") continue;
335
+ const cy = it.y + it.h / 2;
336
+ let next = null;
337
+ for (const o of items) {
338
+ if (o === it || o.x < it.x + it.w * 0.5) continue;
339
+ if (Math.abs(o.y + o.h / 2 - cy) > Math.max(it.h, o.h) / 2) continue;
340
+ if (o.x - (it.x + it.w) > it.h * 3) continue;
341
+ if (!next || o.x < next.x) next = o;
342
+ }
343
+ if (next && HANGUL_START.test(next.text.trimStart())) it.text = "\u25CB";
344
+ }
345
+ }
346
+
347
+ // src/ocr/crop.ts
348
+ var REC_HEIGHT = 48;
349
+ var REC_MAX_WIDTH = 3200;
350
+ var MIN_SIZE = 3;
351
+ function bandBoxes(b, bands, pageH) {
352
+ const out = [];
353
+ for (let i = 0; i < bands.length; i++) {
354
+ const { y0, y1, x0, x1 } = bands[i];
355
+ const bh = y1 - y0;
356
+ const upGap = i > 0 ? (y0 - bands[i - 1].y1) / 2 : y0;
357
+ const downGap = i < bands.length - 1 ? (bands[i + 1].y0 - y1) / 2 : b.h - y1;
358
+ const top = Math.max(0, Math.round(b.y + y0 - Math.min(bh * 0.2, upGap)));
359
+ const bot = Math.min(pageH, Math.round(b.y + y1 + Math.min(bh * 0.2, downGap)));
360
+ const left = Math.max(b.x, Math.round(b.x + x0 - bh * 0.2));
361
+ const right = Math.min(b.x + b.w, Math.round(b.x + x1 + bh * 0.2));
362
+ if (bot - top >= MIN_SIZE && right - left >= MIN_SIZE) out.push({ x: left, y: top, w: right - left, h: bot - top });
363
+ }
364
+ return out;
365
+ }
366
+ function lineCrop(rgba, pageW, box, rot) {
367
+ const srcW = rot === 0 ? box.w : box.h;
368
+ const srcH = rot === 0 ? box.h : box.w;
369
+ const rw = Math.min(REC_MAX_WIDTH, Math.max(16, Math.round(srcW * REC_HEIGHT / srcH)));
370
+ const rgb = new Uint8Array(rw * REC_HEIGHT * 3);
371
+ const at = (u, v) => {
372
+ let px, py;
373
+ if (rot === 0) {
374
+ px = u;
375
+ py = v;
376
+ } else if (rot === 90) {
377
+ px = box.w - 1 - v;
378
+ py = u;
379
+ } else {
380
+ px = v;
381
+ py = box.h - 1 - u;
382
+ }
383
+ return ((box.y + py) * pageW + (box.x + px)) * 4;
384
+ };
385
+ const fx = srcW / rw;
386
+ const fy = srcH / REC_HEIGHT;
387
+ for (let dy = 0; dy < REC_HEIGHT; dy++) {
388
+ let sy = (dy + 0.5) * fy - 0.5;
389
+ if (sy < 0) sy = 0;
390
+ const y0 = Math.min(srcH - 1, Math.floor(sy));
391
+ const y1 = Math.min(srcH - 1, y0 + 1);
392
+ const wy = sy - y0;
393
+ for (let dx = 0; dx < rw; dx++) {
394
+ let sx = (dx + 0.5) * fx - 0.5;
395
+ if (sx < 0) sx = 0;
396
+ const x0 = Math.min(srcW - 1, Math.floor(sx));
397
+ const x1 = Math.min(srcW - 1, x0 + 1);
398
+ const wx = sx - x0;
399
+ const i00 = at(x0, y0), i01 = at(x1, y0), i10 = at(x0, y1), i11 = at(x1, y1);
400
+ const o = (dy * rw + dx) * 3;
401
+ for (let c = 0; c < 3; c++) {
402
+ const top = rgba[i00 + c] * (1 - wx) + rgba[i01 + c] * wx;
403
+ const bottom = rgba[i10 + c] * (1 - wx) + rgba[i11 + c] * wx;
404
+ rgb[o + c] = Math.round(top * (1 - wy) + bottom * wy);
405
+ }
406
+ }
407
+ }
408
+ return { rgb, w: rw };
409
+ }
410
+
411
+ // src/ocr/engine.ts
412
+ var DEFAULT_OCR_TUNING = Object.freeze({
413
+ detLongSide: 960,
414
+ detThresh: 0.3,
415
+ detBoxThresh: 0.6,
416
+ detUnclip: 1.5,
417
+ textScore: 0.5,
418
+ recBatch: 1,
419
+ splitTall: true,
420
+ minInkContrast: 35,
421
+ postprocess: true,
422
+ tightBoxes: true
423
+ });
424
+ var DET_MIN_SIZE = 3;
425
+ var DET_MAX_BOXES = 1e3;
426
+ var REC_MIN_WIDTH = 320;
427
+ var REC_BATCH_MAX_PIXELS = 48 * 16e3;
428
+ var TALL_RATIO = 1.5;
429
+ var ROTATE_RATIO = 3;
430
+ var DET_MEAN = [0.485, 0.456, 0.406];
431
+ var DET_STD = [0.229, 0.224, 0.225];
432
+ var OcrEngine = class _OcrEngine {
433
+ det;
434
+ rec;
435
+ dict;
436
+ ort;
437
+ sharp;
438
+ constructor(parts) {
439
+ this.det = parts.det;
440
+ this.rec = parts.rec;
441
+ this.dict = parts.dict;
442
+ this.ort = parts.ort;
443
+ this.sharp = parts.sharp;
444
+ }
445
+ static async create() {
446
+ const [ortMod, sharpModRaw] = await Promise.all([
447
+ tryImport("onnxruntime-node", () => import("onnxruntime-node")),
448
+ tryImport(
449
+ "sharp",
450
+ () => import("sharp")
451
+ )
452
+ ]);
453
+ const sharpAny = sharpModRaw;
454
+ const sharpMod = typeof sharpAny === "function" ? sharpAny : sharpAny.default ?? sharpAny;
455
+ const dir = getOcrModelsDir();
456
+ const sessionOpts = {
457
+ graphOptimizationLevel: "all",
458
+ executionProviders: ["cpu"],
459
+ logSeverityLevel: 3
460
+ // paddle2onnx 변환 잔여물 W 로그 폭주 억제
461
+ };
462
+ const [det, rec, dictYml] = await Promise.all([
463
+ ortMod.InferenceSession.create(join(dir, OCR_DET_MODEL.filename), sessionOpts),
464
+ ortMod.InferenceSession.create(join(dir, OCR_REC_MODEL.filename), sessionOpts),
465
+ readFile(join(dir, OCR_REC_DICT.filename), "utf-8")
466
+ ]);
467
+ const dict = parseCharacterDict(dictYml);
468
+ 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");
469
+ return new _OcrEngine({ det, rec, dict, ort: ortMod, sharp: sharpMod });
470
+ }
471
+ /** onnxruntime-node 1.14+ InferenceSession.release() — 구버전은 무시 */
472
+ async destroy() {
473
+ for (const s of [this.det, this.rec]) {
474
+ const rel = s.release;
475
+ if (typeof rel === "function") {
476
+ try {
477
+ await rel.call(s);
478
+ } catch {
479
+ }
480
+ }
481
+ }
482
+ }
483
+ /**
484
+ * 페이지 RGBA 픽셀 → 텍스트 라인 인식.
485
+ * 반환 좌표는 입력 픽셀 기준. 라인은 위→아래, 좌→우 정렬.
486
+ * @param stats 저신뢰(conf<0.5) 폐기 라인 카운트 출력 — 종전엔 무음 폐기라 관측 불가
487
+ */
488
+ async recognizePage(rgba, width, height, stats, tuning = DEFAULT_OCR_TUNING) {
489
+ if (width < DET_MIN_SIZE || height < DET_MIN_SIZE) return [];
490
+ const boxes = await this.detect(rgba, width, height, tuning);
491
+ const jobs = [];
492
+ let group = 0;
493
+ for (const b of boxes) {
494
+ const gray = grayCrop(rgba, width, b);
495
+ const ink = inkStats(gray);
496
+ if (tuning.minInkContrast > 0 && ink.contrast < tuning.minInkContrast) continue;
497
+ if (tuning.splitTall && b.h >= b.w * TALL_RATIO) {
498
+ const bands = splitRowBands(gray, b.w, b.h, ink, 0.45);
499
+ if (bands.length >= 2) {
500
+ for (const sub of bandBoxes(b, bands, height)) jobs.push({ box: sub, rot: 0, group: group++ });
501
+ continue;
502
+ }
503
+ if (b.h >= b.w * ROTATE_RATIO) {
504
+ for (const rot of [0, 90, 270]) jobs.push({ box: b, rot, group });
505
+ group++;
506
+ continue;
507
+ }
508
+ }
509
+ jobs.push({ box: b, rot: 0, group: group++ });
510
+ }
511
+ const results = await this.recognizeJobs(rgba, width, jobs, tuning.recBatch);
512
+ const best = /* @__PURE__ */ new Map();
513
+ jobs.forEach((job, i) => {
514
+ const r = results[i];
515
+ if (!r) return;
516
+ const cur = best.get(job.group);
517
+ if (!cur || r.confidence > cur.confidence) best.set(job.group, { job, ...r });
518
+ });
519
+ const items = [];
520
+ for (const { job, text: raw, confidence } of best.values()) {
521
+ const text = tuning.postprocess ? restoreSymbols(raw.trim()) : raw;
522
+ if (!text.trim()) continue;
523
+ if (tuning.postprocess && isDotFragment(text)) continue;
524
+ if (confidence < tuning.textScore) {
525
+ if (stats) stats.droppedLowConf++;
526
+ continue;
527
+ }
528
+ let b = job.box;
529
+ if (tuning.tightBoxes) {
530
+ const gray = grayCrop(rgba, width, b);
531
+ const t = inkBounds(gray, b.w, b.h, inkStats(gray));
532
+ b = { x: b.x + t.x0, y: b.y + t.y0, w: t.x1 - t.x0, h: t.y1 - t.y0 };
533
+ }
534
+ items.push({ text, x: b.x, y: b.y, w: b.w, h: b.h, confidence });
535
+ }
536
+ if (tuning.postprocess) restoreBulletItems(items);
537
+ items.sort((a, b) => a.y - b.y || a.x - b.x);
538
+ return items;
539
+ }
540
+ // ─── det ─────────────────────────────────────────────
541
+ async detect(rgba, width, height, tuning) {
542
+ const ratio = tuning.detLongSide / Math.max(width, height);
543
+ const dw = Math.max(32, Math.round(width * ratio / 32) * 32);
544
+ const dh = Math.max(32, Math.round(height * ratio / 32) * 32);
545
+ const rgb = await this.sharp(rgba, { raw: { width, height, channels: 4 } }).resize(dw, dh, { fit: "fill" }).removeAlpha().raw().toBuffer();
546
+ const plane = dw * dh;
547
+ const input = new Float32Array(3 * plane);
548
+ for (let i = 0; i < plane; i++) {
549
+ const r = rgb[i * 3] / 255;
550
+ const g = rgb[i * 3 + 1] / 255;
551
+ const b = rgb[i * 3 + 2] / 255;
552
+ input[i] = (b - DET_MEAN[0]) / DET_STD[0];
553
+ input[plane + i] = (g - DET_MEAN[1]) / DET_STD[1];
554
+ input[2 * plane + i] = (r - DET_MEAN[2]) / DET_STD[2];
555
+ }
556
+ const tensor = new this.ort.Tensor("float32", input, [1, 3, dh, dw]);
557
+ const out = await this.det.run({ [this.det.inputNames[0]]: tensor });
558
+ const probMap = out[this.det.outputNames[0]].data;
559
+ const rawBoxes = componentBoxes(probMap, dw, dh, tuning.detThresh, tuning.detBoxThresh);
560
+ const sx = width / dw;
561
+ const sy = height / dh;
562
+ const boxes = [];
563
+ for (const rb of rawBoxes.slice(0, DET_MAX_BOXES)) {
564
+ const bw = rb.x2 - rb.x1 + 1;
565
+ const bh = rb.y2 - rb.y1 + 1;
566
+ const delta = bw * bh * tuning.detUnclip / (2 * (bw + bh));
567
+ const x1 = Math.max(0, Math.floor((rb.x1 - delta) * sx));
568
+ const y1 = Math.max(0, Math.floor((rb.y1 - delta) * sy));
569
+ const x2 = Math.min(width, Math.ceil((rb.x2 + 1 + delta) * sx));
570
+ const y2 = Math.min(height, Math.ceil((rb.y2 + 1 + delta) * sy));
571
+ if (x2 - x1 < DET_MIN_SIZE || y2 - y1 < DET_MIN_SIZE) continue;
572
+ boxes.push({ x: x1, y: y1, w: x2 - x1, h: y2 - y1 });
573
+ }
574
+ return boxes;
575
+ }
576
+ // ─── rec ─────────────────────────────────────────────
577
+ /** 라인 작업들을 폭 비율 순으로 배치 인식 — 결과는 jobs 순서 */
578
+ async recognizeJobs(rgba, pageW, jobs, batchSize) {
579
+ const crops = jobs.map((j) => lineCrop(rgba, pageW, j.box, j.rot));
580
+ const order = crops.map((_, i) => i).sort((a, b) => crops[a].w - crops[b].w);
581
+ const results = new Array(jobs.length).fill(null);
582
+ const plane = REC_HEIGHT;
583
+ for (let s = 0; s < order.length; ) {
584
+ let e = s + 1;
585
+ 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++;
586
+ const idx = order.slice(s, e);
587
+ const bw = Math.max(REC_MIN_WIDTH, crops[idx[idx.length - 1]].w);
588
+ const n = idx.length;
589
+ const chw = 3 * REC_HEIGHT * bw;
590
+ const input = new Float32Array(n * chw);
591
+ idx.forEach((ci, k) => {
592
+ const c = crops[ci];
593
+ const base = k * chw;
594
+ const p = REC_HEIGHT * bw;
595
+ for (let y = 0; y < REC_HEIGHT; y++) {
596
+ for (let x = 0; x < c.w; x++) {
597
+ const src = (y * c.w + x) * 3;
598
+ const dst = base + y * bw + x;
599
+ input[dst] = c.rgb[src + 2] / 127.5 - 1;
600
+ input[dst + p] = c.rgb[src + 1] / 127.5 - 1;
601
+ input[dst + 2 * p] = c.rgb[src] / 127.5 - 1;
602
+ }
603
+ }
604
+ });
605
+ const tensor = new this.ort.Tensor("float32", input, [n, 3, REC_HEIGHT, bw]);
606
+ const out = await this.rec.run({ [this.rec.inputNames[0]]: tensor });
607
+ const logits = out[this.rec.outputNames[0]];
608
+ const [, T, C] = logits.dims;
609
+ const data = logits.data;
610
+ idx.forEach((ci, k) => {
611
+ results[ci] = ctcDecode(data.subarray(k * T * C, (k + 1) * T * C), T, C, this.dict);
612
+ });
613
+ s = e;
614
+ }
615
+ return results;
616
+ }
617
+ };
618
+ function ctcDecode(data, T, C, dict) {
619
+ let text = "";
620
+ let confSum = 0;
621
+ let confCount = 0;
622
+ let prev = -1;
623
+ for (let t = 0; t < T; t++) {
624
+ const off = t * C;
625
+ let best = 0;
626
+ let bestV = data[off];
627
+ for (let c = 1; c < C; c++) {
628
+ const v = data[off + c];
629
+ if (v > bestV) {
630
+ bestV = v;
631
+ best = c;
632
+ }
633
+ }
634
+ const repeat = best === prev;
635
+ prev = best;
636
+ if (best === 0 || repeat) continue;
637
+ let p = bestV;
638
+ if (p > 1.0001 || p < 0) {
639
+ let denom = 0;
640
+ for (let c = 0; c < C; c++) denom += Math.exp(data[off + c] - bestV);
641
+ p = 1 / denom;
642
+ }
643
+ confSum += p;
644
+ confCount++;
645
+ if (best >= 1 && best <= dict.length) text += dict[best - 1];
646
+ else if (best === dict.length + 1) text += " ";
647
+ }
648
+ if (!text) return null;
649
+ return { text, confidence: confCount > 0 ? confSum / confCount : 0 };
650
+ }
651
+ function componentBoxes(prob, w, h, thresh = DEFAULT_OCR_TUNING.detThresh, boxThresh = DEFAULT_OCR_TUNING.detBoxThresh) {
652
+ const visited = new Uint8Array(w * h);
653
+ const boxes = [];
654
+ const stack = [];
655
+ for (let start = 0; start < w * h; start++) {
656
+ if (visited[start] || prob[start] <= thresh) continue;
657
+ let x1 = start % w, x2 = x1, y1 = start / w | 0, y2 = y1;
658
+ let sum = 0;
659
+ let count = 0;
660
+ stack.length = 0;
661
+ stack.push(start);
662
+ visited[start] = 1;
663
+ while (stack.length) {
664
+ const p = stack.pop();
665
+ const px = p % w;
666
+ const py = p / w | 0;
667
+ sum += prob[p];
668
+ count++;
669
+ if (px < x1) x1 = px;
670
+ if (px > x2) x2 = px;
671
+ if (py < y1) y1 = py;
672
+ if (py > y2) y2 = py;
673
+ if (px > 0 && !visited[p - 1] && prob[p - 1] > thresh) {
674
+ visited[p - 1] = 1;
675
+ stack.push(p - 1);
676
+ }
677
+ if (px < w - 1 && !visited[p + 1] && prob[p + 1] > thresh) {
678
+ visited[p + 1] = 1;
679
+ stack.push(p + 1);
680
+ }
681
+ if (py > 0 && !visited[p - w] && prob[p - w] > thresh) {
682
+ visited[p - w] = 1;
683
+ stack.push(p - w);
684
+ }
685
+ if (py < h - 1 && !visited[p + w] && prob[p + w] > thresh) {
686
+ visited[p + w] = 1;
687
+ stack.push(p + w);
688
+ }
689
+ }
690
+ if (x2 - x1 + 1 < DET_MIN_SIZE && y2 - y1 + 1 < DET_MIN_SIZE) continue;
691
+ boxes.push({ x1, y1, x2, y2, score: sum / count });
692
+ }
693
+ return boxes.filter((b) => b.score >= boxThresh).sort((a, b) => a.y1 - b.y1 || a.x1 - b.x1);
694
+ }
695
+ async function tryImport(name, loader) {
696
+ try {
697
+ return await loader();
698
+ } catch (e) {
699
+ throw new Error(
700
+ `\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}`
701
+ );
702
+ }
703
+ }
704
+ var enginePromise = null;
705
+ function getOcrEngine() {
706
+ if (!enginePromise) {
707
+ enginePromise = OcrEngine.create().catch((err) => {
708
+ enginePromise = null;
709
+ throw err;
710
+ });
711
+ }
712
+ return enginePromise;
713
+ }
714
+
715
+ // src/ocr/deskew.ts
716
+ var MAX_SKEW_DEG = 5;
717
+ var MIN_APPLY_DEG = 0.25;
718
+ var MIN_GAIN = 1.03;
719
+ var TARGET_LONG = 1e3;
720
+ function estimateSkew(rgba, width, height) {
721
+ const step = Math.max(1, Math.ceil(Math.max(width, height) / TARGET_LONG));
722
+ const sw = Math.floor(width / step), sh = Math.floor(height / step);
723
+ const xs = [], ys = [];
724
+ for (let y = 0; y < sh; y++) {
725
+ for (let x = 0; x < sw; x++) {
726
+ const i = (y * step * width + x * step) * 4;
727
+ const l = rgba[i] * 77 + rgba[i + 1] * 150 + rgba[i + 2] * 29 >> 8;
728
+ if (l < 128) {
729
+ xs.push(x);
730
+ ys.push(y);
731
+ }
732
+ }
733
+ }
734
+ if (xs.length < 200) return { angle: 0, gain: 1 };
735
+ const diag = Math.ceil(Math.hypot(sw, sh));
736
+ const hist = new Float64Array(2 * diag + 2);
737
+ const score = (deg) => {
738
+ const t = deg * Math.PI / 180;
739
+ const c = Math.cos(t), s = Math.sin(t);
740
+ hist.fill(0);
741
+ for (let k = 0; k < xs.length; k++) hist[Math.round(ys[k] * c + xs[k] * s) + diag]++;
742
+ let sum = 0;
743
+ for (let i = 0; i < hist.length; i++) sum += hist[i] * hist[i];
744
+ return sum;
745
+ };
746
+ const s0 = score(0);
747
+ let best = 0, bestS = s0;
748
+ for (let d = -MAX_SKEW_DEG; d <= MAX_SKEW_DEG + 1e-9; d += 0.2) {
749
+ const v = score(d);
750
+ if (v > bestS) {
751
+ bestS = v;
752
+ best = d;
753
+ }
754
+ }
755
+ const coarse = best;
756
+ for (let d = coarse - 0.2; d <= coarse + 0.2 + 1e-9; d += 0.02) {
757
+ const v = score(d);
758
+ if (v > bestS) {
759
+ bestS = v;
760
+ best = d;
761
+ }
762
+ }
763
+ return { angle: +(-best).toFixed(2), gain: s0 > 0 ? bestS / s0 : 1 };
764
+ }
765
+ function deskewPage(rgba, width, height) {
766
+ const est = estimateSkew(rgba, width, height);
767
+ if (Math.abs(est.angle) < MIN_APPLY_DEG || est.gain < MIN_GAIN) return { rgba, angle: 0 };
768
+ return { rgba: rotateRgba(rgba, width, height, est.angle), angle: est.angle };
769
+ }
770
+ function rotateRgba(rgba, width, height, deg) {
771
+ const out = new Uint8Array(rgba.length);
772
+ const t = deg * Math.PI / 180;
773
+ const c = Math.cos(t), s = Math.sin(t);
774
+ const cx = (width - 1) / 2, cy = (height - 1) / 2;
775
+ for (let y = 0; y < height; y++) {
776
+ const dy = y - cy;
777
+ for (let x = 0; x < width; x++) {
778
+ const dx = x - cx;
779
+ const sx = c * dx - s * dy + cx;
780
+ const sy = s * dx + c * dy + cy;
781
+ const o = (y * width + x) * 4;
782
+ const x0 = Math.floor(sx), y0 = Math.floor(sy);
783
+ if (x0 < 0 || y0 < 0 || x0 >= width - 1 || y0 >= height - 1) {
784
+ out[o] = 255;
785
+ out[o + 1] = 255;
786
+ out[o + 2] = 255;
787
+ out[o + 3] = 255;
788
+ continue;
789
+ }
790
+ const wx = sx - x0, wy = sy - y0;
791
+ const i00 = (y0 * width + x0) * 4, i01 = i00 + 4, i10 = i00 + width * 4, i11 = i10 + 4;
792
+ for (let ch = 0; ch < 4; ch++) {
793
+ const top = rgba[i00 + ch] * (1 - wx) + rgba[i01 + ch] * wx;
794
+ const bot = rgba[i10 + ch] * (1 - wx) + rgba[i11 + ch] * wx;
795
+ out[o + ch] = Math.round(top * (1 - wy) + bot * wy);
796
+ }
797
+ }
798
+ }
799
+ return out;
800
+ }
801
+
802
+ // src/ocr/pdf-ocr.ts
803
+ var OCR_RENDER_SCALE = 3;
804
+ var PAGE_TIMEOUT_MS = 12e4;
805
+ async function runPdfOcr(buffer, targets, mode, warnings, onProgress, detectTables = true) {
806
+ const result = /* @__PURE__ */ new Map();
807
+ if (targets.size === 0) return result;
808
+ const pdfiumMod = await tryImport2(
809
+ "@hyzyla/pdfium",
810
+ () => import("@hyzyla/pdfium")
811
+ );
812
+ if (mode === "builtin") {
813
+ await ensureOcrModels((p) => {
814
+ if (p.phase === "download" && p.downloaded === 0) {
815
+ process.stderr.write(`[kordoc-ocr] ${p.spec.name} \uB2E4\uC6B4\uB85C\uB4DC \uC911 (~${p.spec.sizeMb}MB)...
816
+ `);
817
+ }
818
+ });
819
+ }
820
+ const engine = mode === "builtin" ? await getOcrEngine() : null;
821
+ const pdfium = await pdfiumMod.PDFiumLibrary.init();
822
+ const doc = await pdfium.loadDocument(new Uint8Array(buffer));
823
+ try {
824
+ let done = 0;
825
+ for (const page of doc.pages()) {
826
+ const pageNo = page.number + 1;
827
+ if (!targets.has(pageNo)) continue;
828
+ onProgress?.(++done, targets.size);
829
+ try {
830
+ const blocks = await withTimeout(
831
+ ocrOnePage(page, pageNo, mode, engine, warnings, detectTables),
832
+ PAGE_TIMEOUT_MS,
833
+ `OCR \uD398\uC774\uC9C0 ${pageNo} \uD0C0\uC784\uC544\uC6C3 (${PAGE_TIMEOUT_MS / 1e3}\uCD08)`
834
+ );
835
+ result.set(pageNo, blocks);
836
+ } catch (e) {
837
+ warnings.push({
838
+ page: pageNo,
839
+ message: `\uD398\uC774\uC9C0 ${pageNo} OCR \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`,
840
+ code: "OCR_FAILED"
841
+ });
842
+ }
843
+ }
844
+ } finally {
845
+ doc.destroy();
846
+ pdfium.destroy();
847
+ }
848
+ return result;
849
+ }
850
+ async function ocrOnePage(page, pageNo, mode, engine, warnings, detectTables) {
851
+ const { originalWidth: pdfW, originalHeight: pdfH } = page.getOriginalSize();
852
+ const rendered = await page.render({
853
+ scale: OCR_RENDER_SCALE,
854
+ render: async ({ data }) => data
855
+ });
856
+ const { data: bgra, width: rw, height: rh } = rendered;
857
+ const rgba = bgraToRgba(bgra);
858
+ if (mode === "builtin") {
859
+ const { rgba: upright } = deskewPage(rgba, rw, rh);
860
+ const stats = { droppedLowConf: 0 };
861
+ const items = await engine.recognizePage(upright, rw, rh, stats);
862
+ if (stats.droppedLowConf > 0) {
863
+ warnings.push({
864
+ page: pageNo,
865
+ message: `\uD398\uC774\uC9C0 ${pageNo}: \uC800\uC2E0\uB8B0 OCR \uB77C\uC778 ${stats.droppedLowConf}\uAC1C \uD3D0\uAE30 (\uC778\uC2DD \uACB0\uC190 \uAC00\uB2A5)`,
866
+ code: "OCR_LOW_CONF"
867
+ });
868
+ }
869
+ const scale = rh / pdfH;
870
+ const ruling = detectRulingLines(upright, rw, rh, scale);
871
+ const extraLines = rulingToPdfLines(ruling, scale, pdfH);
872
+ return ocrItemsToBlocks(items, pageNo, pdfW, pdfH, scale, extraLines, detectTables);
873
+ }
874
+ const sharpModRaw = await tryImport2(
875
+ "sharp",
876
+ () => import("sharp")
877
+ );
878
+ const sharpAny = sharpModRaw;
879
+ const sharp = typeof sharpAny === "function" ? sharpAny : sharpAny.default ?? sharpAny;
880
+ const png = await sharp(rgba, { raw: { width: rw, height: rh, channels: 4 } }).png().toBuffer();
881
+ const text = await mode(new Uint8Array(png), pageNo, "image/png");
882
+ if (!text.trim()) {
883
+ warnings.push({ page: pageNo, message: `\uD398\uC774\uC9C0 ${pageNo} OCR \uACB0\uACFC \uC5C6\uC74C`, code: "OCR_FAILED" });
884
+ return [];
885
+ }
886
+ return [{ type: "paragraph", text: text.trim(), pageNumber: pageNo }];
887
+ }
888
+ function ocrItemsToBlocks(items, pageNumber, pdfW, pdfH, scale, extraLines, detectTables = true) {
889
+ const norm = items.map((it) => {
890
+ const h = it.h / scale;
891
+ return {
892
+ text: it.text,
893
+ x: Math.round(it.x / scale),
894
+ // NormItem.y 는 pdfjs transform[5] = 베이스라인 (bottom-up) — 잉크 하단으로 근사
895
+ y: Math.round(pdfH - (it.y + it.h) / scale),
896
+ w: Math.round(it.w / scale),
897
+ h: Math.round(h),
898
+ // 박스는 잉크 외곽(engine tightBoxes) — 한글 줄 잉크 높이 ≈ 0.9em
899
+ fontSize: Math.max(1, Math.round(h / 0.9)),
900
+ fontName: "ocr",
901
+ isHidden: false
902
+ };
903
+ });
904
+ return extractPageBlocksWithLines(norm, pageNumber, { fnArray: [], argsArray: [] }, pdfW, pdfH, extraLines, detectTables);
905
+ }
906
+ async function tryImport2(name, loader) {
907
+ try {
908
+ return await loader();
909
+ } catch (e) {
910
+ throw new Error(
911
+ `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}`
912
+ );
913
+ }
914
+ }
915
+ async function withTimeout(promise, ms, msg) {
916
+ promise.catch(() => {
917
+ });
918
+ let timer;
919
+ try {
920
+ return await Promise.race([
921
+ promise,
922
+ new Promise((_, reject) => {
923
+ timer = setTimeout(() => reject(new Error(msg)), ms);
924
+ })
925
+ ]);
926
+ } finally {
927
+ if (timer) clearTimeout(timer);
928
+ }
929
+ }
930
+ function bgraToRgba(bgra) {
931
+ const out = new Uint8Array(bgra.length);
932
+ for (let i = 0; i < bgra.length; i += 4) {
933
+ out[i] = bgra[i + 2];
934
+ out[i + 1] = bgra[i + 1];
935
+ out[i + 2] = bgra[i];
936
+ out[i + 3] = bgra[i + 3];
937
+ }
938
+ return out;
939
+ }
940
+
941
+ export {
942
+ getOcrEngine,
943
+ detectRulingLines,
944
+ rulingToPdfLines,
945
+ deskewPage,
946
+ runPdfOcr,
947
+ ocrItemsToBlocks
948
+ };
949
+ //# sourceMappingURL=chunk-VJHAUMCF.js.map