kordoc 4.24.6 → 4.24.7

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 (139) hide show
  1. package/README.md +3 -2
  2. package/dist/{-7J7Y3XPH.js → -GQDQXRQN.js} +11 -11
  3. package/dist/{chunk-AM5ALISV.cjs → chunk-7BZBMXWD.cjs} +6 -6
  4. package/dist/chunk-7BZBMXWD.cjs.map +1 -0
  5. package/dist/{chunk-VMRH6UVA.js → chunk-7ICC55XY.js} +2 -2
  6. package/dist/{chunk-435ECXNG.cjs → chunk-AYGMF6H5.cjs} +3 -3
  7. package/dist/chunk-AYGMF6H5.cjs.map +1 -0
  8. package/dist/{chunk-3ZLS54V4.js → chunk-BLWDQEBY.js} +2 -2
  9. package/dist/{chunk-WNOWRBM4.js → chunk-BNIXSB6D.js} +2 -2
  10. package/dist/{chunk-5XBZJ3O4.js → chunk-C4AY47C5.js} +2 -2
  11. package/dist/{chunk-PKCOOSDT.js → chunk-CG2Z6NB3.js} +2 -2
  12. package/dist/{chunk-G343MTBX.js → chunk-DEDUWANG.js} +43 -15
  13. package/dist/chunk-DEDUWANG.js.map +1 -0
  14. package/dist/{chunk-TLJJF3J6.js → chunk-DZRXJEOF.js} +3 -3
  15. package/dist/{chunk-TZVVK5MI.js → chunk-E35YPWOX.js} +2 -2
  16. package/dist/{chunk-W75FRCAP.js → chunk-EIGCAAVM.js} +909 -36
  17. package/dist/chunk-EIGCAAVM.js.map +1 -0
  18. package/dist/{chunk-BKTEGPEO.js → chunk-EIUXBJZP.js} +3 -3
  19. package/dist/{chunk-XVTGFK2F.js → chunk-EJPPVB2Y.js} +2 -2
  20. package/dist/{chunk-EVBSSU3N.js → chunk-FIS3WY5Q.js} +2 -2
  21. package/dist/{chunk-GI5LWQ3S.js → chunk-FQVWYJNM.js} +2 -2
  22. package/dist/{chunk-BZW2WS44.js → chunk-GMKFXPEI.js} +82 -715
  23. package/dist/chunk-GMKFXPEI.js.map +1 -0
  24. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  25. package/dist/chunk-J55UQC5H.cjs.map +1 -1
  26. package/dist/{chunk-DFRQD254.js → chunk-JKZY6FX5.js} +2 -2
  27. package/dist/{chunk-XM2Y4DB5.js → chunk-MLFPEETP.js} +2 -2
  28. package/dist/{chunk-JIU7E7Z6.cjs → chunk-MNG25FK4.cjs} +1082 -209
  29. package/dist/chunk-MNG25FK4.cjs.map +1 -0
  30. package/dist/{chunk-5RZBLPVP.js → chunk-N2JZGYFK.js} +2 -2
  31. package/dist/{chunk-GYX26HZR.js → chunk-O4RQCDN2.js} +910 -37
  32. package/dist/chunk-O4RQCDN2.js.map +1 -0
  33. package/dist/{chunk-BBKRJE57.js → chunk-OMJPCWKS.js} +2 -2
  34. package/dist/{chunk-RMUDX5Q2.js → chunk-OPNMXBUR.js} +2 -2
  35. package/dist/chunk-TK3FDAEE.cjs.map +1 -1
  36. package/dist/{chunk-HAMIOQYO.js → chunk-UDUEOBS5.js} +2 -2
  37. package/dist/{chunk-J4XCQNVH.cjs → chunk-V4PS653P.cjs} +140 -773
  38. package/dist/chunk-V4PS653P.cjs.map +1 -0
  39. package/dist/{chunk-ISTIOEIL.cjs → chunk-VHAOAQZZ.cjs} +3 -3
  40. package/dist/chunk-VHAOAQZZ.cjs.map +1 -0
  41. package/dist/{chunk-EBJWBGWF.js → chunk-WVZXPEHX.js} +82 -715
  42. package/dist/chunk-WVZXPEHX.js.map +1 -0
  43. package/dist/{chunk-2JDVPQ33.cjs → chunk-YQB3LPKQ.cjs} +2 -2
  44. package/dist/chunk-YQB3LPKQ.cjs.map +1 -0
  45. package/dist/{chunk-KR4UKOX2.js → chunk-ZQQGV7K2.js} +6 -6
  46. package/dist/chunks-XURCANPP.js +10 -0
  47. package/dist/cli.js +30 -30
  48. package/dist/{formula-DKNZKJ5Z.js → formula-6XLWGUCB.js} +4 -4
  49. package/dist/{formula-D7LBREWP.cjs → formula-C55TRBHH.cjs} +9 -9
  50. package/dist/formula-C55TRBHH.cjs.map +1 -0
  51. package/dist/{formula-T7D5JIZR.js → formula-DCC5EOSX.js} +3 -3
  52. package/dist/{image-ocr-N3I3MUKF.cjs → image-ocr-EV7GVD3W.cjs} +17 -17
  53. package/dist/image-ocr-EV7GVD3W.cjs.map +1 -0
  54. package/dist/{image-ocr-7NCSIZGA.js → image-ocr-K3ZXDUNU.js} +6 -6
  55. package/dist/{image-ocr-5POXVGN2.js → image-ocr-L736MGYD.js} +9 -9
  56. package/dist/index.cjs +729 -701
  57. package/dist/index.cjs.map +1 -1
  58. package/dist/index.js +38 -10
  59. package/dist/index.js.map +1 -1
  60. package/dist/mcp.js +23 -23
  61. package/dist/{model-bundle-XT7G2IJU.js → model-bundle-OOOJFMGR.js} +5 -5
  62. package/dist/{models-AB2VO45L.js → models-ELFZII4R.js} +5 -5
  63. package/dist/page-range-QHHFTL7X.cjs.map +1 -1
  64. package/dist/{parse-worker-v2-YSSAUPWU.js → parse-worker-v2-WS35WZY5.js} +12 -12
  65. package/dist/{parser-KGDUPSA7.js → parser-CNRI55MG.js} +9 -9
  66. package/dist/{parser-ES3I54QL.js → parser-K7Y44QHS.js} +12 -12
  67. package/dist/{parser-AGDY3ZOA.cjs → parser-X6P3JBT2.cjs} +159 -159
  68. package/dist/parser-X6P3JBT2.cjs.map +1 -0
  69. package/dist/{pdf-ocr-7QSP7JLG.js → pdf-ocr-MVNHXGPS.js} +6 -6
  70. package/dist/{pdf-ocr-53WUTX6H.js → pdf-ocr-QQPZLIHM.js} +9 -9
  71. package/dist/pdf-ocr-S6GSKOPT.cjs +31 -0
  72. package/dist/pdf-ocr-S6GSKOPT.cjs.map +1 -0
  73. package/dist/{profile-io-GGEJ3MZW.js → profile-io-GSRLMU4D.js} +3 -3
  74. package/dist/{rasterize-YXICEI5S.js → rasterize-3H5QKDH2.js} +3 -3
  75. package/dist/{redact-doc-OYXBNTWI.js → redact-doc-TLBF7J7W.js} +11 -11
  76. package/dist/{render-4PBNDLK2.js → render-FTCOQ6WO.js} +7 -7
  77. package/dist/seal-74R7KDRY.js +10 -0
  78. package/dist/{setup-WHTMPK74.js → setup-WVZ47AKV.js} +3 -3
  79. package/dist/visual-TEW6Y3NH.js +21 -0
  80. package/dist/{watch-P6UWW52D.js → watch-CA32Z7XB.js} +12 -12
  81. package/package.json +1 -1
  82. package/dist/chunk-2JDVPQ33.cjs.map +0 -1
  83. package/dist/chunk-435ECXNG.cjs.map +0 -1
  84. package/dist/chunk-AM5ALISV.cjs.map +0 -1
  85. package/dist/chunk-BZW2WS44.js.map +0 -1
  86. package/dist/chunk-EBJWBGWF.js.map +0 -1
  87. package/dist/chunk-G343MTBX.js.map +0 -1
  88. package/dist/chunk-GYX26HZR.js.map +0 -1
  89. package/dist/chunk-ISTIOEIL.cjs.map +0 -1
  90. package/dist/chunk-J4XCQNVH.cjs.map +0 -1
  91. package/dist/chunk-JIU7E7Z6.cjs.map +0 -1
  92. package/dist/chunk-W75FRCAP.js.map +0 -1
  93. package/dist/chunks-3MWJ7HOZ.js +0 -10
  94. package/dist/formula-D7LBREWP.cjs.map +0 -1
  95. package/dist/image-ocr-N3I3MUKF.cjs.map +0 -1
  96. package/dist/parser-AGDY3ZOA.cjs.map +0 -1
  97. package/dist/pdf-ocr-X6OAHK46.cjs +0 -31
  98. package/dist/pdf-ocr-X6OAHK46.cjs.map +0 -1
  99. package/dist/seal-KJYAU23C.js +0 -10
  100. package/dist/visual-PHKJTSFR.js +0 -21
  101. /package/dist/{-7J7Y3XPH.js.map → -GQDQXRQN.js.map} +0 -0
  102. /package/dist/{chunk-VMRH6UVA.js.map → chunk-7ICC55XY.js.map} +0 -0
  103. /package/dist/{chunk-3ZLS54V4.js.map → chunk-BLWDQEBY.js.map} +0 -0
  104. /package/dist/{chunk-WNOWRBM4.js.map → chunk-BNIXSB6D.js.map} +0 -0
  105. /package/dist/{chunk-5XBZJ3O4.js.map → chunk-C4AY47C5.js.map} +0 -0
  106. /package/dist/{chunk-PKCOOSDT.js.map → chunk-CG2Z6NB3.js.map} +0 -0
  107. /package/dist/{chunk-TLJJF3J6.js.map → chunk-DZRXJEOF.js.map} +0 -0
  108. /package/dist/{chunk-TZVVK5MI.js.map → chunk-E35YPWOX.js.map} +0 -0
  109. /package/dist/{chunk-BKTEGPEO.js.map → chunk-EIUXBJZP.js.map} +0 -0
  110. /package/dist/{chunk-XVTGFK2F.js.map → chunk-EJPPVB2Y.js.map} +0 -0
  111. /package/dist/{chunk-EVBSSU3N.js.map → chunk-FIS3WY5Q.js.map} +0 -0
  112. /package/dist/{chunk-GI5LWQ3S.js.map → chunk-FQVWYJNM.js.map} +0 -0
  113. /package/dist/{chunk-DFRQD254.js.map → chunk-JKZY6FX5.js.map} +0 -0
  114. /package/dist/{chunk-XM2Y4DB5.js.map → chunk-MLFPEETP.js.map} +0 -0
  115. /package/dist/{chunk-5RZBLPVP.js.map → chunk-N2JZGYFK.js.map} +0 -0
  116. /package/dist/{chunk-BBKRJE57.js.map → chunk-OMJPCWKS.js.map} +0 -0
  117. /package/dist/{chunk-RMUDX5Q2.js.map → chunk-OPNMXBUR.js.map} +0 -0
  118. /package/dist/{chunk-HAMIOQYO.js.map → chunk-UDUEOBS5.js.map} +0 -0
  119. /package/dist/{chunk-KR4UKOX2.js.map → chunk-ZQQGV7K2.js.map} +0 -0
  120. /package/dist/{chunks-3MWJ7HOZ.js.map → chunks-XURCANPP.js.map} +0 -0
  121. /package/dist/{formula-DKNZKJ5Z.js.map → formula-6XLWGUCB.js.map} +0 -0
  122. /package/dist/{formula-T7D5JIZR.js.map → formula-DCC5EOSX.js.map} +0 -0
  123. /package/dist/{image-ocr-7NCSIZGA.js.map → image-ocr-K3ZXDUNU.js.map} +0 -0
  124. /package/dist/{image-ocr-5POXVGN2.js.map → image-ocr-L736MGYD.js.map} +0 -0
  125. /package/dist/{model-bundle-XT7G2IJU.js.map → model-bundle-OOOJFMGR.js.map} +0 -0
  126. /package/dist/{models-AB2VO45L.js.map → models-ELFZII4R.js.map} +0 -0
  127. /package/dist/{parse-worker-v2-YSSAUPWU.js.map → parse-worker-v2-WS35WZY5.js.map} +0 -0
  128. /package/dist/{parser-KGDUPSA7.js.map → parser-CNRI55MG.js.map} +0 -0
  129. /package/dist/{parser-ES3I54QL.js.map → parser-K7Y44QHS.js.map} +0 -0
  130. /package/dist/{pdf-ocr-53WUTX6H.js.map → pdf-ocr-MVNHXGPS.js.map} +0 -0
  131. /package/dist/{pdf-ocr-7QSP7JLG.js.map → pdf-ocr-QQPZLIHM.js.map} +0 -0
  132. /package/dist/{profile-io-GGEJ3MZW.js.map → profile-io-GSRLMU4D.js.map} +0 -0
  133. /package/dist/{rasterize-YXICEI5S.js.map → rasterize-3H5QKDH2.js.map} +0 -0
  134. /package/dist/{redact-doc-OYXBNTWI.js.map → redact-doc-TLBF7J7W.js.map} +0 -0
  135. /package/dist/{render-4PBNDLK2.js.map → render-FTCOQ6WO.js.map} +0 -0
  136. /package/dist/{seal-KJYAU23C.js.map → seal-74R7KDRY.js.map} +0 -0
  137. /package/dist/{setup-WHTMPK74.js.map → setup-WVZ47AKV.js.map} +0 -0
  138. /package/dist/{visual-PHKJTSFR.js.map → visual-TEW6Y3NH.js.map} +0 -0
  139. /package/dist/{watch-P6UWW52D.js.map → watch-CA32Z7XB.js.map} +0 -0
@@ -4,20 +4,43 @@ import {
4
4
  import {
5
5
  DEFAULT_OCR_MODELS,
6
6
  UNRULED_REGION_TABLES,
7
+ bracketFeatures,
8
+ bracketShape,
9
+ circledAt,
10
+ copyOcrTextGaps,
11
+ dashRatio,
12
+ discAt,
13
+ edgeTrim,
7
14
  ensureOcrModelsFor,
8
15
  extractPageBlocksWithLines,
16
+ findOcrTextGaps,
17
+ gapGlyphs,
18
+ grayCrop,
19
+ inkBounds,
20
+ inkStats,
21
+ leaderRuns,
22
+ leadingBullet,
23
+ leadingTriangle,
9
24
  ocrSelectionKey,
10
25
  ocrSourcePath,
11
26
  parseCharacterDict,
12
- resolveOcrModels
13
- } from "./chunk-GYX26HZR.js";
27
+ quoteHead,
28
+ rememberOcrTextGaps,
29
+ resolveOcrModels,
30
+ ringBullet,
31
+ romanStems,
32
+ serifOne,
33
+ splitRowBands,
34
+ starRun,
35
+ tallInkCount
36
+ } from "./chunk-O4RQCDN2.js";
14
37
  import {
15
38
  validateOcrOptions
16
- } from "./chunk-WNOWRBM4.js";
39
+ } from "./chunk-BNIXSB6D.js";
17
40
  import {
18
41
  KordocError,
19
42
  OPTIONAL_DEP_INSTALL_HINT
20
- } from "./chunk-DFRQD254.js";
43
+ } from "./chunk-JKZY6FX5.js";
21
44
 
22
45
  // src/ocr/ruling-lines.ts
23
46
  var INK_LUMA_MAX = 205;
@@ -228,15 +251,15 @@ function dottedRuns(mask, base, step, span, minLenPx, maxGap, maxDash, maxPeriod
228
251
  const flush = () => {
229
252
  const cnt = chain.length >> 1;
230
253
  if (cnt >= DOT_MIN_COUNT && chain[chain.length - 1] - chain[0] + 1 >= minLenPx) {
231
- const periods = [], dashes = [], gaps2 = [];
254
+ const periods = [], dashes2 = [], gaps2 = [];
232
255
  for (let k = 0; k < cnt; k++) {
233
- dashes.push(chain[2 * k + 1] - chain[2 * k] + 1);
256
+ dashes2.push(chain[2 * k + 1] - chain[2 * k] + 1);
234
257
  if (k) {
235
258
  periods.push(chain[2 * k] - chain[2 * k - 2]);
236
259
  gaps2.push(chain[2 * k] - chain[2 * k - 1] - 1);
237
260
  }
238
261
  }
239
- if (median2(periods) <= maxPeriod || median2(dashes) >= 1.5 * median2(gaps2)) runs.push({ lo: chain[0], hi: chain[chain.length - 1] });
262
+ if (median2(periods) <= maxPeriod || median2(dashes2) >= 1.5 * median2(gaps2)) runs.push({ lo: chain[0], hi: chain[chain.length - 1] });
240
263
  }
241
264
  chain.length = 0;
242
265
  };
@@ -346,710 +369,8 @@ import { createHash } from "crypto";
346
369
  import { readFile, stat } from "fs/promises";
347
370
  import { basename } from "path";
348
371
 
349
- // src/ocr/line-split.ts
350
- function grayCrop(rgba, pageW, box) {
351
- const out = new Uint8Array(box.w * box.h);
352
- for (let y = 0; y < box.h; y++) {
353
- let si = ((box.y + y) * pageW + box.x) * 4;
354
- let di = y * box.w;
355
- for (let x = 0; x < box.w; x++, si += 4, di++) {
356
- out[di] = rgba[si] * 77 + rgba[si + 1] * 150 + rgba[si + 2] * 29 >> 8;
357
- }
358
- }
359
- return out;
360
- }
361
- function inkStats(gray) {
362
- const hist = new Uint32Array(256);
363
- for (let i = 0; i < gray.length; i++) hist[gray[i]]++;
364
- const total = gray.length;
365
- let sumAll = 0;
366
- for (let v = 0; v < 256; v++) sumAll += v * hist[v];
367
- let wB = 0, sumB = 0, best = -1, thr = 127;
368
- for (let t = 0; t < 256; t++) {
369
- wB += hist[t];
370
- if (wB === 0) continue;
371
- const wF = total - wB;
372
- if (wF === 0) break;
373
- sumB += t * hist[t];
374
- const mB = sumB / wB;
375
- const mF = (sumAll - sumB) / wF;
376
- const between = wB * wF * (mB - mF) * (mB - mF);
377
- if (between > best) {
378
- best = between;
379
- thr = t;
380
- }
381
- }
382
- let nDark = 0, sDark = 0;
383
- for (let v = 0; v <= thr; v++) {
384
- nDark += hist[v];
385
- sDark += v * hist[v];
386
- }
387
- const nLight = total - nDark;
388
- const sLight = sumAll - sDark;
389
- if (nDark === 0 || nLight === 0) return { threshold: thr, darkInk: true, contrast: 0, inkRatio: 0 };
390
- const darkInk = nDark <= nLight;
391
- return {
392
- threshold: thr,
393
- darkInk,
394
- contrast: sLight / nLight - sDark / nDark,
395
- inkRatio: (darkInk ? nDark : nLight) / total
396
- };
397
- }
398
- function splitRowBands(gray, w, h, ink, minBandRatio) {
399
- const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
400
- const colInk = new Uint32Array(w);
401
- for (let y = 0; y < h; y++) {
402
- const off = y * w;
403
- for (let x = 0; x < w; x++) if (isInk(gray[off + x])) colInk[x]++;
404
- }
405
- const ruleCol = new Uint8Array(w);
406
- let liveW = 0;
407
- for (let x = 0; x < w; x++) {
408
- if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
409
- else liveW++;
410
- }
411
- const rowInk = new Uint32Array(h);
412
- for (let y = 0; y < h; y++) {
413
- let n = 0;
414
- const off = y * w;
415
- for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) n++;
416
- rowInk[y] = n >= liveW * 0.85 ? 0 : n;
417
- }
418
- const blank = Math.max(1, Math.floor(w * 0.02));
419
- let bands = [];
420
- let start = -1;
421
- for (let y = 0; y <= h; y++) {
422
- const on = y < h && rowInk[y] > blank;
423
- if (on && start < 0) start = y;
424
- else if (!on && start >= 0) {
425
- bands.push([start, y]);
426
- start = -1;
427
- }
428
- }
429
- const inkCols = (y0, y1) => {
430
- let x0 = w, x1 = -1;
431
- for (let y = y0; y < y1; y++) {
432
- const off = y * w;
433
- for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) {
434
- if (x < x0) x0 = x;
435
- if (x > x1) x1 = x;
436
- }
437
- }
438
- return x1 >= x0 ? [x0, x1 + 1] : [0, w];
439
- };
440
- if (bands.length <= 1) return [{ y0: bands[0]?.[0] ?? 0, y1: bands[0]?.[1] ?? h, x0: 0, x1: w }];
441
- const widths = bands.map(([y0, y1]) => {
442
- const [a, b] = inkCols(y0, y1);
443
- return b - a;
444
- }).sort((a, b) => a - b);
445
- const charSize = widths[widths.length >> 1];
446
- const minBand = Math.max(3, minBandRatio * charSize);
447
- for (let i = 0; i + 1 < bands.length; ) {
448
- const gap = bands[i + 1][0] - bands[i][1];
449
- if (gap <= Math.max(2, charSize * 0.15) && bands[i + 1][1] - bands[i][0] <= charSize * 1.3) {
450
- bands.splice(i, 2, [bands[i][0], bands[i + 1][1]]);
451
- } else i++;
452
- }
453
- while (bands.length > 1 && bands[0][0] <= 1 && bands[0][1] - bands[0][0] < minBand) bands.shift();
454
- while (bands.length > 1 && bands[bands.length - 1][1] >= h - 1 && bands[bands.length - 1][1] - bands[bands.length - 1][0] < minBand) bands.pop();
455
- for (; ; ) {
456
- let idx = -1, minH = Infinity;
457
- for (let i = 0; i < bands.length; i++) {
458
- const bh = bands[i][1] - bands[i][0];
459
- if (bh < minBand && bh < minH) {
460
- minH = bh;
461
- idx = i;
462
- }
463
- }
464
- if (idx < 0 || bands.length === 1) break;
465
- const gapPrev = idx > 0 ? bands[idx][0] - bands[idx - 1][1] : Infinity;
466
- const gapNext = idx < bands.length - 1 ? bands[idx + 1][0] - bands[idx][1] : Infinity;
467
- const j = gapPrev <= gapNext ? idx - 1 : idx + 1;
468
- const a = Math.min(idx, j), b = Math.max(idx, j);
469
- bands = [...bands.slice(0, a), [bands[a][0], bands[b][1]], ...bands.slice(b + 1)];
470
- }
471
- if (bands.length === 1) return [{ y0: bands[0][0], y1: bands[0][1], x0: 0, x1: w }];
472
- return bands.map(([y0, y1]) => {
473
- const [x0, x1] = inkCols(y0, y1);
474
- return { y0, y1, x0, x1 };
475
- });
476
- }
477
- function components(gray, w, h, ink) {
478
- const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
479
- const comps = [];
480
- const label = new Int32Array(w * h);
481
- const stack = [];
482
- for (let p0 = 0; p0 < w * h; p0++) {
483
- if (label[p0] || !isInk(gray[p0])) continue;
484
- const c = { x0: w, x1: 0, y0: h, y1: 0, id: comps.length + 1 };
485
- label[p0] = c.id;
486
- stack.push(p0);
487
- while (stack.length) {
488
- const p = stack.pop();
489
- const x = p % w, y = p / w | 0;
490
- if (x < c.x0) c.x0 = x;
491
- if (x + 1 > c.x1) c.x1 = x + 1;
492
- if (y < c.y0) c.y0 = y;
493
- if (y + 1 > c.y1) c.y1 = y + 1;
494
- for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
495
- const nx = x + dx, ny = y + dy;
496
- if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
497
- const q = ny * w + nx;
498
- if (!label[q] && isInk(gray[q])) {
499
- label[q] = c.id;
500
- stack.push(q);
501
- }
502
- }
503
- }
504
- if (c.y1 - c.y0 < h * 0.85 && c.x1 - c.x0 < w * 0.85) comps.push(c);
505
- }
506
- return { comps, label };
507
- }
508
- function leadingTriangle(gray, w, h, ink) {
509
- const { comps, label } = components(gray, w, h, ink);
510
- let charH = 0;
511
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
512
- const first = comps.filter((c) => c.y1 - c.y0 >= charH * 0.4).sort((a, b) => a.x0 - b.x0)[0];
513
- if (!first || charH < 8) return null;
514
- const cw = first.x1 - first.x0, ch = first.y1 - first.y0;
515
- if (cw < ch * 0.8 || cw > ch * 1.8) return null;
516
- const rowSpan = (y) => {
517
- let n = 0, lo = -1, hi = -1, runs = 0, prev = false;
518
- for (let x = first.x0; x < first.x1; x++) {
519
- const on = label[y * w + x] === first.id;
520
- if (on) {
521
- n++;
522
- if (lo < 0) lo = x - first.x0;
523
- hi = x - first.x0;
524
- if (!prev) runs++;
525
- }
526
- prev = on;
527
- }
528
- return { n, lo, hi, runs };
529
- };
530
- const base = Math.max(rowSpan(first.y1 - 1).n, rowSpan(first.y1 - 2).n);
531
- if (base < cw * 0.7) return null;
532
- for (let y = first.y0; y < first.y0 + Math.max(1, Math.round(ch * 0.2)); y++) {
533
- const r = rowSpan(y);
534
- if (r.n === 0) continue;
535
- if (r.hi - r.lo + 1 > cw * 0.4 || (r.lo + r.hi) / 2 < cw * 0.25 || (r.lo + r.hi) / 2 > cw * 0.75) return null;
536
- }
537
- const mid = rowSpan(first.y0 + Math.round(ch * 0.6));
538
- if (mid.lo < 0 || mid.lo > cw * 0.3 || mid.hi < cw * 0.7) return null;
539
- return mid.runs >= 2 ? "\u25B3" : "\u25B2";
540
- }
541
- function leaderRuns(gray, w, h, ink, minDots) {
542
- const comps = components(gray, w, h, ink).comps.sort((a, b) => a.x0 - b.x0);
543
- const blobs = [];
544
- for (const c of comps) {
545
- const last = blobs[blobs.length - 1];
546
- if (last && c.x0 < last.x1) {
547
- last.x1 = Math.max(last.x1, c.x1);
548
- last.y0 = Math.min(last.y0, c.y0);
549
- last.y1 = Math.max(last.y1, c.y1);
550
- } else blobs.push({ x0: c.x0, x1: c.x1, y0: c.y0, y1: c.y1 });
551
- }
552
- let charH = 0;
553
- for (const b of blobs) charH = Math.max(charH, b.y1 - b.y0);
554
- if (charH < 8) return [];
555
- const small = (c) => c.x1 - c.x0 <= charH * 0.3 && c.y1 - c.y0 <= charH * 0.3;
556
- let bandTop = h, bandBot = 0;
557
- for (const c of comps) if (!small(c)) {
558
- bandTop = Math.min(bandTop, c.y0);
559
- bandBot = Math.max(bandBot, c.y1);
560
- }
561
- const cy = (c) => (c.y0 + c.y1) / 2;
562
- const isDot = (b) => small(b) && cy(b) >= bandTop && cy(b) <= bandBot;
563
- const runs = [];
564
- for (let i = 0; i < blobs.length; ) {
565
- if (!isDot(blobs[i])) {
566
- i++;
567
- continue;
568
- }
569
- let j = i;
570
- while (j + 1 < blobs.length && isDot(blobs[j + 1]) && blobs[j + 1].x0 - blobs[j].x1 <= charH && Math.abs(cy(blobs[j + 1]) - cy(blobs[i])) <= charH * 0.2) j++;
571
- const x0 = blobs[i].x0, x1 = blobs[j].x1, y = cy(blobs[i]);
572
- const onLine = comps.some((c) => !small(c) && c.y0 <= y && c.y1 >= y && (c.x1 <= x0 + 2 && c.x1 >= x0 - 2 * charH || c.x0 >= x1 - 2 && c.x0 <= x1 + 2 * charH));
573
- const run = blobs.slice(i, j + 1).filter((b) => b.x0 > 0 && b.x1 < w);
574
- const spread = (xs) => {
575
- const v = [...xs].sort((a, b) => a - b).slice(xs.length >= 5 ? 1 : 0, xs.length >= 5 ? -1 : void 0);
576
- return v[v.length - 1] / Math.max(1, v[0]);
577
- };
578
- const even = run.length >= 2 && spread(run.map((b) => b.x1 - b.x0)) <= 2.5 && spread(run.map((b) => b.y1 - b.y0)) <= 2.5 && spread(run.slice(1).map((b, k) => b.x0 - run[k].x0)) <= 1.8;
579
- if (j - i + 1 >= minDots && onLine && even) runs.push([x0, x1]);
580
- i = j + 1;
581
- }
582
- return runs;
583
- }
584
- function inkBounds(gray, w, h, ink) {
585
- const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
586
- const colInk = new Uint32Array(w);
587
- const rowInk = new Uint32Array(h);
588
- for (let y = 0; y < h; y++) {
589
- const off = y * w;
590
- for (let x = 0; x < w; x++) if (isInk(gray[off + x])) {
591
- colInk[x]++;
592
- rowInk[y]++;
593
- }
594
- }
595
- const ruleCol = new Uint8Array(w), ruleRow = new Uint8Array(h);
596
- for (let x = 0; x < w; x++) if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
597
- for (let y = 0; y < h; y++) if (rowInk[y] >= w * 0.85) ruleRow[y] = 1;
598
- let x0 = w, x1 = -1, y0 = h, y1 = -1;
599
- for (let y = 0; y < h; y++) {
600
- if (ruleRow[y]) continue;
601
- const off = y * w;
602
- for (let x = 0; x < w; x++) {
603
- if (ruleCol[x] || !isInk(gray[off + x])) continue;
604
- if (x < x0) x0 = x;
605
- if (x > x1) x1 = x;
606
- if (y < y0) y0 = y;
607
- if (y > y1) y1 = y;
608
- }
609
- }
610
- if (x1 < x0 || y1 < y0) return { x0: 0, y0: 0, x1: w, y1: h };
611
- return { x0, y0, x1: x1 + 1, y1: y1 + 1 };
612
- }
613
- function bracketFeatures(gray, w, h, ink, cx) {
614
- const { comps, label } = components(gray, w, h, ink);
615
- let charH = 0;
616
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
617
- if (charH < 8) return null;
618
- const tol = Math.max(2, charH * 0.3);
619
- let best = null, bestD = Infinity;
620
- for (const c of comps) {
621
- if (c.y1 - c.y0 < charH * 0.3 || c.x1 < cx - tol || c.x0 > cx + tol) continue;
622
- const d = Math.abs((c.x0 + c.x1) / 2 - cx);
623
- if (d < bestD) {
624
- bestD = d;
625
- best = c;
626
- }
627
- }
628
- if (!best) return null;
629
- const b = best;
630
- const cw = b.x1 - b.x0, ch = b.y1 - b.y0;
631
- if (cw < 3 || ch < 8) return null;
632
- const spans = [];
633
- for (let y = b.y0; y < b.y1; y++) {
634
- let n = 0;
635
- for (let x = b.x0; x < b.x1; x++) if (label[y * w + x] === b.id) n++;
636
- spans.push(n);
637
- }
638
- const band = Math.max(1, Math.round(ch * 0.15));
639
- const top = Math.max(...spans.slice(0, band)) / cw, bottom = Math.max(...spans.slice(ch - band)) / cw;
640
- const midRows = spans.slice(Math.floor(ch * 0.3), Math.ceil(ch * 0.7)).sort((p, q) => p - q);
641
- const stroke = midRows[midRows.length >> 1] ?? 0;
642
- const taper = spans.filter((n) => n > stroke && n < cw).length / ch;
643
- const fill = spans.reduce((a, n) => a + n, 0) / (cw * ch);
644
- return { top, bottom, taper, fill, cw, ch, charH };
645
- }
646
- function romanStems(gray, w, h, ink, prevX, nextX) {
647
- const { comps, label } = components(gray, w, h, ink);
648
- let charH = 0;
649
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
650
- if (charH < 10) return null;
651
- const x0 = prevX + charH * 0.35, x1 = nextX - charH * 0.35;
652
- const sel = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6 && (c.x0 + c.x1) / 2 >= x0 && (c.x0 + c.x1) / 2 < x1);
653
- if (!sel.length) return null;
654
- const ids = new Set(sel.map((c) => c.id));
655
- const ux0 = Math.min(...sel.map((c) => c.x0)), ux1 = Math.max(...sel.map((c) => c.x1));
656
- const uy0 = Math.min(...sel.map((c) => c.y0)), uy1 = Math.max(...sel.map((c) => c.y1));
657
- const uh = uy1 - uy0;
658
- const counts = /* @__PURE__ */ new Map();
659
- let rows = 0;
660
- for (let y = uy0 + Math.floor(uh * 0.25); y < uy1 - Math.floor(uh * 0.25); y++) {
661
- let runs = 0, len = 0, thick = false;
662
- for (let x = ux0; x <= ux1; x++) {
663
- const on = x < ux1 && ids.has(label[y * w + x]);
664
- if (on) len++;
665
- else if (len) {
666
- runs++;
667
- if (len > uh * 0.35) thick = true;
668
- len = 0;
669
- }
670
- }
671
- rows++;
672
- if (!thick) counts.set(runs, (counts.get(runs) ?? 0) + 1);
673
- }
674
- const [n, k] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [0, 0];
675
- return n >= 1 && n <= 3 && k >= rows * 0.7 ? n : null;
676
- }
677
- function circledAt(gray, w, h, ink, xs) {
678
- const { comps } = components(gray, w, h, ink);
679
- let charH = 0;
680
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
681
- const rings = charH < 10 ? [] : comps.filter((r) => {
682
- const rw = r.x1 - r.x0, rh = r.y1 - r.y0;
683
- 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);
684
- });
685
- return xs.map((x) => rings.some((r) => x >= r.x0 && x <= r.x1));
686
- }
687
- function quoteHead(gray, w, h, ink, cx) {
688
- const { comps, label } = components(gray, w, h, ink);
689
- let charH = 0;
690
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
691
- if (charH < 10) return null;
692
- const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
693
- if (!tall.length) return null;
694
- const mid = (Math.min(...tall.map((c) => c.y0)) + Math.max(...tall.map((c) => c.y1))) / 2;
695
- let best = null, bestD = charH * 0.4;
696
- for (const c of comps) {
697
- const ch = c.y1 - c.y0, d = Math.abs((c.x0 + c.x1) / 2 - cx);
698
- if (ch < charH * 0.12 || ch > charH * 0.5 || (c.y0 + c.y1) / 2 > mid || d > bestD) continue;
699
- bestD = d;
700
- best = c;
701
- }
702
- if (!best) return null;
703
- let n = 0, sy = 0;
704
- 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) {
705
- n++;
706
- sy += y;
707
- }
708
- return n ? (sy / n - best.y0) / (best.y1 - best.y0) : null;
709
- }
710
- function edgeTrim(gray, w, h, ink) {
711
- const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
712
- const colInk = new Uint32Array(w);
713
- for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (isInk(gray[y * w + x])) colInk[x]++;
714
- const ruleCol = new Uint8Array(w);
715
- let liveW = 0;
716
- for (let x = 0; x < w; x++) {
717
- if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
718
- else liveW++;
719
- }
720
- const blank = Math.max(1, Math.floor(w * 0.02));
721
- const bands = [];
722
- let start = -1;
723
- for (let y = 0; y <= h; y++) {
724
- let n = 0;
725
- if (y < h) {
726
- for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[y * w + x])) n++;
727
- }
728
- const on = y < h && n > blank && n < liveW * 0.85;
729
- if (on && start < 0) start = y;
730
- else if (!on && start >= 0) {
731
- bands.push([start, y]);
732
- start = -1;
733
- }
734
- }
735
- let y0 = 0, y1 = h;
736
- if (bands.length >= 2 && h >= 24) {
737
- const mh = Math.max(...bands.map(([a, b]) => b - a));
738
- const [top, next] = [bands[0], bands[1]], [bot, prev] = [bands[bands.length - 1], bands[bands.length - 2]];
739
- if (top[0] <= 1 && top[1] - top[0] < mh * 0.45) y0 = top[1] + next[0] >> 1;
740
- if (bot[1] >= h - 1 && bot[1] - bot[0] < mh * 0.45) y1 = bot[0] + prev[1] + 1 >> 1;
741
- if (y0 > 0 || y1 < h) {
742
- let runs = 0, len = 0;
743
- for (let x = 0; x <= w; x++) {
744
- let on = false;
745
- if (x < w && !ruleCol[x]) for (let y = y0; y < y1 && !on; y++) on = isInk(gray[y * w + x]);
746
- if (on) len++;
747
- else {
748
- if (len >= (y1 - y0) * 0.3) runs++;
749
- len = 0;
750
- }
751
- }
752
- if (runs < 2) {
753
- y0 = 0;
754
- y1 = h;
755
- }
756
- }
757
- }
758
- const textCol = (x) => {
759
- for (let y = y0; y < y1; y++) if (isInk(gray[y * w + x])) return true;
760
- return false;
761
- };
762
- const border = (x) => colInk[x] >= h * 0.95;
763
- const gap = Math.max(2, Math.round(h * 0.1));
764
- const clearFrom = (x, dir) => {
765
- for (let k = 0; k < gap; k++) {
766
- const c = x + dir * k;
767
- if (c < 0 || c >= w || textCol(c)) return false;
768
- }
769
- return true;
770
- };
771
- let x0 = 0, x1 = w;
772
- for (let x = 0; x < w * 0.2; x++) {
773
- if (border(x)) {
774
- let e = x;
775
- while (e + 1 < w && border(e + 1)) e++;
776
- if (clearFrom(e + 1, 1)) x0 = e + 1;
777
- break;
778
- }
779
- if (textCol(x)) break;
780
- }
781
- for (let x = w - 1; x >= w * 0.8; x--) {
782
- if (border(x)) {
783
- let e = x;
784
- while (e - 1 >= 0 && border(e - 1)) e--;
785
- if (clearFrom(e - 1, -1)) x1 = e;
786
- break;
787
- }
788
- if (textCol(x)) break;
789
- }
790
- return y0 > 0 || y1 < h || x0 > 0 || x1 < w ? { x0, x1, y0, y1 } : null;
791
- }
792
- function tallInkCount(gray, w, h, ink) {
793
- const { comps } = components(gray, w, h, ink);
794
- let charH = 0;
795
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
796
- const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.5).sort((a, b) => a.x0 - b.x0);
797
- let n = 0, end = -1;
798
- for (const c of tall) {
799
- if (c.x0 >= end) n++;
800
- end = Math.max(end, c.x1);
801
- }
802
- return n;
803
- }
804
- function gapGlyphs(gray, w, h, ink) {
805
- const { comps, label } = components(gray, w, h, ink);
806
- let charH = 0;
807
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
808
- if (charH < 10) return { glyphs: [], bandH: 0 };
809
- const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
810
- const bandT = Math.min(...tall.map((c) => c.y0)), bandB = Math.max(...tall.map((c) => c.y1)), bh = bandB - bandT;
811
- const inBand = (c) => c.y1 > bandT && c.y0 < bandB;
812
- const out = [];
813
- for (const c of comps) {
814
- if (!inBand(c)) continue;
815
- const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
816
- if (ch >= bh * 0.9 && cw <= bh * 0.15) continue;
817
- 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));
818
- const apart = !others.some((o) => o.x0 < c.x1 + 1 && o.x1 > c.x0 - 1);
819
- const nearlyApart = !others.some((o) => Math.min(o.x1, c.x1) - Math.max(o.x0, c.x0) > 2);
820
- const spans = [], runs = [];
821
- let n = 0;
822
- for (let y = c.y0; y < c.y1; y++) {
823
- let s = 0, r = 0, prev = false;
824
- for (let x = c.x0; x < c.x1; x++) {
825
- const on = label[y * w + x] === c.id;
826
- if (on) {
827
- s++;
828
- if (!prev) r++;
829
- }
830
- prev = on;
831
- }
832
- spans.push(s);
833
- runs.push(r);
834
- n += s;
835
- }
836
- const fill = n / (cw * ch), cy = ((c.y0 + c.y1) / 2 - bandT) / bh;
837
- const band = Math.max(1, Math.round(ch * 0.15));
838
- const top = Math.max(...spans.slice(0, band)) / cw, bottom = Math.max(...spans.slice(ch - band)) / cw;
839
- let mark = null;
840
- const speck = Math.max(3, bh * 0.08);
841
- const dot = ch <= bh * 0.25 && cw <= bh * 0.25 && ch >= speck && cw >= speck && cw >= ch * 0.6 && cw <= ch * 1.6;
842
- 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;
843
- if (!apart && !(nearlyApart && ch >= bh * 0.6)) {
844
- if (small) out.push({ x0: c.x0, x1: c.x1, mark: "\xB7" });
845
- continue;
846
- }
847
- if (!apart) {
848
- } else if (dot && fill >= 0.6 && cy >= 0.3 && cy <= 0.7) mark = "\xB7";
849
- else if (small) mark = "\xB7";
850
- else if (dot && fill >= 0.6 && cy >= 0.75 && cw >= ch * 0.8) mark = ".";
851
- 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";
852
- else if (ch <= bh * 0.15 && cw >= ch * 2 && cw <= bh * 0.8 && fill >= 0.7 && cy >= 0.35 && cy <= 0.75) mark = "-";
853
- 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";
854
- else if (ch >= bh * 0.3 && ch <= bh * 0.65 && fill < 0.5) {
855
- const stem = (from, to) => {
856
- let sx = 0, k = 0;
857
- for (let y = from; y < to; y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
858
- sx += x - c.x0;
859
- k++;
860
- }
861
- return k ? sx / k / cw : 0.5;
862
- };
863
- if (c.y0 <= bandT + bh * 0.15 && top >= 0.7 && bottom <= 0.5 && stem(c.y0 + band, c.y1) <= 0.35) mark = "\u300C";
864
- else if (c.y1 >= bandB - bh * 0.15 && bottom >= 0.7 && top <= 0.5 && stem(c.y0, c.y1 - band) >= 0.65) mark = "\u300D";
865
- } 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";
866
- if (!mark && ch >= bh * 0.6 && cw >= ch * 0.25 && cw <= ch * 0.7 && fill <= 0.4 && runs.every((r) => r <= 1)) {
867
- const mx = (y) => {
868
- let sx = 0, k = 0;
869
- for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
870
- sx += x;
871
- k++;
872
- }
873
- return k ? sx / k : NaN;
874
- };
875
- if (mx(c.y0 + band) - mx(c.y1 - 1 - band) >= cw * 0.5) mark = "/";
876
- }
877
- if (mark) out.push({ x0: c.x0, x1: c.x1, mark });
878
- }
879
- return { glyphs: out, bandH: bh };
880
- }
881
- function ringBullet(gray, w, h, ink, cx) {
882
- const { comps, label } = components(gray, w, h, ink);
883
- let charH = 0;
884
- for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
885
- if (charH < 10) return null;
886
- const tall = comps.filter((c2) => c2.y1 - c2.y0 >= charH * 0.6);
887
- const bandT = Math.min(...tall.map((c2) => c2.y0)), bh = Math.max(...tall.map((c2) => c2.y1)) - bandT;
888
- let best = null, bestD = bh * 0.6;
889
- for (const c2 of comps) {
890
- const d = Math.abs((c2.x0 + c2.x1) / 2 - cx);
891
- if (c2.y1 - c2.y0 >= bh * 0.3 && d < bestD) {
892
- bestD = d;
893
- best = c2;
894
- }
895
- }
896
- if (!best) return null;
897
- const c = best, cw = c.x1 - c.x0, ch = c.y1 - c.y0;
898
- if (cw < ch * 0.85 || cw > ch * 1.2) return null;
899
- const runs = (xs, at) => {
900
- let n2 = 0, len = 0, mx2 = 0, mn = Infinity;
901
- for (const k of [...xs, -1]) {
902
- if (k >= 0 && at(k)) {
903
- if (!len) n2++;
904
- len++;
905
- mx2 = Math.max(mx2, len);
906
- } else {
907
- if (len) mn = Math.min(mn, len);
908
- len = 0;
909
- }
910
- }
911
- return { n: n2, mx: mx2, mn };
912
- };
913
- const my = c.y0 + c.y1 >> 1, mx = c.x0 + c.x1 >> 1;
914
- const row = runs(Array.from({ length: cw }, (_, k) => c.x0 + k), (x) => label[my * w + x] === c.id);
915
- const col = runs(Array.from({ length: ch }, (_, k) => c.y0 + k), (y) => label[y * w + mx] === c.id);
916
- if (row.n !== 2 || col.n !== 2) return null;
917
- let n = 0;
918
- 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++;
919
- const fill = n / (cw * ch), rel = ch / bh;
920
- if (rel >= 0.62 && (fill <= 0.23 ? row.mx <= cw * 0.1 : fill <= 0.33 && row.mn <= cw * 0.08)) return "\u25CB";
921
- if (rel <= 0.58 && fill >= 0.24) return "\u3147";
922
- if (rel >= 0.6 && rel <= 0.8 && fill >= 0.3 && ((c.y0 + c.y1) / 2 - bandT) / bh >= 0.55) return "o";
923
- return null;
924
- }
925
- function discAt(gray, w, h, ink, cx) {
926
- const { comps, label } = components(gray, w, h, ink);
927
- let charH = 0;
928
- for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
929
- if (charH < 10) return false;
930
- const tall = comps.filter((c2) => c2.y1 - c2.y0 >= charH * 0.6);
931
- const bh = Math.max(...tall.map((c2) => c2.y1)) - Math.min(...tall.map((c2) => c2.y0));
932
- 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];
933
- if (!c) return false;
934
- const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
935
- if (ch < bh * 0.45 || ch > bh * 1.05 || cw < ch * 0.8 || cw > ch * 1.25) return false;
936
- let n = 0;
937
- 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++;
938
- const fill = n / (cw * ch);
939
- return fill >= 0.65 && fill <= 0.9;
940
- }
941
- function serifOne(gray, w, h, ink, cx) {
942
- const { comps, label } = components(gray, w, h, ink);
943
- let charH = 0;
944
- for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
945
- if (charH < 10) return false;
946
- 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];
947
- if (!c) return false;
948
- const ch = c.y1 - c.y0;
949
- const span = (a, b) => {
950
- let mn = Infinity, mx = -1;
951
- 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) {
952
- mn = Math.min(mn, x);
953
- mx = Math.max(mx, x);
954
- }
955
- return [mn, mx];
956
- };
957
- const [m0, m1] = span(0.4, 0.6), [t0, t1] = span(0, 0.2), [b0, b1] = span(0.8, 1);
958
- const sw = m1 - m0 + 1;
959
- const even = (l, r) => l >= sw * 0.4 && r >= sw * 0.4 && Math.abs(l - r) <= Math.max(l, r) * 0.35;
960
- return sw >= 2 && sw <= ch * 0.3 && even(m0 - t0, t1 - m1) && even(m0 - b0, b1 - m1);
961
- }
962
- function starRun(gray, w, h, ink, cx) {
963
- const { comps, label } = components(gray, w, h, ink);
964
- let charH = 0;
965
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
966
- if (charH < 10) return 0;
967
- const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
968
- const bandT = Math.min(...tall.map((c) => c.y0)), bh = Math.max(...tall.map((c) => c.y1)) - bandT;
969
- const star = (c) => {
970
- const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
971
- 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;
972
- let n = 0;
973
- 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++;
974
- const fill = n / (cw * ch);
975
- return fill >= 0.25 && fill <= 0.65;
976
- };
977
- const stars2 = comps.filter(star).sort((a, b) => a.x0 - b.x0);
978
- const k = stars2.findIndex((c) => cx >= c.x0 - (c.x1 - c.x0) && cx <= c.x1 + (c.x1 - c.x0));
979
- if (k < 0) return 0;
980
- let lo = k, hi = k;
981
- while (lo > 0 && stars2[lo].x0 - stars2[lo - 1].x1 <= stars2[lo].x1 - stars2[lo].x0) lo--;
982
- while (hi + 1 < stars2.length && stars2[hi + 1].x0 - stars2[hi].x1 <= stars2[hi].x1 - stars2[hi].x0) hi++;
983
- return hi - lo + 1;
984
- }
985
- var BRACKET_TAPER = 0.16;
986
- var BRACKET_SOLID = 0.4;
987
- var BRACKET_WIDE = 0.265;
988
- var BRACKET_LENS = 0.35;
989
- function bracketShape(f, close) {
990
- const [near, far] = close ? [f.bottom, f.top] : [f.top, f.bottom];
991
- const paren = close ? ")" : "(";
992
- if (near >= 0.7 && far <= 0.5) return f.taper < BRACKET_TAPER ? close ? "\u300D" : "\u300C" : paren;
993
- if (near >= 0.7 && far >= 0.7 && f.taper >= BRACKET_TAPER) {
994
- return f.fill >= BRACKET_SOLID && (f.cw >= f.ch * BRACKET_WIDE || f.taper >= BRACKET_LENS) ? close ? "\u3011" : "\u3010" : paren;
995
- }
996
- return null;
997
- }
998
- function leadingBullet(gray, w, h, ink, firstX) {
999
- const { comps, label } = components(gray, w, h, ink);
1000
- let charH = 0;
1001
- for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
1002
- if (charH < 10) return null;
1003
- const cand = comps.filter((c) => c.y1 - c.y0 >= charH * 0.15).sort((a, b) => a.x0 - b.x0);
1004
- const first = cand[0];
1005
- if (!first) return null;
1006
- const cl = cand.filter((c) => c.x0 < first.x1 && c.x1 > first.x0);
1007
- 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));
1008
- const cw = x1 - x0, ch = y1 - y0;
1009
- if (cw < ch * 0.8 || cw > ch * 1.25) return null;
1010
- const next = cand.find((c) => c.x0 >= x1);
1011
- if (next && next.x0 - x1 < ch * 0.3) return null;
1012
- let inkN = 0;
1013
- 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++;
1014
- const fill = inkN / (cw * ch);
1015
- const rel = ch / charH;
1016
- const ring2 = (c) => {
1017
- const a = c.x1 - c.x0, b = c.y1 - c.y0;
1018
- return a >= b * 0.75 && a <= b * 1.33;
1019
- };
1020
- 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));
1021
- const span = (y) => {
1022
- let n = 0;
1023
- for (let x = x0; x < x1; x++) if (cl.some((c) => label[y * w + x] === c.id)) n++;
1024
- return n;
1025
- };
1026
- const band = Math.max(1, Math.round(ch * 0.15));
1027
- const edgeFull = (from, dir) => {
1028
- let m = 0;
1029
- for (let k = 0; k < band; k++) m = Math.max(m, span(from + dir * k));
1030
- return m >= cw * 0.8;
1031
- };
1032
- let midRuns = 0;
1033
- {
1034
- let prev = false;
1035
- const my = y0 + (ch >> 1);
1036
- for (let x = x0; x < x1; x++) {
1037
- const on = cl.some((c) => label[my * w + x] === c.id);
1038
- if (on && !prev) midRuns++;
1039
- prev = on;
1040
- }
1041
- }
1042
- const hollowSquare = cl.length === 1 && fill < 0.5 && midRuns === 2 && edgeFull(y0, 1) && edgeFull(y1 - 1, -1);
1043
- let mark = null;
1044
- if (hollowSquare && rel >= 0.6 && rel <= 0.92) mark = "\u25A1";
1045
- else if (nested && fill < 0.45 && rel >= 0.6) mark = "\u25CE";
1046
- else if (cl.length === 1 && fill >= 0.9 && rel >= 0.28 && rel <= 0.6) mark = "\u25AA";
1047
- else if (cl.length === 1 && fill >= 0.65 && fill <= 0.88 && rel >= 0.5 && rel <= 0.8) mark = "\u25CF";
1048
- if (!mark) return null;
1049
- return { mark, covers: firstX <= x1 + ch * 0.3 };
1050
- }
1051
-
1052
372
  // src/ocr/glyph-restore.ts
373
+ var INKLESS_DELTA = 32;
1053
374
  function restoreGlyphs(rgba, pageW, box, text, steps, stepPx, note = {}) {
1054
375
  const chars = [...text];
1055
376
  if (chars.length !== steps.length) return text;
@@ -1060,11 +381,37 @@ function restoreGlyphs(rgba, pageW, box, text, steps, stepPx, note = {}) {
1060
381
  if (/(^|\D)[1-9](\D|$)/.test(text)) circled(c);
1061
382
  if (text.includes("*")) stars(c);
1062
383
  if (/[\u00b7\u2022]/.test(text)) discs(c);
384
+ if (/[-\u2013\u2014]/.test(text)) dashes(c);
1063
385
  gaps(c);
1064
386
  note.ringLead = ring(c);
1065
387
  if (/[I1Ⅰ-Ⅲ]|^\s*\./.test(text)) roman(c);
1066
388
  return chars.join("");
1067
389
  }
390
+ function trimInklessEnds(rgba, pageW, box, text, steps, stepPx) {
391
+ const chars = [...text], { w, h } = box;
392
+ if (chars.length !== steps.length || steps.some((s) => (s + 0.5) * stepPx > w + stepPx)) return { text, steps };
393
+ const gray = grayCrop(rgba, pageW, box);
394
+ const hist = new Uint32Array(256);
395
+ for (const v of gray) hist[v]++;
396
+ let bg = 0;
397
+ for (let n = 0; bg < 255 && (n += hist[bg]) < gray.length / 2; ) bg++;
398
+ const cols = new Uint32Array(w);
399
+ for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (Math.abs(gray[y * w + x] - bg) > INKLESS_DELTA) cols[x]++;
400
+ const half = Math.max(stepPx, h * 0.25);
401
+ const inkless = (i) => {
402
+ const cx = (steps[i] + 0.5) * stepPx;
403
+ for (let x = Math.max(0, Math.floor(cx - half)); x <= Math.min(w - 1, Math.ceil(cx + half)); x++) if (cols[x] && cols[x] < h * 0.9) return false;
404
+ return true;
405
+ };
406
+ const blank = (i) => !chars[i].trim();
407
+ const mark = (i) => /\p{P}/u.test(chars[i]);
408
+ let a = 0, b = chars.length;
409
+ while (a < b && (blank(a) || !mark(a) && inkless(a))) a++;
410
+ while (b > a && (blank(b - 1) || !mark(b - 1) && inkless(b - 1))) b--;
411
+ const cut = (from, to) => chars.slice(from, to).some((c) => c.trim());
412
+ if (a === b || !cut(0, a) && !cut(b, chars.length)) return { text, steps };
413
+ return { text: chars.slice(a, b).join(""), steps: steps.slice(a, b) };
414
+ }
1068
415
  function brackets({ chars, cx, gray, w, h, ink }) {
1069
416
  for (let i = 0; i < chars.length; i++) {
1070
417
  if (chars[i] !== "[" && chars[i] !== "]") continue;
@@ -1097,6 +444,16 @@ function quotes({ chars, cx, gray, w, h, ink }) {
1097
444
  }
1098
445
  }
1099
446
  }
447
+ function dashes({ chars, cx, gray, w, h, ink }) {
448
+ const head = chars.findIndex((c) => c.trim()), letters = chars.some((c) => /[가-힣A-Za-z]/.test(c));
449
+ for (let i = 0; i < chars.length; i++) {
450
+ if (!/^[-\u2013\u2014]$/.test(chars[i])) continue;
451
+ const r = dashRatio(gray, w, h, ink, cx(i));
452
+ if (r === null) continue;
453
+ if (r < 0.65) chars[i] = "-";
454
+ else if (r > 0.75 && i === head && letters) chars[i] = "\u2014";
455
+ }
456
+ }
1100
457
  function circled({ chars, cx, gray, w, h, ink }) {
1101
458
  const at = chars.flatMap((c, i) => /^[1-9]$/.test(c) && !/\d/.test(chars[i - 1] ?? "") && !/\d/.test(chars[i + 1] ?? "") ? [i] : []);
1102
459
  if (!at.length) return;
@@ -1548,7 +905,9 @@ var OcrEngine = class _OcrEngine {
1548
905
  if (!cur || score(job, r.confidence) > score(cur.job, cur.confidence)) best.set(job.group, { job, ...r });
1549
906
  });
1550
907
  let items = [];
1551
- for (const { job, text: read, confidence, steps, stepPx } of best.values()) {
908
+ for (const r of best.values()) {
909
+ const { job, confidence, stepPx } = r;
910
+ const { text: read, steps } = tuning.postprocess && job.rot === 0 ? trimInklessEnds(rgba, width, job.box, r.text, r.steps, stepPx) : r;
1552
911
  const note = {};
1553
912
  const raw = tuning.postprocess && job.rot === 0 ? restoreGlyphs(rgba, width, job.box, read, steps, stepPx, note) : read;
1554
913
  let text = tuning.postprocess ? restoreSymbols(raw.trim(), note.ringLead) : raw;
@@ -1584,7 +943,9 @@ var OcrEngine = class _OcrEngine {
1584
943
  if (text) joins[job.join].parts.push({ x: job.box.x, text, dotsBefore: job.dotsBefore === true, confidence });
1585
944
  continue;
1586
945
  }
1587
- items.push({ text, ...this.itemBox(rgba, width, job.box, tuning), confidence });
946
+ const item = { text, ...this.itemBox(rgba, width, job.box, tuning), confidence };
947
+ if (job.rot === 0) rememberOcrTextGaps(item, findOcrTextGaps(rgba, width, job.box, read, text, steps, stepPx));
948
+ items.push(item);
1588
949
  }
1589
950
  const leaderEnds = /* @__PURE__ */ new Map();
1590
951
  for (const j of joins) {
@@ -2111,7 +1472,11 @@ async function closerReads(rgba, rw, rh, pdfH, hi, regions, engine, tuning = REG
2111
1472
  const crop = new Uint8Array(cw * ch * 4);
2112
1473
  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);
2113
1474
  const items = await engine.recognizePage(crop, cw, ch, void 0, tuning);
2114
- out.push(items.map((it) => ({ ...it, x: (it.x + x0) / 2, y: (it.y + y0) / 2, w: it.w / 2, h: it.h / 2 })));
1475
+ out.push(items.map((it) => {
1476
+ const placed = { ...it, x: (it.x + x0) / 2, y: (it.y + y0) / 2, w: it.w / 2, h: it.h / 2 };
1477
+ copyOcrTextGaps(it, placed, 0.5, x0 / 2);
1478
+ return placed;
1479
+ }));
2115
1480
  }
2116
1481
  return out;
2117
1482
  }
@@ -2176,7 +1541,7 @@ function ocrItemsToBlocks(items, pageNumber, pdfW, pdfH, scale, extraLines, dete
2176
1541
  }
2177
1542
  const norm = items.map((it, i) => {
2178
1543
  const h = (figureRegion ? Math.max(1, bottoms[i] - it.y) : it.h) / scale;
2179
- return {
1544
+ const normalized = {
2180
1545
  text: it.text,
2181
1546
  x: Math.round(xs[i].x / scale),
2182
1547
  // NormItem.y 는 pdfjs transform[5] = 베이스라인 (bottom-up) — 잉크 하단으로 근사
@@ -2189,6 +1554,8 @@ function ocrItemsToBlocks(items, pageNumber, pdfW, pdfH, scale, extraLines, dete
2189
1554
  isHidden: false,
2190
1555
  ...xs[i].space ? { hasSpaceBefore: true } : {}
2191
1556
  };
1557
+ copyOcrTextGaps(it, normalized, 1 / scale);
1558
+ return normalized;
2192
1559
  });
2193
1560
  return extractPageBlocksWithLines(norm, pageNumber, opList, pdfW, pdfH, extraLines, detectTables);
2194
1561
  }
@@ -2243,4 +1610,4 @@ export {
2243
1610
  groupOcrLines,
2244
1611
  ocrItemsToBlocks
2245
1612
  };
2246
- //# sourceMappingURL=chunk-BZW2WS44.js.map
1613
+ //# sourceMappingURL=chunk-GMKFXPEI.js.map