kordoc 4.16.0 → 4.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +344 -1135
  2. package/dist/{-VQNLLLPE.js → -4G6MA4X6.js} +11 -11
  3. package/dist/chunk-3IB7LBHC.js +57 -0
  4. package/dist/chunk-3IB7LBHC.js.map +1 -0
  5. package/dist/{chunk-6LMKQM7I.js → chunk-4VODJKFL.js} +3 -3
  6. package/dist/{chunk-UDX7TSOC.js → chunk-5KS5PWJR.js} +2 -2
  7. package/dist/chunk-62W7QCEX.cjs +57 -0
  8. package/dist/chunk-62W7QCEX.cjs.map +1 -0
  9. package/dist/{chunk-NNYF2AGC.js → chunk-7KNGSQU7.js} +4 -4
  10. package/dist/chunk-7KNGSQU7.js.map +1 -0
  11. package/dist/{chunk-DYENR3PX.js → chunk-AUWGDP4G.js} +2 -2
  12. package/dist/{chunk-H6KMTYYZ.cjs → chunk-CEIATRDW.cjs} +3 -3
  13. package/dist/{chunk-H6KMTYYZ.cjs.map → chunk-CEIATRDW.cjs.map} +1 -1
  14. package/dist/{chunk-N6TPODYL.js → chunk-DES2LGFZ.js} +2 -2
  15. package/dist/{chunk-PQRP4NSF.js → chunk-F4NRLLY4.js} +2 -2
  16. package/dist/{chunk-B5NEGP65.cjs → chunk-FKUDAIJI.cjs} +692 -33
  17. package/dist/chunk-FKUDAIJI.cjs.map +1 -0
  18. package/dist/{chunk-XWA625AN.js → chunk-FSDJLAOW.js} +3 -1
  19. package/dist/chunk-FSDJLAOW.js.map +1 -0
  20. package/dist/{chunk-PIMB2YNO.js → chunk-GO5IFOUF.js} +2 -2
  21. package/dist/{chunk-476OYHGZ.js → chunk-IQGIT2VX.js} +37 -7
  22. package/dist/chunk-IQGIT2VX.js.map +1 -0
  23. package/dist/chunk-J3IF6KGW.js +59 -0
  24. package/dist/chunk-J3IF6KGW.js.map +1 -0
  25. package/dist/{chunk-C75WE52C.js → chunk-JELRWAZC.js} +2 -2
  26. package/dist/{chunk-46DZF3RK.js → chunk-JFLAM3MM.js} +2 -2
  27. package/dist/{chunk-A3QDRMVD.js → chunk-KIOQ4N37.js} +2 -2
  28. package/dist/{chunk-TGD7UFHD.js → chunk-LEIAEWZ7.js} +4 -4
  29. package/dist/chunk-LEIAEWZ7.js.map +1 -0
  30. package/dist/{chunk-GOQBON77.js → chunk-MLIS253L.js} +2 -2
  31. package/dist/{chunk-JB5X5NNL.js → chunk-OXLPVQYK.js} +680 -21
  32. package/dist/chunk-OXLPVQYK.js.map +1 -0
  33. package/dist/{chunk-RC4C7Q33.js → chunk-P6ZDXTV6.js} +2 -2
  34. package/dist/{chunk-CB5XDCSL.js → chunk-QOCS7W7R.js} +5034 -4569
  35. package/dist/chunk-QOCS7W7R.js.map +1 -0
  36. package/dist/{chunk-3JWMDL3S.js → chunk-S2PL227Q.js} +2 -2
  37. package/dist/{chunk-HIVK5V27.js → chunk-SFEQYKYJ.js} +133 -110
  38. package/dist/chunk-SFEQYKYJ.js.map +1 -0
  39. package/dist/{chunk-NWODPYBD.cjs → chunk-STM3ORY7.cjs} +2 -2
  40. package/dist/{chunk-NWODPYBD.cjs.map → chunk-STM3ORY7.cjs.map} +1 -1
  41. package/dist/{chunk-43FKGDDO.js → chunk-TXUKTOG2.js} +213 -30
  42. package/dist/chunk-TXUKTOG2.js.map +1 -0
  43. package/dist/{chunk-XHOZB2WP.js → chunk-UTP2WCHS.js} +681 -22
  44. package/dist/chunk-UTP2WCHS.js.map +1 -0
  45. package/dist/{chunk-MH6MAL64.cjs → chunk-WDQTFAXY.cjs} +217 -114
  46. package/dist/chunk-WDQTFAXY.cjs.map +1 -0
  47. package/dist/{chunk-DRY3L5KQ.cjs → chunk-XTFN5WKS.cjs} +7 -7
  48. package/dist/chunk-XTFN5WKS.cjs.map +1 -0
  49. package/dist/chunks-NUNIKS2J.js +10 -0
  50. package/dist/cli.js +33 -30
  51. package/dist/cli.js.map +1 -1
  52. package/dist/{formula-FNQYHMIY.js → formula-EPKLPDU7.js} +3 -3
  53. package/dist/{formula-5775PTP2.js → formula-NARUOXRX.js} +4 -4
  54. package/dist/{formula-VEFKYXPB.cjs → formula-WIMV5TUX.cjs} +9 -9
  55. package/dist/{formula-VEFKYXPB.cjs.map → formula-WIMV5TUX.cjs.map} +1 -1
  56. package/dist/{image-ocr-PYZU4TO7.js → image-ocr-3WJ4QH3T.js} +8 -8
  57. package/dist/{image-ocr-YA2MQBRP.js → image-ocr-5BUJQ4TK.js} +6 -6
  58. package/dist/{image-ocr-6JZR734G.cjs → image-ocr-AXWXR6JV.cjs} +15 -15
  59. package/dist/{image-ocr-6JZR734G.cjs.map → image-ocr-AXWXR6JV.cjs.map} +1 -1
  60. package/dist/index.cjs +1900 -1406
  61. package/dist/index.cjs.map +1 -1
  62. package/dist/index.d.cts +24 -6
  63. package/dist/index.d.ts +24 -6
  64. package/dist/index.js +1282 -788
  65. package/dist/index.js.map +1 -1
  66. package/dist/mcp.js +34 -28
  67. package/dist/mcp.js.map +1 -1
  68. package/dist/{model-bundle-O6J7ORVC.js → model-bundle-IMBW7D2Z.js} +5 -5
  69. package/dist/{models-YMOKZGCI.js → models-SY75IBOE.js} +5 -5
  70. package/dist/{parser-YAQV7FBO.cjs → parser-2LU4EVU3.cjs} +360 -219
  71. package/dist/parser-2LU4EVU3.cjs.map +1 -0
  72. package/dist/{parser-3B6I2XL7.js → parser-N6476XAC.js} +225 -84
  73. package/dist/parser-N6476XAC.js.map +1 -0
  74. package/dist/{parser-PXATL5DM.js → parser-ZQM3664Z.js} +222 -81
  75. package/dist/parser-ZQM3664Z.js.map +1 -0
  76. package/dist/pdf-ocr-6DEE23IP.js +15 -0
  77. package/dist/pdf-ocr-GHEJVPX2.cjs +16 -0
  78. package/dist/{pdf-ocr-T2JXQATA.cjs.map → pdf-ocr-GHEJVPX2.cjs.map} +1 -1
  79. package/dist/pdf-ocr-MQTWODVU.js +18 -0
  80. package/dist/{profile-io-ZTV2MKRT.js → profile-io-GJPKCYPT.js} +3 -3
  81. package/dist/{rasterize-MMUZ537K.js → rasterize-UOALJQDS.js} +3 -3
  82. package/dist/{redact-doc-S42XGV5D.js → redact-doc-22SYW3KH.js} +11 -11
  83. package/dist/{render-WCWPZKJ7.js → render-MJ4H3R23.js} +7 -7
  84. package/dist/seal-XWMRUTI4.js +10 -0
  85. package/dist/{setup-D35LKZCH.js → setup-2URYVUEO.js} +3 -3
  86. package/dist/visual-USPS4QRC.js +20 -0
  87. package/dist/{watch-AX64CIMN.js → watch-ALXZGTOB.js} +12 -12
  88. package/package.json +1 -1
  89. package/dist/chunk-43FKGDDO.js.map +0 -1
  90. package/dist/chunk-476OYHGZ.js.map +0 -1
  91. package/dist/chunk-B5NEGP65.cjs.map +0 -1
  92. package/dist/chunk-CB5XDCSL.js.map +0 -1
  93. package/dist/chunk-DRY3L5KQ.cjs.map +0 -1
  94. package/dist/chunk-HIVK5V27.js.map +0 -1
  95. package/dist/chunk-JB5X5NNL.js.map +0 -1
  96. package/dist/chunk-MH6MAL64.cjs.map +0 -1
  97. package/dist/chunk-NNYF2AGC.js.map +0 -1
  98. package/dist/chunk-QJERJZJ6.js +0 -11
  99. package/dist/chunk-QJERJZJ6.js.map +0 -1
  100. package/dist/chunk-TGD7UFHD.js.map +0 -1
  101. package/dist/chunk-UQ6TZDW6.js +0 -13
  102. package/dist/chunk-UQ6TZDW6.js.map +0 -1
  103. package/dist/chunk-XHOZB2WP.js.map +0 -1
  104. package/dist/chunk-XWA625AN.js.map +0 -1
  105. package/dist/chunk-ZY5K3LPM.cjs +0 -11
  106. package/dist/chunk-ZY5K3LPM.cjs.map +0 -1
  107. package/dist/chunks-JD25E5PM.js +0 -10
  108. package/dist/parser-3B6I2XL7.js.map +0 -1
  109. package/dist/parser-PXATL5DM.js.map +0 -1
  110. package/dist/parser-YAQV7FBO.cjs.map +0 -1
  111. package/dist/pdf-ocr-2TTO5PX2.js +0 -15
  112. package/dist/pdf-ocr-T2JXQATA.cjs +0 -16
  113. package/dist/pdf-ocr-WF2NQA6X.js +0 -18
  114. package/dist/seal-WHK4GOHR.js +0 -10
  115. package/dist/visual-WL4TDE5B.js +0 -20
  116. /package/dist/{-VQNLLLPE.js.map → -4G6MA4X6.js.map} +0 -0
  117. /package/dist/{chunk-6LMKQM7I.js.map → chunk-4VODJKFL.js.map} +0 -0
  118. /package/dist/{chunk-UDX7TSOC.js.map → chunk-5KS5PWJR.js.map} +0 -0
  119. /package/dist/{chunk-DYENR3PX.js.map → chunk-AUWGDP4G.js.map} +0 -0
  120. /package/dist/{chunk-N6TPODYL.js.map → chunk-DES2LGFZ.js.map} +0 -0
  121. /package/dist/{chunk-PQRP4NSF.js.map → chunk-F4NRLLY4.js.map} +0 -0
  122. /package/dist/{chunk-PIMB2YNO.js.map → chunk-GO5IFOUF.js.map} +0 -0
  123. /package/dist/{chunk-C75WE52C.js.map → chunk-JELRWAZC.js.map} +0 -0
  124. /package/dist/{chunk-46DZF3RK.js.map → chunk-JFLAM3MM.js.map} +0 -0
  125. /package/dist/{chunk-A3QDRMVD.js.map → chunk-KIOQ4N37.js.map} +0 -0
  126. /package/dist/{chunk-GOQBON77.js.map → chunk-MLIS253L.js.map} +0 -0
  127. /package/dist/{chunk-RC4C7Q33.js.map → chunk-P6ZDXTV6.js.map} +0 -0
  128. /package/dist/{chunk-3JWMDL3S.js.map → chunk-S2PL227Q.js.map} +0 -0
  129. /package/dist/{chunks-JD25E5PM.js.map → chunks-NUNIKS2J.js.map} +0 -0
  130. /package/dist/{formula-FNQYHMIY.js.map → formula-EPKLPDU7.js.map} +0 -0
  131. /package/dist/{formula-5775PTP2.js.map → formula-NARUOXRX.js.map} +0 -0
  132. /package/dist/{image-ocr-PYZU4TO7.js.map → image-ocr-3WJ4QH3T.js.map} +0 -0
  133. /package/dist/{image-ocr-YA2MQBRP.js.map → image-ocr-5BUJQ4TK.js.map} +0 -0
  134. /package/dist/{model-bundle-O6J7ORVC.js.map → model-bundle-IMBW7D2Z.js.map} +0 -0
  135. /package/dist/{models-YMOKZGCI.js.map → models-SY75IBOE.js.map} +0 -0
  136. /package/dist/{pdf-ocr-2TTO5PX2.js.map → pdf-ocr-6DEE23IP.js.map} +0 -0
  137. /package/dist/{pdf-ocr-WF2NQA6X.js.map → pdf-ocr-MQTWODVU.js.map} +0 -0
  138. /package/dist/{profile-io-ZTV2MKRT.js.map → profile-io-GJPKCYPT.js.map} +0 -0
  139. /package/dist/{rasterize-MMUZ537K.js.map → rasterize-UOALJQDS.js.map} +0 -0
  140. /package/dist/{redact-doc-S42XGV5D.js.map → redact-doc-22SYW3KH.js.map} +0 -0
  141. /package/dist/{render-WCWPZKJ7.js.map → render-MJ4H3R23.js.map} +0 -0
  142. /package/dist/{seal-WHK4GOHR.js.map → seal-XWMRUTI4.js.map} +0 -0
  143. /package/dist/{setup-D35LKZCH.js.map → setup-2URYVUEO.js.map} +0 -0
  144. /package/dist/{visual-WL4TDE5B.js.map → visual-USPS4QRC.js.map} +0 -0
  145. /package/dist/{watch-AX64CIMN.js.map → watch-ALXZGTOB.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  extractPageBlocksWithLines
4
- } from "./chunk-43FKGDDO.js";
4
+ } from "./chunk-SFEQYKYJ.js";
5
5
  import {
6
6
  OCR_DET_MODEL,
7
7
  OCR_REC_DICT,
@@ -9,10 +9,10 @@ import {
9
9
  ensureOcrModels,
10
10
  getOcrModelsDir,
11
11
  parseCharacterDict
12
- } from "./chunk-N6TPODYL.js";
12
+ } from "./chunk-DES2LGFZ.js";
13
13
  import {
14
14
  OPTIONAL_DEP_INSTALL_HINT
15
- } from "./chunk-RC4C7Q33.js";
15
+ } from "./chunk-P6ZDXTV6.js";
16
16
 
17
17
  // src/ocr/ruling-lines.ts
18
18
  var INK_LUMA_MAX = 205;
@@ -188,19 +188,19 @@ function solidRuns(mask, base, step, span, minLenPx) {
188
188
  function dottedRuns(mask, base, step, span, minLenPx, maxGap, maxDash, maxPeriod) {
189
189
  const runs = [];
190
190
  const chain = [];
191
- const median = (xs) => xs.sort((a, b) => a - b)[xs.length >> 1];
191
+ const median2 = (xs) => xs.sort((a, b) => a - b)[xs.length >> 1];
192
192
  const flush = () => {
193
193
  const cnt = chain.length >> 1;
194
194
  if (cnt >= DOT_MIN_COUNT && chain[chain.length - 1] - chain[0] + 1 >= minLenPx) {
195
- const periods = [], dashes = [], gaps = [];
195
+ const periods = [], dashes = [], gaps2 = [];
196
196
  for (let k = 0; k < cnt; k++) {
197
197
  dashes.push(chain[2 * k + 1] - chain[2 * k] + 1);
198
198
  if (k) {
199
199
  periods.push(chain[2 * k] - chain[2 * k - 2]);
200
- gaps.push(chain[2 * k] - chain[2 * k - 1] - 1);
200
+ gaps2.push(chain[2 * k] - chain[2 * k - 1] - 1);
201
201
  }
202
202
  }
203
- if (median(periods) <= maxPeriod || median(dashes) >= 1.5 * median(gaps)) runs.push({ lo: chain[0], hi: chain[chain.length - 1] });
203
+ if (median2(periods) <= maxPeriod || median2(dashes) >= 1.5 * median2(gaps2)) runs.push({ lo: chain[0], hi: chain[chain.length - 1] });
204
204
  }
205
205
  chain.length = 0;
206
206
  };
@@ -573,12 +573,614 @@ function inkBounds(gray, w, h, ink) {
573
573
  if (x1 < x0 || y1 < y0) return { x0: 0, y0: 0, x1: w, y1: h };
574
574
  return { x0, y0, x1: x1 + 1, y1: y1 + 1 };
575
575
  }
576
+ function bracketFeatures(gray, w, h, ink, cx) {
577
+ const { comps, label } = components(gray, w, h, ink);
578
+ let charH = 0;
579
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
580
+ if (charH < 8) return null;
581
+ const tol = Math.max(2, charH * 0.3);
582
+ let best = null, bestD = Infinity;
583
+ for (const c of comps) {
584
+ if (c.y1 - c.y0 < charH * 0.3 || c.x1 < cx - tol || c.x0 > cx + tol) continue;
585
+ const d = Math.abs((c.x0 + c.x1) / 2 - cx);
586
+ if (d < bestD) {
587
+ bestD = d;
588
+ best = c;
589
+ }
590
+ }
591
+ if (!best) return null;
592
+ const b = best;
593
+ const cw = b.x1 - b.x0, ch = b.y1 - b.y0;
594
+ if (cw < 3 || ch < 8) return null;
595
+ const spans = [];
596
+ for (let y = b.y0; y < b.y1; y++) {
597
+ let n = 0;
598
+ for (let x = b.x0; x < b.x1; x++) if (label[y * w + x] === b.id) n++;
599
+ spans.push(n);
600
+ }
601
+ const band = Math.max(1, Math.round(ch * 0.15));
602
+ const top = Math.max(...spans.slice(0, band)) / cw, bottom = Math.max(...spans.slice(ch - band)) / cw;
603
+ const midRows = spans.slice(Math.floor(ch * 0.3), Math.ceil(ch * 0.7)).sort((p, q) => p - q);
604
+ const stroke = midRows[midRows.length >> 1] ?? 0;
605
+ const taper = spans.filter((n) => n > stroke && n < cw).length / ch;
606
+ const fill = spans.reduce((a, n) => a + n, 0) / (cw * ch);
607
+ return { top, bottom, taper, fill, cw, ch, charH };
608
+ }
609
+ function romanStems(gray, w, h, ink, prevX, nextX) {
610
+ const { comps, label } = components(gray, w, h, ink);
611
+ let charH = 0;
612
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
613
+ if (charH < 10) return null;
614
+ const x0 = prevX + charH * 0.35, x1 = nextX - charH * 0.35;
615
+ const sel = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6 && (c.x0 + c.x1) / 2 >= x0 && (c.x0 + c.x1) / 2 < x1);
616
+ if (!sel.length) return null;
617
+ const ids = new Set(sel.map((c) => c.id));
618
+ const ux0 = Math.min(...sel.map((c) => c.x0)), ux1 = Math.max(...sel.map((c) => c.x1));
619
+ const uy0 = Math.min(...sel.map((c) => c.y0)), uy1 = Math.max(...sel.map((c) => c.y1));
620
+ const uh = uy1 - uy0;
621
+ const counts = /* @__PURE__ */ new Map();
622
+ let rows = 0;
623
+ for (let y = uy0 + Math.floor(uh * 0.25); y < uy1 - Math.floor(uh * 0.25); y++) {
624
+ let runs = 0, len = 0, thick = false;
625
+ for (let x = ux0; x <= ux1; x++) {
626
+ const on = x < ux1 && ids.has(label[y * w + x]);
627
+ if (on) len++;
628
+ else if (len) {
629
+ runs++;
630
+ if (len > uh * 0.35) thick = true;
631
+ len = 0;
632
+ }
633
+ }
634
+ rows++;
635
+ if (!thick) counts.set(runs, (counts.get(runs) ?? 0) + 1);
636
+ }
637
+ const [n, k] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [0, 0];
638
+ return n >= 1 && n <= 3 && k >= rows * 0.7 ? n : null;
639
+ }
640
+ function circledAt(gray, w, h, ink, xs) {
641
+ const { comps } = components(gray, w, h, ink);
642
+ let charH = 0;
643
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
644
+ const rings = charH < 10 ? [] : comps.filter((r) => {
645
+ const rw = r.x1 - r.x0, rh = r.y1 - r.y0;
646
+ return rh >= charH * 0.55 && rw >= rh * 0.8 && rw <= rh * 1.25 && comps.some((d) => d !== r && d.x0 > r.x0 && d.x1 < r.x1 && d.y0 > r.y0 && d.y1 < r.y1 && d.y1 - d.y0 >= rh * 0.35);
647
+ });
648
+ return xs.map((x) => rings.some((r) => x >= r.x0 && x <= r.x1));
649
+ }
650
+ function quoteHead(gray, w, h, ink, cx) {
651
+ const { comps, label } = components(gray, w, h, ink);
652
+ let charH = 0;
653
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
654
+ if (charH < 10) return null;
655
+ const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
656
+ if (!tall.length) return null;
657
+ const mid = (Math.min(...tall.map((c) => c.y0)) + Math.max(...tall.map((c) => c.y1))) / 2;
658
+ let best = null, bestD = charH * 0.4;
659
+ for (const c of comps) {
660
+ const ch = c.y1 - c.y0, d = Math.abs((c.x0 + c.x1) / 2 - cx);
661
+ if (ch < charH * 0.12 || ch > charH * 0.5 || (c.y0 + c.y1) / 2 > mid || d > bestD) continue;
662
+ bestD = d;
663
+ best = c;
664
+ }
665
+ if (!best) return null;
666
+ let n = 0, sy = 0;
667
+ for (let y = best.y0; y < best.y1; y++) for (let x = best.x0; x < best.x1; x++) if (label[y * w + x] === best.id) {
668
+ n++;
669
+ sy += y;
670
+ }
671
+ return n ? (sy / n - best.y0) / (best.y1 - best.y0) : null;
672
+ }
673
+ function edgeTrim(gray, w, h, ink) {
674
+ const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
675
+ const colInk = new Uint32Array(w);
676
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (isInk(gray[y * w + x])) colInk[x]++;
677
+ const ruleCol = new Uint8Array(w);
678
+ let liveW = 0;
679
+ for (let x = 0; x < w; x++) {
680
+ if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
681
+ else liveW++;
682
+ }
683
+ const blank = Math.max(1, Math.floor(w * 0.02));
684
+ const bands = [];
685
+ let start = -1;
686
+ for (let y = 0; y <= h; y++) {
687
+ let n = 0;
688
+ if (y < h) {
689
+ for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[y * w + x])) n++;
690
+ }
691
+ const on = y < h && n > blank && n < liveW * 0.85;
692
+ if (on && start < 0) start = y;
693
+ else if (!on && start >= 0) {
694
+ bands.push([start, y]);
695
+ start = -1;
696
+ }
697
+ }
698
+ let y0 = 0, y1 = h;
699
+ if (bands.length >= 2 && h >= 24) {
700
+ const mh = Math.max(...bands.map(([a, b]) => b - a));
701
+ const [top, next] = [bands[0], bands[1]], [bot, prev] = [bands[bands.length - 1], bands[bands.length - 2]];
702
+ if (top[0] <= 1 && top[1] - top[0] < mh * 0.45) y0 = top[1] + next[0] >> 1;
703
+ if (bot[1] >= h - 1 && bot[1] - bot[0] < mh * 0.45) y1 = bot[0] + prev[1] + 1 >> 1;
704
+ if (y0 > 0 || y1 < h) {
705
+ let runs = 0, len = 0;
706
+ for (let x = 0; x <= w; x++) {
707
+ let on = false;
708
+ if (x < w && !ruleCol[x]) for (let y = y0; y < y1 && !on; y++) on = isInk(gray[y * w + x]);
709
+ if (on) len++;
710
+ else {
711
+ if (len >= (y1 - y0) * 0.3) runs++;
712
+ len = 0;
713
+ }
714
+ }
715
+ if (runs < 2) {
716
+ y0 = 0;
717
+ y1 = h;
718
+ }
719
+ }
720
+ }
721
+ const textCol = (x) => {
722
+ for (let y = y0; y < y1; y++) if (isInk(gray[y * w + x])) return true;
723
+ return false;
724
+ };
725
+ const border = (x) => colInk[x] >= h * 0.95;
726
+ const gap = Math.max(2, Math.round(h * 0.1));
727
+ const clearFrom = (x, dir) => {
728
+ for (let k = 0; k < gap; k++) {
729
+ const c = x + dir * k;
730
+ if (c < 0 || c >= w || textCol(c)) return false;
731
+ }
732
+ return true;
733
+ };
734
+ let x0 = 0, x1 = w;
735
+ for (let x = 0; x < w * 0.2; x++) {
736
+ if (border(x)) {
737
+ let e = x;
738
+ while (e + 1 < w && border(e + 1)) e++;
739
+ if (clearFrom(e + 1, 1)) x0 = e + 1;
740
+ break;
741
+ }
742
+ if (textCol(x)) break;
743
+ }
744
+ for (let x = w - 1; x >= w * 0.8; x--) {
745
+ if (border(x)) {
746
+ let e = x;
747
+ while (e - 1 >= 0 && border(e - 1)) e--;
748
+ if (clearFrom(e - 1, -1)) x1 = e;
749
+ break;
750
+ }
751
+ if (textCol(x)) break;
752
+ }
753
+ return y0 > 0 || y1 < h || x0 > 0 || x1 < w ? { x0, x1, y0, y1 } : null;
754
+ }
755
+ function tallInkCount(gray, w, h, ink) {
756
+ const { comps } = components(gray, w, h, ink);
757
+ let charH = 0;
758
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
759
+ const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.5).sort((a, b) => a.x0 - b.x0);
760
+ let n = 0, end = -1;
761
+ for (const c of tall) {
762
+ if (c.x0 >= end) n++;
763
+ end = Math.max(end, c.x1);
764
+ }
765
+ return n;
766
+ }
767
+ function gapGlyphs(gray, w, h, ink) {
768
+ const { comps, label } = components(gray, w, h, ink);
769
+ let charH = 0;
770
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
771
+ if (charH < 10) return { glyphs: [], bandH: 0 };
772
+ const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
773
+ const bandT = Math.min(...tall.map((c) => c.y0)), bandB = Math.max(...tall.map((c) => c.y1)), bh = bandB - bandT;
774
+ const inBand = (c) => c.y1 > bandT && c.y0 < bandB;
775
+ const out = [];
776
+ for (const c of comps) {
777
+ if (!inBand(c)) continue;
778
+ const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
779
+ if (ch >= bh * 0.9 && cw <= bh * 0.15) continue;
780
+ const others = comps.filter((o) => o !== c && inBand(o) && !(o.x0 >= c.x0 && o.x1 <= c.x1 && o.y0 >= c.y0 && o.y1 <= c.y1));
781
+ const apart = !others.some((o) => o.x0 < c.x1 + 1 && o.x1 > c.x0 - 1);
782
+ const nearlyApart = !others.some((o) => Math.min(o.x1, c.x1) - Math.max(o.x0, c.x0) > 2);
783
+ const spans = [], runs = [];
784
+ let n = 0;
785
+ for (let y = c.y0; y < c.y1; y++) {
786
+ let s = 0, r = 0, prev = false;
787
+ for (let x = c.x0; x < c.x1; x++) {
788
+ const on = label[y * w + x] === c.id;
789
+ if (on) {
790
+ s++;
791
+ if (!prev) r++;
792
+ }
793
+ prev = on;
794
+ }
795
+ spans.push(s);
796
+ runs.push(r);
797
+ n += s;
798
+ }
799
+ const fill = n / (cw * ch), cy = ((c.y0 + c.y1) / 2 - bandT) / bh;
800
+ const band = Math.max(1, Math.round(ch * 0.15));
801
+ const top = Math.max(...spans.slice(0, band)) / cw, bottom = Math.max(...spans.slice(ch - band)) / cw;
802
+ let mark = null;
803
+ const speck = Math.max(3, bh * 0.08);
804
+ const dot = ch <= bh * 0.25 && cw <= bh * 0.25 && ch >= speck && cw >= speck && cw >= ch * 0.6 && cw <= ch * 1.6;
805
+ const small = ch <= bh * 0.25 && cw <= bh * 0.25 && ch >= 2 && cw >= 2 && cw >= ch * 0.6 && cw <= ch * 1.6 && fill >= 0.6 && cy >= 0.3 && cy <= 0.7;
806
+ if (!apart && !(nearlyApart && ch >= bh * 0.6)) {
807
+ if (small) out.push({ x0: c.x0, x1: c.x1, mark: "\xB7" });
808
+ continue;
809
+ }
810
+ if (!apart) {
811
+ } else if (dot && fill >= 0.6 && cy >= 0.3 && cy <= 0.7) mark = "\xB7";
812
+ else if (small) mark = "\xB7";
813
+ else if (dot && fill >= 0.6 && cy >= 0.75 && cw >= ch * 0.8) mark = ".";
814
+ else if (ch <= bh * 0.4 && ch >= speck && cw >= ch * 0.75 && cw <= ch * 1.3 && fill < 0.6 && runs[ch >> 1] === 2 && cy >= 0.6) mark = "\u3002";
815
+ else if (ch <= bh * 0.15 && cw >= ch * 2 && cw <= bh * 0.8 && fill >= 0.7 && cy >= 0.35 && cy <= 0.75) mark = "-";
816
+ else if (ch >= bh * 0.5 && cw >= ch * 0.9 && cw <= ch * 1.4 && fill >= 0.4 && runs.every((r) => r <= 1) && top <= 0.4 && bottom >= 0.8) mark = "\u25B2";
817
+ else if (ch >= bh * 0.3 && ch <= bh * 0.65 && fill < 0.5) {
818
+ const stem = (from, to) => {
819
+ let sx = 0, k = 0;
820
+ for (let y = from; y < to; y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
821
+ sx += x - c.x0;
822
+ k++;
823
+ }
824
+ return k ? sx / k / cw : 0.5;
825
+ };
826
+ if (c.y0 <= bandT + bh * 0.15 && top >= 0.7 && bottom <= 0.5 && stem(c.y0 + band, c.y1) <= 0.35) mark = "\u300C";
827
+ else if (c.y1 >= bandB - bh * 0.15 && bottom >= 0.7 && top <= 0.5 && stem(c.y0, c.y1 - band) >= 0.65) mark = "\u300D";
828
+ } else if (ch >= charH * 0.6 && cw >= ch * 0.8 && cw <= ch * 1.25 && fill < 0.5 && top >= 0.8 && bottom >= 0.8 && runs[ch >> 1] === 2) mark = ch <= charH * 0.92 ? "\u25A1" : "\u25A1\0";
829
+ if (!mark && ch >= bh * 0.6 && cw >= ch * 0.25 && cw <= ch * 0.7 && fill <= 0.4 && runs.every((r) => r <= 1)) {
830
+ const mx = (y) => {
831
+ let sx = 0, k = 0;
832
+ for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
833
+ sx += x;
834
+ k++;
835
+ }
836
+ return k ? sx / k : NaN;
837
+ };
838
+ if (mx(c.y0 + band) - mx(c.y1 - 1 - band) >= cw * 0.5) mark = "/";
839
+ }
840
+ if (mark) out.push({ x0: c.x0, x1: c.x1, mark });
841
+ }
842
+ return { glyphs: out, bandH: bh };
843
+ }
844
+ function ringBullet(gray, w, h, ink, cx) {
845
+ const { comps, label } = components(gray, w, h, ink);
846
+ let charH = 0;
847
+ for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
848
+ if (charH < 10) return null;
849
+ const tall = comps.filter((c2) => c2.y1 - c2.y0 >= charH * 0.6);
850
+ const bandT = Math.min(...tall.map((c2) => c2.y0)), bh = Math.max(...tall.map((c2) => c2.y1)) - bandT;
851
+ let best = null, bestD = bh * 0.6;
852
+ for (const c2 of comps) {
853
+ const d = Math.abs((c2.x0 + c2.x1) / 2 - cx);
854
+ if (c2.y1 - c2.y0 >= bh * 0.3 && d < bestD) {
855
+ bestD = d;
856
+ best = c2;
857
+ }
858
+ }
859
+ if (!best) return null;
860
+ const c = best, cw = c.x1 - c.x0, ch = c.y1 - c.y0;
861
+ if (cw < ch * 0.85 || cw > ch * 1.2) return null;
862
+ const runs = (xs, at) => {
863
+ let n2 = 0, len = 0, mx2 = 0, mn = Infinity;
864
+ for (const k of [...xs, -1]) {
865
+ if (k >= 0 && at(k)) {
866
+ if (!len) n2++;
867
+ len++;
868
+ mx2 = Math.max(mx2, len);
869
+ } else {
870
+ if (len) mn = Math.min(mn, len);
871
+ len = 0;
872
+ }
873
+ }
874
+ return { n: n2, mx: mx2, mn };
875
+ };
876
+ const my = c.y0 + c.y1 >> 1, mx = c.x0 + c.x1 >> 1;
877
+ const row = runs(Array.from({ length: cw }, (_, k) => c.x0 + k), (x) => label[my * w + x] === c.id);
878
+ const col = runs(Array.from({ length: ch }, (_, k) => c.y0 + k), (y) => label[y * w + mx] === c.id);
879
+ if (row.n !== 2 || col.n !== 2) return null;
880
+ let n = 0;
881
+ for (let y = c.y0; y < c.y1; y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) n++;
882
+ const fill = n / (cw * ch), rel = ch / bh;
883
+ if (rel >= 0.62 && (fill <= 0.23 ? row.mx <= cw * 0.1 : fill <= 0.33 && row.mn <= cw * 0.08)) return "\u25CB";
884
+ if (rel <= 0.58 && fill >= 0.24) return "\u3147";
885
+ if (rel >= 0.6 && rel <= 0.8 && fill >= 0.3 && ((c.y0 + c.y1) / 2 - bandT) / bh >= 0.55) return "o";
886
+ return null;
887
+ }
888
+ function discAt(gray, w, h, ink, cx) {
889
+ const { comps, label } = components(gray, w, h, ink);
890
+ let charH = 0;
891
+ for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
892
+ if (charH < 10) return false;
893
+ const tall = comps.filter((c2) => c2.y1 - c2.y0 >= charH * 0.6);
894
+ const bh = Math.max(...tall.map((c2) => c2.y1)) - Math.min(...tall.map((c2) => c2.y0));
895
+ const c = comps.filter((o) => Math.abs((o.x0 + o.x1) / 2 - cx) <= bh * 0.4).sort((a, b) => Math.abs((a.x0 + a.x1) / 2 - cx) - Math.abs((b.x0 + b.x1) / 2 - cx))[0];
896
+ if (!c) return false;
897
+ const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
898
+ if (ch < bh * 0.45 || ch > bh * 1.05 || cw < ch * 0.8 || cw > ch * 1.25) return false;
899
+ let n = 0;
900
+ for (let y = c.y0; y < c.y1; y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) n++;
901
+ const fill = n / (cw * ch);
902
+ return fill >= 0.65 && fill <= 0.9;
903
+ }
904
+ function serifOne(gray, w, h, ink, cx) {
905
+ const { comps, label } = components(gray, w, h, ink);
906
+ let charH = 0;
907
+ for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
908
+ if (charH < 10) return false;
909
+ const c = comps.filter((o) => o.x0 - 3 <= cx && o.x1 + 3 >= cx && o.y1 - o.y0 >= charH * 0.6).sort((a, b) => b.y1 - b.y0 - (a.y1 - a.y0))[0];
910
+ if (!c) return false;
911
+ const ch = c.y1 - c.y0;
912
+ const span = (a, b) => {
913
+ let mn = Infinity, mx = -1;
914
+ for (let y = c.y0 + Math.floor(ch * a); y < c.y0 + Math.ceil(ch * b); y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
915
+ mn = Math.min(mn, x);
916
+ mx = Math.max(mx, x);
917
+ }
918
+ return [mn, mx];
919
+ };
920
+ const [m0, m1] = span(0.4, 0.6), [t0, t1] = span(0, 0.2), [b0, b1] = span(0.8, 1);
921
+ const sw = m1 - m0 + 1;
922
+ const even = (l, r) => l >= sw * 0.4 && r >= sw * 0.4 && Math.abs(l - r) <= Math.max(l, r) * 0.35;
923
+ return sw >= 2 && sw <= ch * 0.3 && even(m0 - t0, t1 - m1) && even(m0 - b0, b1 - m1);
924
+ }
925
+ function starRun(gray, w, h, ink, cx) {
926
+ const { comps, label } = components(gray, w, h, ink);
927
+ let charH = 0;
928
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
929
+ if (charH < 10) return 0;
930
+ const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
931
+ const bandT = Math.min(...tall.map((c) => c.y0)), bh = Math.max(...tall.map((c) => c.y1)) - bandT;
932
+ const star = (c) => {
933
+ const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
934
+ if (ch < bh * 0.2 || ch > bh * 0.5 || cw < ch * 0.7 || cw > ch * 1.4 || ((c.y0 + c.y1) / 2 - bandT) / bh > 0.55) return false;
935
+ let n = 0;
936
+ for (let y = c.y0; y < c.y1; y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) n++;
937
+ const fill = n / (cw * ch);
938
+ return fill >= 0.25 && fill <= 0.65;
939
+ };
940
+ const stars2 = comps.filter(star).sort((a, b) => a.x0 - b.x0);
941
+ const k = stars2.findIndex((c) => cx >= c.x0 - (c.x1 - c.x0) && cx <= c.x1 + (c.x1 - c.x0));
942
+ if (k < 0) return 0;
943
+ let lo = k, hi = k;
944
+ while (lo > 0 && stars2[lo].x0 - stars2[lo - 1].x1 <= stars2[lo].x1 - stars2[lo].x0) lo--;
945
+ while (hi + 1 < stars2.length && stars2[hi + 1].x0 - stars2[hi].x1 <= stars2[hi].x1 - stars2[hi].x0) hi++;
946
+ return hi - lo + 1;
947
+ }
948
+ var BRACKET_TAPER = 0.16;
949
+ var BRACKET_SOLID = 0.4;
950
+ var BRACKET_WIDE = 0.265;
951
+ var BRACKET_LENS = 0.35;
952
+ function bracketShape(f, close) {
953
+ const [near, far] = close ? [f.bottom, f.top] : [f.top, f.bottom];
954
+ const paren = close ? ")" : "(";
955
+ if (near >= 0.7 && far <= 0.5) return f.taper < BRACKET_TAPER ? close ? "\u300D" : "\u300C" : paren;
956
+ if (near >= 0.7 && far >= 0.7 && f.taper >= BRACKET_TAPER) {
957
+ return f.fill >= BRACKET_SOLID && (f.cw >= f.ch * BRACKET_WIDE || f.taper >= BRACKET_LENS) ? close ? "\u3011" : "\u3010" : paren;
958
+ }
959
+ return null;
960
+ }
961
+ function leadingBullet(gray, w, h, ink, firstX) {
962
+ const { comps, label } = components(gray, w, h, ink);
963
+ let charH = 0;
964
+ for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
965
+ if (charH < 10) return null;
966
+ const cand = comps.filter((c) => c.y1 - c.y0 >= charH * 0.15).sort((a, b) => a.x0 - b.x0);
967
+ const first = cand[0];
968
+ if (!first) return null;
969
+ const cl = cand.filter((c) => c.x0 < first.x1 && c.x1 > first.x0);
970
+ const x0 = Math.min(...cl.map((c) => c.x0)), x1 = Math.max(...cl.map((c) => c.x1)), y0 = Math.min(...cl.map((c) => c.y0)), y1 = Math.max(...cl.map((c) => c.y1));
971
+ const cw = x1 - x0, ch = y1 - y0;
972
+ if (cw < ch * 0.8 || cw > ch * 1.25) return null;
973
+ const next = cand.find((c) => c.x0 >= x1);
974
+ if (next && next.x0 - x1 < ch * 0.3) return null;
975
+ let inkN = 0;
976
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) if (cl.some((c) => label[y * w + x] === c.id)) inkN++;
977
+ const fill = inkN / (cw * ch);
978
+ const rel = ch / charH;
979
+ const ring2 = (c) => {
980
+ const a = c.x1 - c.x0, b = c.y1 - c.y0;
981
+ return a >= b * 0.75 && a <= b * 1.33;
982
+ };
983
+ const nested = cl.length >= 2 && cl.some((a) => cl.some((b) => a !== b && b.x0 > a.x0 && b.x1 < a.x1 && b.y0 > a.y0 && b.y1 < a.y1 && ring2(b) && Math.abs(b.x0 + b.x1 - (a.x0 + a.x1)) <= (a.x1 - a.x0) * 0.2 && Math.abs(b.y0 + b.y1 - (a.y0 + a.y1)) <= (a.y1 - a.y0) * 0.2));
984
+ const span = (y) => {
985
+ let n = 0;
986
+ for (let x = x0; x < x1; x++) if (cl.some((c) => label[y * w + x] === c.id)) n++;
987
+ return n;
988
+ };
989
+ const band = Math.max(1, Math.round(ch * 0.15));
990
+ const edgeFull = (from, dir) => {
991
+ let m = 0;
992
+ for (let k = 0; k < band; k++) m = Math.max(m, span(from + dir * k));
993
+ return m >= cw * 0.8;
994
+ };
995
+ let midRuns = 0;
996
+ {
997
+ let prev = false;
998
+ const my = y0 + (ch >> 1);
999
+ for (let x = x0; x < x1; x++) {
1000
+ const on = cl.some((c) => label[my * w + x] === c.id);
1001
+ if (on && !prev) midRuns++;
1002
+ prev = on;
1003
+ }
1004
+ }
1005
+ const hollowSquare = cl.length === 1 && fill < 0.5 && midRuns === 2 && edgeFull(y0, 1) && edgeFull(y1 - 1, -1);
1006
+ let mark = null;
1007
+ if (hollowSquare && rel >= 0.6 && rel <= 0.92) mark = "\u25A1";
1008
+ else if (nested && fill < 0.45 && rel >= 0.6) mark = "\u25CE";
1009
+ else if (cl.length === 1 && fill >= 0.9 && rel >= 0.28 && rel <= 0.6) mark = "\u25AA";
1010
+ else if (cl.length === 1 && fill >= 0.65 && fill <= 0.88 && rel >= 0.5 && rel <= 0.8) mark = "\u25CF";
1011
+ if (!mark) return null;
1012
+ return { mark, covers: firstX <= x1 + ch * 0.3 };
1013
+ }
1014
+
1015
+ // src/ocr/glyph-restore.ts
1016
+ function restoreGlyphs(rgba, pageW, box, text, steps, stepPx, note = {}) {
1017
+ const chars = [...text];
1018
+ if (chars.length !== steps.length) return text;
1019
+ const gray = grayCrop(rgba, pageW, box);
1020
+ const c = { chars, cx: (i) => (steps[i] + 0.5) * stepPx, gray, w: box.w, h: box.h, ink: inkStats(gray) };
1021
+ if (/[[\]]/.test(text)) brackets(c);
1022
+ if (/['"]/.test(text)) quotes(c);
1023
+ if (/(^|\D)[1-9](\D|$)/.test(text)) circled(c);
1024
+ if (text.includes("*")) stars(c);
1025
+ if (/[\u00b7\u2022]/.test(text)) discs(c);
1026
+ gaps(c);
1027
+ note.ringLead = ring(c);
1028
+ if (/[I1Ⅰ-Ⅲ]|^\s*\./.test(text)) roman(c);
1029
+ return chars.join("");
1030
+ }
1031
+ function brackets({ chars, cx, gray, w, h, ink }) {
1032
+ for (let i = 0; i < chars.length; i++) {
1033
+ if (chars[i] !== "[" && chars[i] !== "]") continue;
1034
+ const f = bracketFeatures(gray, w, h, ink, cx(i));
1035
+ const shape = f && bracketShape(f, chars[i] === "]");
1036
+ if (shape) chars[i] = shape;
1037
+ }
1038
+ }
1039
+ function quotes({ chars, cx, gray, w, h, ink }) {
1040
+ const year = (i) => chars[i] === "'" && !/[\p{L}\p{N}]/u.test(chars[i - 1] ?? "") && /^\d{2}(?!\d)/.test(chars.slice(i + 1, i + 4).join(""));
1041
+ const single = { "'": chars.filter((c, i) => c === "'" && !year(i)).length === 1, '"': chars.filter((c) => c === '"').length === 1 };
1042
+ const open = { "'": false, '"': false };
1043
+ for (let i = 0; i < chars.length; i++) {
1044
+ const c = chars[i];
1045
+ const kind = c === "'" || c === "\u2018" || c === "\u2019" ? "'" : c === '"' || c === "\u201C" || c === "\u201D" ? '"' : null;
1046
+ if (!kind) continue;
1047
+ if (c !== kind) {
1048
+ open[kind] = c === "\u2018" || c === "\u201C";
1049
+ continue;
1050
+ }
1051
+ if (open[kind]) {
1052
+ open[kind] = false;
1053
+ continue;
1054
+ }
1055
+ if (!year(i) && !single[kind]) continue;
1056
+ const r = quoteHead(gray, w, h, ink, cx(i));
1057
+ if (r !== null && r >= 0.47) {
1058
+ chars[i] = kind === "'" ? "\u2018" : "\u201C";
1059
+ open[kind] = true;
1060
+ }
1061
+ }
1062
+ }
1063
+ function circled({ chars, cx, gray, w, h, ink }) {
1064
+ const at = chars.flatMap((c, i) => /^[1-9]$/.test(c) && !/\d/.test(chars[i - 1] ?? "") && !/\d/.test(chars[i + 1] ?? "") ? [i] : []);
1065
+ if (!at.length) return;
1066
+ const hit = circledAt(gray, w, h, ink, at.map(cx));
1067
+ at.forEach((i, k) => {
1068
+ if (hit[k]) chars[i] = String.fromCharCode(9311 + Number(chars[i]));
1069
+ });
1070
+ }
1071
+ function stars({ chars, cx, gray, w, h, ink }) {
1072
+ for (let i = 0; i < chars.length; i++) {
1073
+ if (chars[i] !== "*" || chars[i - 1] === "*") continue;
1074
+ let e = i;
1075
+ while (chars[e + 1] === "*") e++;
1076
+ const n = starRun(gray, w, h, ink, cx(i));
1077
+ if (n > e - i + 1 && n <= 4) chars[e] += "*".repeat(n - (e - i + 1));
1078
+ i = e;
1079
+ }
1080
+ }
1081
+ function discs({ chars, cx, gray, w, h, ink }) {
1082
+ for (let i = 0; i < chars.length; i++) if (/^[\u00b7\u2022]$/.test(chars[i]) && discAt(gray, w, h, ink, cx(i))) chars[i] = "\u25CF";
1083
+ }
1084
+ function gaps({ chars, cx, gray, w, h, ink }) {
1085
+ const { glyphs, bandH } = gapGlyphs(gray, w, h, ink);
1086
+ if (!glyphs.length) return;
1087
+ const live = chars.map((c, i) => i).filter((i) => chars[i].trim());
1088
+ if (!live.length) return;
1089
+ const d = [];
1090
+ for (let k = 0; k + 1 < live.length; k++) if (/[가-힣]$/.test(chars[live[k]]) && /^[가-힣]/.test(chars[live[k + 1]])) d.push(cx(live[k + 1]) - cx(live[k]));
1091
+ const pitch = d.length >= 2 ? d.sort((a, b) => a - b)[d.length >> 1] : bandH;
1092
+ const nearTo = (g) => (i) => Math.abs(cx(i) - (g.x0 + g.x1) / 2) <= Math.max(pitch * 0.45, (g.x1 - g.x0) / 2 + 1);
1093
+ const loose = glyphs.filter((g) => /^[\u00b7.]/.test(g.mark) && !live.some(nearTo(g)));
1094
+ for (const g of glyphs.sort((a, b) => a.x0 - b.x0)) {
1095
+ const mid = (g.x0 + g.x1) / 2;
1096
+ const near = nearTo(g);
1097
+ const cover = live.filter(near);
1098
+ if (g.mark === "\xB7") {
1099
+ const sp = chars.findIndex((c, i) => c === " " && near(i));
1100
+ const p = sp >= 0 ? chars.slice(0, sp).reverse().find((c) => c.trim()) : void 0, q = sp >= 0 ? chars.slice(sp + 1).find((c) => c.trim()) : void 0;
1101
+ if (p && q && /[가-힣]$/.test(p) && /^[가-힣]/.test(q) && !loose.some((o) => o !== g && Math.abs((o.x0 + o.x1) / 2 - mid) <= pitch * 1.5)) chars[sp] = "\xB7";
1102
+ continue;
1103
+ }
1104
+ if (cover.length) {
1105
+ if (g.mark.startsWith("\u25A1") && cover.length === 1 && /^[0Oo\u25cb]$/.test(chars[cover[0]])) chars[cover[0]] = "\u25A1";
1106
+ continue;
1107
+ }
1108
+ if (g.mark === "\u25A1\0") continue;
1109
+ const before = live.filter((i) => cx(i) < mid), after = live.filter((i) => cx(i) > mid);
1110
+ if (!before.length) {
1111
+ if (after.length && /^[\u3002\u00b7\u300c\u25b2]$/.test(g.mark) && /^[가-힣]/.test(chars[after[0]])) chars[after[0]] = g.mark + chars[after[0]];
1112
+ else if (g.mark === "-" && /^[\dⅰ-ⅹivx]+\s*-$/.test(chars.join("").trim())) chars[after[0]] = "-" + chars[after[0]];
1113
+ continue;
1114
+ }
1115
+ if (!after.length && g.mark !== ".") continue;
1116
+ if (g.mark === "/" && !(/\d$/.test(chars[before[before.length - 1]]) && /^\d/.test(chars[after[0]]))) continue;
1117
+ const prev = chars[before[before.length - 1]], next = after.length ? chars[after[0]] : "";
1118
+ if (g.mark === "\xB7" || g.mark === ".") {
1119
+ if (loose.some((o) => o !== g && Math.abs((o.x0 + o.x1) / 2 - mid) <= pitch * 1.5)) continue;
1120
+ if (/[\u2026.\u00b7]$/.test(prev) || /^[\u2026.\u00b7]/.test(next)) continue;
1121
+ }
1122
+ if (g.mark === "\xB7") {
1123
+ if (/^\d/.test(next)) continue;
1124
+ const sp = chars.findIndex((c, i) => c === " " && near(i));
1125
+ if (sp >= 0) {
1126
+ chars[sp] = g.mark;
1127
+ continue;
1128
+ }
1129
+ }
1130
+ chars[before[before.length - 1]] += g.mark;
1131
+ }
1132
+ }
1133
+ function ring({ chars, cx, gray, w, h, ink }) {
1134
+ const k = chars.findIndex((c) => c.trim());
1135
+ if (k < 0 || !/^[\u3147Oo0\u25cb]$/.test(chars[k])) return false;
1136
+ const rest = chars.slice(k + 1).join("").trimStart();
1137
+ if (!/^[(\[「【<]?[가-힣]/.test(rest)) return false;
1138
+ const r = ringBullet(gray, w, h, ink, cx(k));
1139
+ if (r) chars[k] = r;
1140
+ return r !== null;
1141
+ }
1142
+ function roman({ chars, cx, gray, w, h, ink }) {
1143
+ const ROMAN = /^[IⅠ-Ⅲ]$/, LATIN = /[A-Za-z]$/;
1144
+ for (let i = 0; i < chars.length; i++) {
1145
+ if (chars[i] !== "1" || /[\dA-Za-z]$/.test(chars[i - 1] ?? "") || /^[\dA-Za-z,]/.test(chars[i + 1] ?? "")) continue;
1146
+ if (serifOne(gray, w, h, ink, cx(i))) chars[i] = "\u2160";
1147
+ }
1148
+ const inRoman = (i) => ROMAN.test(chars[i]) || chars[i] === "1" && (ROMAN.test(chars[i - 1] ?? "") || ROMAN.test(chars[i + 1] ?? ""));
1149
+ const k = chars.findIndex((c) => c.trim());
1150
+ if (chars[k] === "." && chars.slice(k + 1).some((c) => /[가-힣A-Za-z0-9]/.test(c))) {
1151
+ const n = romanStems(gray, w, h, ink, -Infinity, cx(k));
1152
+ if (n) chars[k] = String.fromCharCode(8543 + n) + chars[k];
1153
+ }
1154
+ for (let s = 0; s < chars.length; ) {
1155
+ if (!inRoman(s) || s > 0 && LATIN.test(chars[s - 1])) {
1156
+ s++;
1157
+ continue;
1158
+ }
1159
+ let e = s;
1160
+ while (e < chars.length && inRoman(e)) e++;
1161
+ const tok = chars.slice(s, e).join("");
1162
+ if (e < chars.length && /^[A-Za-z]/.test(chars[e])) {
1163
+ s = e;
1164
+ continue;
1165
+ }
1166
+ let p = s - 1, q = e;
1167
+ while (p >= 0 && !chars[p].trim()) p--;
1168
+ while (q < chars.length && !chars[q].trim()) q++;
1169
+ const n = romanStems(gray, w, h, ink, p >= 0 ? cx(p) : -Infinity, q < chars.length ? cx(q) : Infinity);
1170
+ const canon = n ? [["\u2160", "I"], ["\u2161", "II"], ["\u2162", "III"]][n - 1] : null;
1171
+ if (canon && !canon.includes(tok)) {
1172
+ chars[s] = canon[0];
1173
+ for (let i = s + 1; i < e; i++) chars[i] = "";
1174
+ }
1175
+ s = e;
1176
+ }
1177
+ }
576
1178
 
577
1179
  // src/ocr/postprocess.ts
578
1180
  var HANGUL_START = /^[\uac00-\ud7a3]/;
579
- function restoreSymbols(text) {
1181
+ function restoreSymbols(text, ringDecided = false) {
580
1182
  let s = text;
581
- s = s.replace(/^([Oo])(\s?)(?=[\uac00-\ud7a3])/, "\u25CB$2");
1183
+ if (!ringDecided) s = s.replace(/^([Oo])(\s?)(?=[\uac00-\ud7a3])/, "\u25CB$2");
582
1184
  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);
583
1185
  s = s.replace(/[\u2206\u0394]/g, "\u25B3");
584
1186
  s = s.replace(/\(cid:\d*\)/g, "");
@@ -698,7 +1300,7 @@ function bandBoxes(b, bands, pageH) {
698
1300
  }
699
1301
  return out;
700
1302
  }
701
- function lineCrop(rgba, pageW, box, rot) {
1303
+ function lineCrop(rgba, pageW, box, rot, keep) {
702
1304
  const srcW = rot === 0 ? box.w : box.h;
703
1305
  const srcH = rot === 0 ? box.h : box.w;
704
1306
  const rw = Math.min(REC_MAX_WIDTH, Math.max(16, Math.round(srcW * REC_HEIGHT / srcH)));
@@ -733,6 +1335,10 @@ function lineCrop(rgba, pageW, box, rot) {
733
1335
  const wx = sx - x0;
734
1336
  const i00 = at(x0, y0), i01 = at(x1, y0), i10 = at(x0, y1), i11 = at(x1, y1);
735
1337
  const o = (dy * rw + dx) * 3;
1338
+ if (keep && rot === 0 && (sx < keep.x0 || sx >= keep.x1 || sy < keep.y0 || sy >= keep.y1)) {
1339
+ rgb[o] = rgb[o + 1] = rgb[o + 2] = keep.bg;
1340
+ continue;
1341
+ }
736
1342
  for (let c = 0; c < 3; c++) {
737
1343
  const top = rgba[i00 + c] * (1 - wx) + rgba[i01 + c] * wx;
738
1344
  const bottom = rgba[i10 + c] * (1 - wx) + rgba[i11 + c] * wx;
@@ -766,6 +1372,7 @@ var DEFAULT_OCR_TUNING = Object.freeze({
766
1372
  textScore: 0.5,
767
1373
  recBatch: 1,
768
1374
  splitTall: true,
1375
+ trimEdges: true,
769
1376
  minInkContrast: 35,
770
1377
  postprocess: true,
771
1378
  tightBoxes: true,
@@ -777,6 +1384,7 @@ var REC_MIN_WIDTH = 320;
777
1384
  var REC_BATCH_MAX_PIXELS = 48 * 16e3;
778
1385
  var TALL_RATIO = 1.5;
779
1386
  var ROTATE_RATIO = 3;
1387
+ var KEEP_MARGIN = 0.06;
780
1388
  var LEADER_MIN_DOTS = 4;
781
1389
  var LEADER_CONTEXT = 0.5;
782
1390
  var MIN_INK_RATIO = 0.01;
@@ -862,6 +1470,8 @@ var OcrEngine = class _OcrEngine {
862
1470
  continue;
863
1471
  }
864
1472
  }
1473
+ const trim = tuning.trimEdges ? edgeTrim(gray, b.w, b.h, ink) : null;
1474
+ const keep = trim ? { ...trim, bg: median(gray) } : void 0;
865
1475
  const leaders = tuning.splitLeaders ? leaderRuns(gray, b.w, b.h, ink, LEADER_MIN_DOTS) : [];
866
1476
  if (leaders.length) {
867
1477
  const join2 = joins.length;
@@ -880,20 +1490,37 @@ var OcrEngine = class _OcrEngine {
880
1490
  joins[join2].trailDots = dots;
881
1491
  continue;
882
1492
  }
1493
+ if (keep) jobs.push({ box: b, rot: 0, group, keep });
883
1494
  jobs.push({ box: b, rot: 0, group: group++ });
884
1495
  }
885
1496
  const results = await this.recognizeJobs(rgba, width, jobs, tuning.recBatch);
886
1497
  const best = /* @__PURE__ */ new Map();
1498
+ const score = (job, confidence) => confidence - (job.keep ? KEEP_MARGIN : 0);
887
1499
  jobs.forEach((job, i) => {
888
1500
  const r = results[i];
889
1501
  if (!r) return;
890
1502
  const cur = best.get(job.group);
891
- if (!cur || r.confidence > cur.confidence) best.set(job.group, { job, ...r });
1503
+ if (!cur || score(job, r.confidence) > score(cur.job, cur.confidence)) best.set(job.group, { job, ...r });
892
1504
  });
893
1505
  let items = [];
894
- for (const { job, text: raw, confidence } of best.values()) {
895
- let text = tuning.postprocess ? restoreSymbols(raw.trim()) : raw;
1506
+ for (const { job, text: read, confidence, steps, stepPx } of best.values()) {
1507
+ const note = {};
1508
+ const raw = tuning.postprocess && job.rot === 0 ? restoreGlyphs(rgba, width, job.box, read, steps, stepPx, note) : read;
1509
+ let text = tuning.postprocess ? restoreSymbols(raw.trim(), note.ringLead) : raw;
896
1510
  if (!text.trim()) continue;
1511
+ if (tuning.postprocess && job.rot === 0 && !/^[◎●▪□■○ㅇ]/.test(text)) {
1512
+ const chars = [...read], k = chars.findIndex((c) => c.trim());
1513
+ if (k >= 0 && steps.length === chars.length) {
1514
+ const g = grayCrop(rgba, width, job.box);
1515
+ const b = leadingBullet(g, job.box.w, job.box.h, inkStats(g), (steps[k] + 0.5) * stepPx);
1516
+ const body = (text.match(/[가-힣]/g) ?? []).length >= 4;
1517
+ const miss = /^[Oo0•·ㆍ∙‧○]/.test(text);
1518
+ if (b?.mark === "\u25CE" && b.covers && /^[Oo0○]/.test(text)) text = b.mark + " " + text.slice(1).trimStart();
1519
+ else if (b?.mark === "\u25A1") {
1520
+ if (!b.covers && body) text = b.mark + " " + text;
1521
+ } else if (b && b.mark !== "\u25CE" && body) text = b.covers ? miss ? b.mark + " " + text.slice(1).trimStart() : text : b.mark + " " + text;
1522
+ }
1523
+ }
897
1524
  if (tuning.postprocess && /^\d/.test(text) && job.rot === 0) {
898
1525
  const g = grayCrop(rgba, width, job.box);
899
1526
  const tri = leadingTriangle(g, job.box.w, job.box.h, inkStats(g));
@@ -974,7 +1601,7 @@ var OcrEngine = class _OcrEngine {
974
1601
  // ─── rec ─────────────────────────────────────────────
975
1602
  /** 라인 작업들을 폭 비율 순으로 배치 인식 — 결과는 jobs 순서 */
976
1603
  async recognizeJobs(rgba, pageW, jobs, batchSize) {
977
- const crops = jobs.map((j) => lineCrop(rgba, pageW, j.box, j.rot));
1604
+ const crops = jobs.map((j) => lineCrop(rgba, pageW, j.box, j.rot, j.keep));
978
1605
  const order = crops.map((_, i) => i).sort((a, b) => crops[a].w - crops[b].w);
979
1606
  const results = new Array(jobs.length).fill(null);
980
1607
  const plane = REC_HEIGHT;
@@ -1006,18 +1633,39 @@ var OcrEngine = class _OcrEngine {
1006
1633
  const [, T, C] = logits.dims;
1007
1634
  const data = logits.data;
1008
1635
  idx.forEach((ci, k) => {
1009
- results[ci] = ctcDecode(data.subarray(k * T * C, (k + 1) * T * C), T, C, this.dict);
1636
+ const r = ctcDecode(data.subarray(k * T * C, (k + 1) * T * C), T, C, this.dict);
1637
+ const job = jobs[ci], src = job.rot === 0 ? job.box.w : job.box.h;
1638
+ let kept = r;
1639
+ const last = crops[ci].w * T / bw + 1;
1640
+ const chars = r ? [...r.text] : [];
1641
+ if (r && job.rot === 0 && chars.length === r.steps.length && r.steps.some((t) => t > last) && !r.text.includes("(cid:")) {
1642
+ const tallCh = (c) => !/[\s.,:;\u00b7\u2026'"\u2018-\u201d`\-_~]/.test(c);
1643
+ const g = grayCrop(rgba, pageW, job.box);
1644
+ const inside = chars.filter((c, i) => r.steps[i] <= last && tallCh(c)).length;
1645
+ let budget = inside <= 2 ? tallInkCount(g, job.box.w, job.box.h, inkStats(g)) - inside : Infinity;
1646
+ const keep = chars.map((c, i) => r.steps[i] <= last || !tallCh(c) || budget-- > 0);
1647
+ if (!keep.every(Boolean)) kept = { ...r, text: chars.filter((_, i) => keep[i]).join(""), steps: r.steps.filter((_, i) => keep[i]) };
1648
+ }
1649
+ results[ci] = kept && { ...kept, stepPx: bw / T * (src / crops[ci].w) };
1010
1650
  });
1011
1651
  s = e;
1012
1652
  }
1013
1653
  return results;
1014
1654
  }
1015
1655
  };
1656
+ function median(gray) {
1657
+ const hist = new Uint32Array(256);
1658
+ for (let i = 0; i < gray.length; i++) hist[gray[i]]++;
1659
+ for (let v = 0, n = 0; v < 256; v++) if ((n += hist[v]) * 2 >= gray.length) return v;
1660
+ return 255;
1661
+ }
1016
1662
  function ctcDecode(data, T, C, dict) {
1017
1663
  let text = "";
1018
1664
  let confSum = 0;
1019
1665
  let confCount = 0;
1020
1666
  let prev = -1;
1667
+ const runs = [];
1668
+ let open = [];
1021
1669
  for (let t = 0; t < T; t++) {
1022
1670
  const off = t * C;
1023
1671
  let best = 0;
@@ -1031,7 +1679,12 @@ function ctcDecode(data, T, C, dict) {
1031
1679
  }
1032
1680
  const repeat = best === prev;
1033
1681
  prev = best;
1034
- if (best === 0 || repeat) continue;
1682
+ if (repeat && best !== 0) {
1683
+ for (const r of open) r[1] = t;
1684
+ continue;
1685
+ }
1686
+ open = [];
1687
+ if (best === 0) continue;
1035
1688
  let p = bestV;
1036
1689
  if (p > 1.0001 || p < 0) {
1037
1690
  let denom = 0;
@@ -1040,11 +1693,16 @@ function ctcDecode(data, T, C, dict) {
1040
1693
  }
1041
1694
  confSum += p;
1042
1695
  confCount++;
1043
- if (best >= 1 && best <= dict.length) text += dict[best - 1];
1044
- else if (best === dict.length + 1) text += " ";
1696
+ const tok = best >= 1 && best <= dict.length ? dict[best - 1] : best === dict.length + 1 ? " " : "";
1697
+ text += tok;
1698
+ for (const _ of tok) {
1699
+ const r = [t, t];
1700
+ runs.push(r);
1701
+ open.push(r);
1702
+ }
1045
1703
  }
1046
1704
  if (!text) return null;
1047
- return { text, confidence: confCount > 0 ? confSum / confCount : 0 };
1705
+ return { text, confidence: confCount > 0 ? confSum / confCount : 0, steps: runs.map(([a, b]) => (a + b) / 2) };
1048
1706
  }
1049
1707
  function componentBoxes(prob, w, h, thresh = DEFAULT_OCR_TUNING.detThresh, boxThresh = DEFAULT_OCR_TUNING.detBoxThresh) {
1050
1708
  const visited = new Uint8Array(w * h);
@@ -1263,7 +1921,7 @@ async function ocrOnePage(page, pageNo, mode, engine, warnings, detectTables, ve
1263
1921
  const upright = vectorOps ? rgba : deskewPage(rgba, rw, rh).rgba;
1264
1922
  const stats = { droppedLowConf: 0 };
1265
1923
  const ruling = vectorOps ? void 0 : detectRulingLines(upright, rw, rh, rh / pdfH);
1266
- const items = await engine.recognizePage(upright, rw, rh, stats, void 0, ruling?.cellDividers);
1924
+ const items = await engine.recognizePage(upright, rw, rh, stats, regions ? REGION_TUNING : void 0, ruling?.cellDividers);
1267
1925
  if (stats.droppedLowConf > 0) {
1268
1926
  warnings.push({
1269
1927
  page: pageNo,
@@ -1315,6 +1973,7 @@ function halve(src, w, h) {
1315
1973
  }
1316
1974
  return { rgba: out, width: W, height: H };
1317
1975
  }
1976
+ var REGION_TUNING = Object.freeze({ ...DEFAULT_OCR_TUNING, trimEdges: false });
1318
1977
  function meanConfidence(items) {
1319
1978
  let n = 0, sum = 0;
1320
1979
  for (const it of items) {
@@ -1335,7 +1994,7 @@ async function closerReads(rgba, rw, rh, pdfH, hi, regions, engine) {
1335
1994
  }
1336
1995
  const crop = new Uint8Array(cw * ch * 4);
1337
1996
  for (let y = 0; y < ch; y++) crop.set(rgba.subarray(((y0 + y) * rw + x0) * 4, ((y0 + y) * rw + x0 + cw) * 4), y * cw * 4);
1338
- const items = await engine.recognizePage(crop, cw, ch);
1997
+ const items = await engine.recognizePage(crop, cw, ch, void 0, REGION_TUNING);
1339
1998
  out.push(items.map((it) => ({ ...it, x: (it.x + x0) / 2, y: (it.y + y0) / 2, w: it.w / 2, h: it.h / 2 })));
1340
1999
  }
1341
2000
  return out;
@@ -1402,4 +2061,4 @@ export {
1402
2061
  runPdfOcr,
1403
2062
  ocrItemsToBlocks
1404
2063
  };
1405
- //# sourceMappingURL=chunk-XHOZB2WP.js.map
2064
+ //# sourceMappingURL=chunk-UTP2WCHS.js.map