kordoc 4.24.6 → 4.24.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/{-7J7Y3XPH.js → -TLCDUU22.js} +11 -11
- package/dist/{chunk-TZVVK5MI.js → chunk-5MQG3GJI.js} +2 -2
- package/dist/{chunk-JIU7E7Z6.cjs → chunk-73MRAQXA.cjs} +1098 -209
- package/dist/chunk-73MRAQXA.cjs.map +1 -0
- package/dist/{chunk-ISTIOEIL.cjs → chunk-7F35EPMM.cjs} +3 -3
- package/dist/{chunk-ISTIOEIL.cjs.map → chunk-7F35EPMM.cjs.map} +1 -1
- package/dist/{chunk-GI5LWQ3S.js → chunk-7JUF6G6Z.js} +2 -2
- package/dist/{chunk-BBKRJE57.js → chunk-7XPTKW76.js} +2 -2
- package/dist/{chunk-XM2Y4DB5.js → chunk-BJABLQMB.js} +2 -2
- package/dist/{chunk-2JDVPQ33.cjs → chunk-C3BOYTY5.cjs} +2 -2
- package/dist/{chunk-2JDVPQ33.cjs.map → chunk-C3BOYTY5.cjs.map} +1 -1
- package/dist/{chunk-EVBSSU3N.js → chunk-CB57N2MG.js} +2 -2
- package/dist/{chunk-WNOWRBM4.js → chunk-D3EHIHYN.js} +2 -2
- package/dist/{chunk-GYX26HZR.js → chunk-DPB6ZVQ5.js} +927 -38
- package/dist/chunk-DPB6ZVQ5.js.map +1 -0
- package/dist/{chunk-DFRQD254.js → chunk-F7UWIMUA.js} +2 -2
- package/dist/{chunk-BKTEGPEO.js → chunk-FSWJC5RR.js} +3 -3
- package/dist/{chunk-W75FRCAP.js → chunk-IT5XO3UG.js} +926 -37
- package/dist/chunk-IT5XO3UG.js.map +1 -0
- package/dist/{chunk-HAMIOQYO.js → chunk-J7MFKS73.js} +2 -2
- package/dist/{chunk-435ECXNG.cjs → chunk-JM7PZM6E.cjs} +3 -3
- package/dist/{chunk-435ECXNG.cjs.map → chunk-JM7PZM6E.cjs.map} +1 -1
- package/dist/{chunk-G343MTBX.js → chunk-K2FGQ5D5.js} +43 -15
- package/dist/chunk-K2FGQ5D5.js.map +1 -0
- package/dist/{chunk-BZW2WS44.js → chunk-KA6JZZIW.js} +83 -716
- package/dist/chunk-KA6JZZIW.js.map +1 -0
- package/dist/{chunk-RMUDX5Q2.js → chunk-N2XN6GAQ.js} +2 -2
- package/dist/{chunk-XVTGFK2F.js → chunk-NE6G6LCP.js} +2 -2
- package/dist/{chunk-AM5ALISV.cjs → chunk-NW2CKOFL.cjs} +6 -6
- package/dist/{chunk-AM5ALISV.cjs.map → chunk-NW2CKOFL.cjs.map} +1 -1
- package/dist/{chunk-5RZBLPVP.js → chunk-OUJIV2AM.js} +4 -4
- package/dist/chunk-OUJIV2AM.js.map +1 -0
- package/dist/{chunk-VMRH6UVA.js → chunk-RCDRTQ37.js} +2 -2
- package/dist/{chunk-KR4UKOX2.js → chunk-RCT5MO53.js} +6 -6
- package/dist/{chunk-3ZLS54V4.js → chunk-TDB27K2Z.js} +2 -2
- package/dist/{chunk-EBJWBGWF.js → chunk-TTUAJYTC.js} +83 -716
- package/dist/chunk-TTUAJYTC.js.map +1 -0
- package/dist/{chunk-5XBZJ3O4.js → chunk-WTSID6AI.js} +2 -2
- package/dist/{chunk-PKCOOSDT.js → chunk-XOUZC7MX.js} +2 -2
- package/dist/{chunk-TLJJF3J6.js → chunk-Z5DT55TW.js} +3 -3
- package/dist/{chunk-J4XCQNVH.cjs → chunk-ZJYNTA5R.cjs} +141 -774
- package/dist/chunk-ZJYNTA5R.cjs.map +1 -0
- package/dist/chunks-S2GGFN6W.js +10 -0
- package/dist/cli.js +30 -30
- package/dist/{formula-DKNZKJ5Z.js → formula-DZMBNPWT.js} +4 -4
- package/dist/{formula-T7D5JIZR.js → formula-PNMQ5JFJ.js} +3 -3
- package/dist/{formula-D7LBREWP.cjs → formula-ZLNSVWS6.cjs} +9 -9
- package/dist/{formula-D7LBREWP.cjs.map → formula-ZLNSVWS6.cjs.map} +1 -1
- package/dist/{image-ocr-5POXVGN2.js → image-ocr-BPH25SFW.js} +9 -9
- package/dist/{image-ocr-7NCSIZGA.js → image-ocr-P5Q6YXTO.js} +6 -6
- package/dist/{image-ocr-N3I3MUKF.cjs → image-ocr-YUXCVOOP.cjs} +17 -17
- package/dist/{image-ocr-N3I3MUKF.cjs.map → image-ocr-YUXCVOOP.cjs.map} +1 -1
- package/dist/index.cjs +729 -701
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +38 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +23 -23
- package/dist/{model-bundle-XT7G2IJU.js → model-bundle-XAM6JFZV.js} +5 -5
- package/dist/{models-AB2VO45L.js → models-6Q5ULSTP.js} +5 -5
- package/dist/{parse-worker-v2-YSSAUPWU.js → parse-worker-v2-ONX5QLJZ.js} +12 -12
- package/dist/{parser-KGDUPSA7.js → parser-54R7ENH6.js} +9 -9
- package/dist/{parser-ES3I54QL.js → parser-JHXN4S6L.js} +12 -12
- package/dist/{parser-AGDY3ZOA.cjs → parser-Z352BN56.cjs} +159 -159
- package/dist/{parser-AGDY3ZOA.cjs.map → parser-Z352BN56.cjs.map} +1 -1
- package/dist/{pdf-ocr-53WUTX6H.js → pdf-ocr-FQ4LAXM6.js} +9 -9
- package/dist/{pdf-ocr-7QSP7JLG.js → pdf-ocr-LFVER7IE.js} +6 -6
- package/dist/pdf-ocr-SMFFJYTD.cjs +31 -0
- package/dist/{pdf-ocr-X6OAHK46.cjs.map → pdf-ocr-SMFFJYTD.cjs.map} +1 -1
- package/dist/{profile-io-GGEJ3MZW.js → profile-io-YW5QWKBB.js} +3 -3
- package/dist/{rasterize-YXICEI5S.js → rasterize-T4S3NMQ7.js} +3 -3
- package/dist/{redact-doc-OYXBNTWI.js → redact-doc-CAVBR5XO.js} +11 -11
- package/dist/{render-4PBNDLK2.js → render-ZW52ZX6R.js} +7 -7
- package/dist/seal-TQ3RLVVT.js +10 -0
- package/dist/{setup-WHTMPK74.js → setup-7UGSJPJP.js} +3 -3
- package/dist/visual-6GXHKPCJ.js +21 -0
- package/dist/{watch-P6UWW52D.js → watch-M4A7B5WS.js} +12 -12
- package/package.json +4 -4
- package/dist/chunk-5RZBLPVP.js.map +0 -1
- package/dist/chunk-BZW2WS44.js.map +0 -1
- package/dist/chunk-EBJWBGWF.js.map +0 -1
- package/dist/chunk-G343MTBX.js.map +0 -1
- package/dist/chunk-GYX26HZR.js.map +0 -1
- package/dist/chunk-J4XCQNVH.cjs.map +0 -1
- package/dist/chunk-JIU7E7Z6.cjs.map +0 -1
- package/dist/chunk-W75FRCAP.js.map +0 -1
- package/dist/chunks-3MWJ7HOZ.js +0 -10
- package/dist/pdf-ocr-X6OAHK46.cjs +0 -31
- package/dist/seal-KJYAU23C.js +0 -10
- package/dist/visual-PHKJTSFR.js +0 -21
- /package/dist/{-7J7Y3XPH.js.map → -TLCDUU22.js.map} +0 -0
- /package/dist/{chunk-TZVVK5MI.js.map → chunk-5MQG3GJI.js.map} +0 -0
- /package/dist/{chunk-GI5LWQ3S.js.map → chunk-7JUF6G6Z.js.map} +0 -0
- /package/dist/{chunk-BBKRJE57.js.map → chunk-7XPTKW76.js.map} +0 -0
- /package/dist/{chunk-XM2Y4DB5.js.map → chunk-BJABLQMB.js.map} +0 -0
- /package/dist/{chunk-EVBSSU3N.js.map → chunk-CB57N2MG.js.map} +0 -0
- /package/dist/{chunk-WNOWRBM4.js.map → chunk-D3EHIHYN.js.map} +0 -0
- /package/dist/{chunk-DFRQD254.js.map → chunk-F7UWIMUA.js.map} +0 -0
- /package/dist/{chunk-BKTEGPEO.js.map → chunk-FSWJC5RR.js.map} +0 -0
- /package/dist/{chunk-HAMIOQYO.js.map → chunk-J7MFKS73.js.map} +0 -0
- /package/dist/{chunk-RMUDX5Q2.js.map → chunk-N2XN6GAQ.js.map} +0 -0
- /package/dist/{chunk-XVTGFK2F.js.map → chunk-NE6G6LCP.js.map} +0 -0
- /package/dist/{chunk-VMRH6UVA.js.map → chunk-RCDRTQ37.js.map} +0 -0
- /package/dist/{chunk-KR4UKOX2.js.map → chunk-RCT5MO53.js.map} +0 -0
- /package/dist/{chunk-3ZLS54V4.js.map → chunk-TDB27K2Z.js.map} +0 -0
- /package/dist/{chunk-5XBZJ3O4.js.map → chunk-WTSID6AI.js.map} +0 -0
- /package/dist/{chunk-PKCOOSDT.js.map → chunk-XOUZC7MX.js.map} +0 -0
- /package/dist/{chunk-TLJJF3J6.js.map → chunk-Z5DT55TW.js.map} +0 -0
- /package/dist/{chunks-3MWJ7HOZ.js.map → chunks-S2GGFN6W.js.map} +0 -0
- /package/dist/{formula-DKNZKJ5Z.js.map → formula-DZMBNPWT.js.map} +0 -0
- /package/dist/{formula-T7D5JIZR.js.map → formula-PNMQ5JFJ.js.map} +0 -0
- /package/dist/{image-ocr-5POXVGN2.js.map → image-ocr-BPH25SFW.js.map} +0 -0
- /package/dist/{image-ocr-7NCSIZGA.js.map → image-ocr-P5Q6YXTO.js.map} +0 -0
- /package/dist/{model-bundle-XT7G2IJU.js.map → model-bundle-XAM6JFZV.js.map} +0 -0
- /package/dist/{models-AB2VO45L.js.map → models-6Q5ULSTP.js.map} +0 -0
- /package/dist/{parse-worker-v2-YSSAUPWU.js.map → parse-worker-v2-ONX5QLJZ.js.map} +0 -0
- /package/dist/{parser-KGDUPSA7.js.map → parser-54R7ENH6.js.map} +0 -0
- /package/dist/{parser-ES3I54QL.js.map → parser-JHXN4S6L.js.map} +0 -0
- /package/dist/{pdf-ocr-53WUTX6H.js.map → pdf-ocr-FQ4LAXM6.js.map} +0 -0
- /package/dist/{pdf-ocr-7QSP7JLG.js.map → pdf-ocr-LFVER7IE.js.map} +0 -0
- /package/dist/{profile-io-GGEJ3MZW.js.map → profile-io-YW5QWKBB.js.map} +0 -0
- /package/dist/{rasterize-YXICEI5S.js.map → rasterize-T4S3NMQ7.js.map} +0 -0
- /package/dist/{redact-doc-OYXBNTWI.js.map → redact-doc-CAVBR5XO.js.map} +0 -0
- /package/dist/{render-4PBNDLK2.js.map → render-ZW52ZX6R.js.map} +0 -0
- /package/dist/{seal-KJYAU23C.js.map → seal-TQ3RLVVT.js.map} +0 -0
- /package/dist/{setup-WHTMPK74.js.map → setup-7UGSJPJP.js.map} +0 -0
- /package/dist/{visual-PHKJTSFR.js.map → visual-6GXHKPCJ.js.map} +0 -0
- /package/dist/{watch-P6UWW52D.js.map → watch-M4A7B5WS.js.map} +0 -0
|
@@ -5,15 +5,15 @@ import {
|
|
|
5
5
|
HEADING_RATIO_H3,
|
|
6
6
|
tidyScriptTags,
|
|
7
7
|
toTex
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-D3EHIHYN.js";
|
|
9
9
|
import {
|
|
10
10
|
ensureModelsIn,
|
|
11
11
|
getModelsDir
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-NE6G6LCP.js";
|
|
13
13
|
import {
|
|
14
14
|
safeMax,
|
|
15
15
|
safeMin
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-F7UWIMUA.js";
|
|
17
17
|
|
|
18
18
|
// src/ocr/models.ts
|
|
19
19
|
import { join } from "path";
|
|
@@ -146,6 +146,773 @@ function ocrModelPath(spec) {
|
|
|
146
146
|
return join(getOcrModelsDir(), spec.filename);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// src/ocr/line-split.ts
|
|
150
|
+
function grayCrop(rgba, pageW, box) {
|
|
151
|
+
const out = new Uint8Array(box.w * box.h);
|
|
152
|
+
for (let y = 0; y < box.h; y++) {
|
|
153
|
+
let si = ((box.y + y) * pageW + box.x) * 4;
|
|
154
|
+
let di = y * box.w;
|
|
155
|
+
for (let x = 0; x < box.w; x++, si += 4, di++) {
|
|
156
|
+
out[di] = rgba[si] * 77 + rgba[si + 1] * 150 + rgba[si + 2] * 29 >> 8;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function inkStats(gray) {
|
|
162
|
+
const hist = new Uint32Array(256);
|
|
163
|
+
for (let i = 0; i < gray.length; i++) hist[gray[i]]++;
|
|
164
|
+
const total = gray.length;
|
|
165
|
+
let sumAll = 0;
|
|
166
|
+
for (let v = 0; v < 256; v++) sumAll += v * hist[v];
|
|
167
|
+
let wB = 0, sumB = 0, best = -1, thr = 127;
|
|
168
|
+
for (let t = 0; t < 256; t++) {
|
|
169
|
+
wB += hist[t];
|
|
170
|
+
if (wB === 0) continue;
|
|
171
|
+
const wF = total - wB;
|
|
172
|
+
if (wF === 0) break;
|
|
173
|
+
sumB += t * hist[t];
|
|
174
|
+
const mB = sumB / wB;
|
|
175
|
+
const mF = (sumAll - sumB) / wF;
|
|
176
|
+
const between = wB * wF * (mB - mF) * (mB - mF);
|
|
177
|
+
if (between > best) {
|
|
178
|
+
best = between;
|
|
179
|
+
thr = t;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let nDark = 0, sDark = 0;
|
|
183
|
+
for (let v = 0; v <= thr; v++) {
|
|
184
|
+
nDark += hist[v];
|
|
185
|
+
sDark += v * hist[v];
|
|
186
|
+
}
|
|
187
|
+
const nLight = total - nDark;
|
|
188
|
+
const sLight = sumAll - sDark;
|
|
189
|
+
if (nDark === 0 || nLight === 0) return { threshold: thr, darkInk: true, contrast: 0, inkRatio: 0 };
|
|
190
|
+
const darkInk = nDark <= nLight;
|
|
191
|
+
return {
|
|
192
|
+
threshold: thr,
|
|
193
|
+
darkInk,
|
|
194
|
+
contrast: sLight / nLight - sDark / nDark,
|
|
195
|
+
inkRatio: (darkInk ? nDark : nLight) / total
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function splitRowBands(gray, w, h, ink, minBandRatio) {
|
|
199
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
200
|
+
const colInk = new Uint32Array(w);
|
|
201
|
+
for (let y = 0; y < h; y++) {
|
|
202
|
+
const off = y * w;
|
|
203
|
+
for (let x = 0; x < w; x++) if (isInk(gray[off + x])) colInk[x]++;
|
|
204
|
+
}
|
|
205
|
+
const ruleCol = new Uint8Array(w);
|
|
206
|
+
let liveW = 0;
|
|
207
|
+
for (let x = 0; x < w; x++) {
|
|
208
|
+
if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
|
|
209
|
+
else liveW++;
|
|
210
|
+
}
|
|
211
|
+
const rowInk = new Uint32Array(h);
|
|
212
|
+
for (let y = 0; y < h; y++) {
|
|
213
|
+
let n = 0;
|
|
214
|
+
const off = y * w;
|
|
215
|
+
for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) n++;
|
|
216
|
+
rowInk[y] = n >= liveW * 0.85 ? 0 : n;
|
|
217
|
+
}
|
|
218
|
+
const blank = Math.max(1, Math.floor(w * 0.02));
|
|
219
|
+
let bands = [];
|
|
220
|
+
let start = -1;
|
|
221
|
+
for (let y = 0; y <= h; y++) {
|
|
222
|
+
const on = y < h && rowInk[y] > blank;
|
|
223
|
+
if (on && start < 0) start = y;
|
|
224
|
+
else if (!on && start >= 0) {
|
|
225
|
+
bands.push([start, y]);
|
|
226
|
+
start = -1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const inkCols = (y0, y1) => {
|
|
230
|
+
let x0 = w, x1 = -1;
|
|
231
|
+
for (let y = y0; y < y1; y++) {
|
|
232
|
+
const off = y * w;
|
|
233
|
+
for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[off + x])) {
|
|
234
|
+
if (x < x0) x0 = x;
|
|
235
|
+
if (x > x1) x1 = x;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return x1 >= x0 ? [x0, x1 + 1] : [0, w];
|
|
239
|
+
};
|
|
240
|
+
if (bands.length <= 1) return [{ y0: bands[0]?.[0] ?? 0, y1: bands[0]?.[1] ?? h, x0: 0, x1: w }];
|
|
241
|
+
const widths = bands.map(([y0, y1]) => {
|
|
242
|
+
const [a, b] = inkCols(y0, y1);
|
|
243
|
+
return b - a;
|
|
244
|
+
}).sort((a, b) => a - b);
|
|
245
|
+
const charSize = widths[widths.length >> 1];
|
|
246
|
+
const minBand = Math.max(3, minBandRatio * charSize);
|
|
247
|
+
for (let i = 0; i + 1 < bands.length; ) {
|
|
248
|
+
const gap = bands[i + 1][0] - bands[i][1];
|
|
249
|
+
if (gap <= Math.max(2, charSize * 0.15) && bands[i + 1][1] - bands[i][0] <= charSize * 1.3) {
|
|
250
|
+
bands.splice(i, 2, [bands[i][0], bands[i + 1][1]]);
|
|
251
|
+
} else i++;
|
|
252
|
+
}
|
|
253
|
+
while (bands.length > 1 && bands[0][0] <= 1 && bands[0][1] - bands[0][0] < minBand) bands.shift();
|
|
254
|
+
while (bands.length > 1 && bands[bands.length - 1][1] >= h - 1 && bands[bands.length - 1][1] - bands[bands.length - 1][0] < minBand) bands.pop();
|
|
255
|
+
for (; ; ) {
|
|
256
|
+
let idx = -1, minH = Infinity;
|
|
257
|
+
for (let i = 0; i < bands.length; i++) {
|
|
258
|
+
const bh = bands[i][1] - bands[i][0];
|
|
259
|
+
if (bh < minBand && bh < minH) {
|
|
260
|
+
minH = bh;
|
|
261
|
+
idx = i;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (idx < 0 || bands.length === 1) break;
|
|
265
|
+
const gapPrev = idx > 0 ? bands[idx][0] - bands[idx - 1][1] : Infinity;
|
|
266
|
+
const gapNext = idx < bands.length - 1 ? bands[idx + 1][0] - bands[idx][1] : Infinity;
|
|
267
|
+
const j = gapPrev <= gapNext ? idx - 1 : idx + 1;
|
|
268
|
+
const a = Math.min(idx, j), b = Math.max(idx, j);
|
|
269
|
+
bands = [...bands.slice(0, a), [bands[a][0], bands[b][1]], ...bands.slice(b + 1)];
|
|
270
|
+
}
|
|
271
|
+
if (bands.length === 1) return [{ y0: bands[0][0], y1: bands[0][1], x0: 0, x1: w }];
|
|
272
|
+
return bands.map(([y0, y1]) => {
|
|
273
|
+
const [x0, x1] = inkCols(y0, y1);
|
|
274
|
+
return { y0, y1, x0, x1 };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function components(gray, w, h, ink) {
|
|
278
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
279
|
+
const comps = [];
|
|
280
|
+
const label = new Int32Array(w * h);
|
|
281
|
+
const stack = [];
|
|
282
|
+
for (let p0 = 0; p0 < w * h; p0++) {
|
|
283
|
+
if (label[p0] || !isInk(gray[p0])) continue;
|
|
284
|
+
const c = { x0: w, x1: 0, y0: h, y1: 0, id: comps.length + 1 };
|
|
285
|
+
label[p0] = c.id;
|
|
286
|
+
stack.push(p0);
|
|
287
|
+
while (stack.length) {
|
|
288
|
+
const p = stack.pop();
|
|
289
|
+
const x = p % w, y = p / w | 0;
|
|
290
|
+
if (x < c.x0) c.x0 = x;
|
|
291
|
+
if (x + 1 > c.x1) c.x1 = x + 1;
|
|
292
|
+
if (y < c.y0) c.y0 = y;
|
|
293
|
+
if (y + 1 > c.y1) c.y1 = y + 1;
|
|
294
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
295
|
+
const nx = x + dx, ny = y + dy;
|
|
296
|
+
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
|
|
297
|
+
const q = ny * w + nx;
|
|
298
|
+
if (!label[q] && isInk(gray[q])) {
|
|
299
|
+
label[q] = c.id;
|
|
300
|
+
stack.push(q);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (c.y1 - c.y0 < h * 0.85 && c.x1 - c.x0 < w * 0.85) comps.push(c);
|
|
305
|
+
}
|
|
306
|
+
return { comps, label };
|
|
307
|
+
}
|
|
308
|
+
function leadingTriangle(gray, w, h, ink) {
|
|
309
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
310
|
+
let charH = 0;
|
|
311
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
312
|
+
const first = comps.filter((c) => c.y1 - c.y0 >= charH * 0.4).sort((a, b) => a.x0 - b.x0)[0];
|
|
313
|
+
if (!first || charH < 8) return null;
|
|
314
|
+
const cw = first.x1 - first.x0, ch = first.y1 - first.y0;
|
|
315
|
+
if (cw < ch * 0.8 || cw > ch * 1.8) return null;
|
|
316
|
+
const rowSpan = (y) => {
|
|
317
|
+
let n = 0, lo = -1, hi = -1, runs = 0, prev = false;
|
|
318
|
+
for (let x = first.x0; x < first.x1; x++) {
|
|
319
|
+
const on = label[y * w + x] === first.id;
|
|
320
|
+
if (on) {
|
|
321
|
+
n++;
|
|
322
|
+
if (lo < 0) lo = x - first.x0;
|
|
323
|
+
hi = x - first.x0;
|
|
324
|
+
if (!prev) runs++;
|
|
325
|
+
}
|
|
326
|
+
prev = on;
|
|
327
|
+
}
|
|
328
|
+
return { n, lo, hi, runs };
|
|
329
|
+
};
|
|
330
|
+
const base = Math.max(rowSpan(first.y1 - 1).n, rowSpan(first.y1 - 2).n);
|
|
331
|
+
if (base < cw * 0.7) return null;
|
|
332
|
+
for (let y = first.y0; y < first.y0 + Math.max(1, Math.round(ch * 0.2)); y++) {
|
|
333
|
+
const r = rowSpan(y);
|
|
334
|
+
if (r.n === 0) continue;
|
|
335
|
+
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;
|
|
336
|
+
}
|
|
337
|
+
const mid = rowSpan(first.y0 + Math.round(ch * 0.6));
|
|
338
|
+
if (mid.lo < 0 || mid.lo > cw * 0.3 || mid.hi < cw * 0.7) return null;
|
|
339
|
+
return mid.runs >= 2 ? "\u25B3" : "\u25B2";
|
|
340
|
+
}
|
|
341
|
+
function leaderRuns(gray, w, h, ink, minDots) {
|
|
342
|
+
const comps = components(gray, w, h, ink).comps.sort((a, b) => a.x0 - b.x0);
|
|
343
|
+
const blobs = [];
|
|
344
|
+
for (const c of comps) {
|
|
345
|
+
const last = blobs[blobs.length - 1];
|
|
346
|
+
if (last && c.x0 < last.x1) {
|
|
347
|
+
last.x1 = Math.max(last.x1, c.x1);
|
|
348
|
+
last.y0 = Math.min(last.y0, c.y0);
|
|
349
|
+
last.y1 = Math.max(last.y1, c.y1);
|
|
350
|
+
} else blobs.push({ x0: c.x0, x1: c.x1, y0: c.y0, y1: c.y1 });
|
|
351
|
+
}
|
|
352
|
+
let charH = 0;
|
|
353
|
+
for (const b of blobs) charH = Math.max(charH, b.y1 - b.y0);
|
|
354
|
+
if (charH < 8) return [];
|
|
355
|
+
const small = (c) => c.x1 - c.x0 <= charH * 0.3 && c.y1 - c.y0 <= charH * 0.3;
|
|
356
|
+
let bandTop = h, bandBot = 0;
|
|
357
|
+
for (const c of comps) if (!small(c)) {
|
|
358
|
+
bandTop = Math.min(bandTop, c.y0);
|
|
359
|
+
bandBot = Math.max(bandBot, c.y1);
|
|
360
|
+
}
|
|
361
|
+
const cy = (c) => (c.y0 + c.y1) / 2;
|
|
362
|
+
const isDot = (b) => small(b) && cy(b) >= bandTop && cy(b) <= bandBot;
|
|
363
|
+
const runs = [];
|
|
364
|
+
for (let i = 0; i < blobs.length; ) {
|
|
365
|
+
if (!isDot(blobs[i])) {
|
|
366
|
+
i++;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
let j = i;
|
|
370
|
+
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++;
|
|
371
|
+
const x0 = blobs[i].x0, x1 = blobs[j].x1, y = cy(blobs[i]);
|
|
372
|
+
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));
|
|
373
|
+
const run = blobs.slice(i, j + 1).filter((b) => b.x0 > 0 && b.x1 < w);
|
|
374
|
+
const spread = (xs) => {
|
|
375
|
+
const v = [...xs].sort((a, b) => a - b).slice(xs.length >= 5 ? 1 : 0, xs.length >= 5 ? -1 : void 0);
|
|
376
|
+
return v[v.length - 1] / Math.max(1, v[0]);
|
|
377
|
+
};
|
|
378
|
+
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;
|
|
379
|
+
if (j - i + 1 >= minDots && onLine && even) runs.push([x0, x1]);
|
|
380
|
+
i = j + 1;
|
|
381
|
+
}
|
|
382
|
+
return runs;
|
|
383
|
+
}
|
|
384
|
+
function inkBounds(gray, w, h, ink) {
|
|
385
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
386
|
+
const colInk = new Uint32Array(w);
|
|
387
|
+
const rowInk = new Uint32Array(h);
|
|
388
|
+
for (let y = 0; y < h; y++) {
|
|
389
|
+
const off = y * w;
|
|
390
|
+
for (let x = 0; x < w; x++) if (isInk(gray[off + x])) {
|
|
391
|
+
colInk[x]++;
|
|
392
|
+
rowInk[y]++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const ruleCol = new Uint8Array(w), ruleRow = new Uint8Array(h);
|
|
396
|
+
for (let x = 0; x < w; x++) if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
|
|
397
|
+
for (let y = 0; y < h; y++) if (rowInk[y] >= w * 0.85) ruleRow[y] = 1;
|
|
398
|
+
let x0 = w, x1 = -1, y0 = h, y1 = -1;
|
|
399
|
+
for (let y = 0; y < h; y++) {
|
|
400
|
+
if (ruleRow[y]) continue;
|
|
401
|
+
const off = y * w;
|
|
402
|
+
for (let x = 0; x < w; x++) {
|
|
403
|
+
if (ruleCol[x] || !isInk(gray[off + x])) continue;
|
|
404
|
+
if (x < x0) x0 = x;
|
|
405
|
+
if (x > x1) x1 = x;
|
|
406
|
+
if (y < y0) y0 = y;
|
|
407
|
+
if (y > y1) y1 = y;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (x1 < x0 || y1 < y0) return { x0: 0, y0: 0, x1: w, y1: h };
|
|
411
|
+
return { x0, y0, x1: x1 + 1, y1: y1 + 1 };
|
|
412
|
+
}
|
|
413
|
+
function bracketFeatures(gray, w, h, ink, cx) {
|
|
414
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
415
|
+
let charH = 0;
|
|
416
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
417
|
+
if (charH < 8) return null;
|
|
418
|
+
const tol = Math.max(2, charH * 0.3);
|
|
419
|
+
let best = null, bestD = Infinity;
|
|
420
|
+
for (const c of comps) {
|
|
421
|
+
if (c.y1 - c.y0 < charH * 0.3 || c.x1 < cx - tol || c.x0 > cx + tol) continue;
|
|
422
|
+
const d = Math.abs((c.x0 + c.x1) / 2 - cx);
|
|
423
|
+
if (d < bestD) {
|
|
424
|
+
bestD = d;
|
|
425
|
+
best = c;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (!best) return null;
|
|
429
|
+
const b = best;
|
|
430
|
+
const cw = b.x1 - b.x0, ch = b.y1 - b.y0;
|
|
431
|
+
if (cw < 3 || ch < 8) return null;
|
|
432
|
+
const spans = [];
|
|
433
|
+
for (let y = b.y0; y < b.y1; y++) {
|
|
434
|
+
let n = 0;
|
|
435
|
+
for (let x = b.x0; x < b.x1; x++) if (label[y * w + x] === b.id) n++;
|
|
436
|
+
spans.push(n);
|
|
437
|
+
}
|
|
438
|
+
const band = Math.max(1, Math.round(ch * 0.15));
|
|
439
|
+
const top = Math.max(...spans.slice(0, band)) / cw, bottom = Math.max(...spans.slice(ch - band)) / cw;
|
|
440
|
+
const midRows = spans.slice(Math.floor(ch * 0.3), Math.ceil(ch * 0.7)).sort((p, q) => p - q);
|
|
441
|
+
const stroke = midRows[midRows.length >> 1] ?? 0;
|
|
442
|
+
const taper = spans.filter((n) => n > stroke && n < cw).length / ch;
|
|
443
|
+
const fill = spans.reduce((a, n) => a + n, 0) / (cw * ch);
|
|
444
|
+
return { top, bottom, taper, fill, cw, ch, charH };
|
|
445
|
+
}
|
|
446
|
+
function romanStems(gray, w, h, ink, prevX, nextX) {
|
|
447
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
448
|
+
let charH = 0;
|
|
449
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
450
|
+
if (charH < 10) return null;
|
|
451
|
+
const x0 = prevX + charH * 0.35, x1 = nextX - charH * 0.35;
|
|
452
|
+
const sel = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6 && (c.x0 + c.x1) / 2 >= x0 && (c.x0 + c.x1) / 2 < x1);
|
|
453
|
+
if (!sel.length) return null;
|
|
454
|
+
const ids = new Set(sel.map((c) => c.id));
|
|
455
|
+
const ux0 = Math.min(...sel.map((c) => c.x0)), ux1 = Math.max(...sel.map((c) => c.x1));
|
|
456
|
+
const uy0 = Math.min(...sel.map((c) => c.y0)), uy1 = Math.max(...sel.map((c) => c.y1));
|
|
457
|
+
const uh = uy1 - uy0;
|
|
458
|
+
const counts = /* @__PURE__ */ new Map();
|
|
459
|
+
let rows = 0;
|
|
460
|
+
for (let y = uy0 + Math.floor(uh * 0.25); y < uy1 - Math.floor(uh * 0.25); y++) {
|
|
461
|
+
let runs = 0, len = 0, thick = false;
|
|
462
|
+
for (let x = ux0; x <= ux1; x++) {
|
|
463
|
+
const on = x < ux1 && ids.has(label[y * w + x]);
|
|
464
|
+
if (on) len++;
|
|
465
|
+
else if (len) {
|
|
466
|
+
runs++;
|
|
467
|
+
if (len > uh * 0.35) thick = true;
|
|
468
|
+
len = 0;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
rows++;
|
|
472
|
+
if (!thick) counts.set(runs, (counts.get(runs) ?? 0) + 1);
|
|
473
|
+
}
|
|
474
|
+
const [n, k] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [0, 0];
|
|
475
|
+
return n >= 1 && n <= 3 && k >= rows * 0.7 ? n : null;
|
|
476
|
+
}
|
|
477
|
+
function circledAt(gray, w, h, ink, xs) {
|
|
478
|
+
const { comps } = components(gray, w, h, ink);
|
|
479
|
+
let charH = 0;
|
|
480
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
481
|
+
const rings = charH < 10 ? [] : comps.filter((r) => {
|
|
482
|
+
const rw = r.x1 - r.x0, rh = r.y1 - r.y0;
|
|
483
|
+
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);
|
|
484
|
+
});
|
|
485
|
+
return xs.map((x) => rings.some((r) => x >= r.x0 && x <= r.x1));
|
|
486
|
+
}
|
|
487
|
+
function quoteHead(gray, w, h, ink, cx) {
|
|
488
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
489
|
+
let charH = 0;
|
|
490
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
491
|
+
if (charH < 10) return null;
|
|
492
|
+
const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
|
|
493
|
+
if (!tall.length) return null;
|
|
494
|
+
const mid = (Math.min(...tall.map((c) => c.y0)) + Math.max(...tall.map((c) => c.y1))) / 2;
|
|
495
|
+
let best = null, bestD = charH * 0.4;
|
|
496
|
+
for (const c of comps) {
|
|
497
|
+
const ch = c.y1 - c.y0, d = Math.abs((c.x0 + c.x1) / 2 - cx);
|
|
498
|
+
if (ch < charH * 0.12 || ch > charH * 0.5 || (c.y0 + c.y1) / 2 > mid || d > bestD) continue;
|
|
499
|
+
bestD = d;
|
|
500
|
+
best = c;
|
|
501
|
+
}
|
|
502
|
+
if (!best) return null;
|
|
503
|
+
let n = 0, sy = 0;
|
|
504
|
+
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) {
|
|
505
|
+
n++;
|
|
506
|
+
sy += y;
|
|
507
|
+
}
|
|
508
|
+
return n ? (sy / n - best.y0) / (best.y1 - best.y0) : null;
|
|
509
|
+
}
|
|
510
|
+
function edgeTrim(gray, w, h, ink) {
|
|
511
|
+
const isInk = (v) => ink.darkInk ? v <= ink.threshold : v > ink.threshold;
|
|
512
|
+
const colInk = new Uint32Array(w);
|
|
513
|
+
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) if (isInk(gray[y * w + x])) colInk[x]++;
|
|
514
|
+
const ruleCol = new Uint8Array(w);
|
|
515
|
+
let liveW = 0;
|
|
516
|
+
for (let x = 0; x < w; x++) {
|
|
517
|
+
if (colInk[x] >= h * 0.85) ruleCol[x] = 1;
|
|
518
|
+
else liveW++;
|
|
519
|
+
}
|
|
520
|
+
const blank = Math.max(1, Math.floor(w * 0.02));
|
|
521
|
+
const bands = [];
|
|
522
|
+
let start = -1;
|
|
523
|
+
for (let y = 0; y <= h; y++) {
|
|
524
|
+
let n = 0;
|
|
525
|
+
if (y < h) {
|
|
526
|
+
for (let x = 0; x < w; x++) if (!ruleCol[x] && isInk(gray[y * w + x])) n++;
|
|
527
|
+
}
|
|
528
|
+
const on = y < h && n > blank && n < liveW * 0.85;
|
|
529
|
+
if (on && start < 0) start = y;
|
|
530
|
+
else if (!on && start >= 0) {
|
|
531
|
+
bands.push([start, y]);
|
|
532
|
+
start = -1;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
let y0 = 0, y1 = h;
|
|
536
|
+
if (bands.length >= 2 && h >= 24) {
|
|
537
|
+
const mh = Math.max(...bands.map(([a, b]) => b - a));
|
|
538
|
+
const [top, next] = [bands[0], bands[1]], [bot, prev] = [bands[bands.length - 1], bands[bands.length - 2]];
|
|
539
|
+
if (top[0] <= 1 && top[1] - top[0] < mh * 0.45) y0 = top[1] + next[0] >> 1;
|
|
540
|
+
if (bot[1] >= h - 1 && bot[1] - bot[0] < mh * 0.45) y1 = bot[0] + prev[1] + 1 >> 1;
|
|
541
|
+
if (y0 > 0 || y1 < h) {
|
|
542
|
+
let runs = 0, len = 0;
|
|
543
|
+
for (let x = 0; x <= w; x++) {
|
|
544
|
+
let on = false;
|
|
545
|
+
if (x < w && !ruleCol[x]) for (let y = y0; y < y1 && !on; y++) on = isInk(gray[y * w + x]);
|
|
546
|
+
if (on) len++;
|
|
547
|
+
else {
|
|
548
|
+
if (len >= (y1 - y0) * 0.3) runs++;
|
|
549
|
+
len = 0;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (runs < 2) {
|
|
553
|
+
y0 = 0;
|
|
554
|
+
y1 = h;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const textCol = (x) => {
|
|
559
|
+
for (let y = y0; y < y1; y++) if (isInk(gray[y * w + x])) return true;
|
|
560
|
+
return false;
|
|
561
|
+
};
|
|
562
|
+
const border = (x) => colInk[x] >= h * 0.95;
|
|
563
|
+
const gap = Math.max(2, Math.round(h * 0.1));
|
|
564
|
+
const clearFrom = (x, dir) => {
|
|
565
|
+
for (let k = 0; k < gap; k++) {
|
|
566
|
+
const c = x + dir * k;
|
|
567
|
+
if (c < 0 || c >= w || textCol(c)) return false;
|
|
568
|
+
}
|
|
569
|
+
return true;
|
|
570
|
+
};
|
|
571
|
+
let x0 = 0, x1 = w;
|
|
572
|
+
for (let x = 0; x < w * 0.2; x++) {
|
|
573
|
+
if (border(x)) {
|
|
574
|
+
let e = x;
|
|
575
|
+
while (e + 1 < w && border(e + 1)) e++;
|
|
576
|
+
if (clearFrom(e + 1, 1)) x0 = e + 1;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
if (textCol(x)) break;
|
|
580
|
+
}
|
|
581
|
+
for (let x = w - 1; x >= w * 0.8; x--) {
|
|
582
|
+
if (border(x)) {
|
|
583
|
+
let e = x;
|
|
584
|
+
while (e - 1 >= 0 && border(e - 1)) e--;
|
|
585
|
+
if (clearFrom(e - 1, -1)) x1 = e;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
if (textCol(x)) break;
|
|
589
|
+
}
|
|
590
|
+
return y0 > 0 || y1 < h || x0 > 0 || x1 < w ? { x0, x1, y0, y1 } : null;
|
|
591
|
+
}
|
|
592
|
+
function tallInkCount(gray, w, h, ink) {
|
|
593
|
+
const { comps } = components(gray, w, h, ink);
|
|
594
|
+
let charH = 0;
|
|
595
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
596
|
+
const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.5).sort((a, b) => a.x0 - b.x0);
|
|
597
|
+
let n = 0, end = -1;
|
|
598
|
+
for (const c of tall) {
|
|
599
|
+
if (c.x0 >= end) n++;
|
|
600
|
+
end = Math.max(end, c.x1);
|
|
601
|
+
}
|
|
602
|
+
return n;
|
|
603
|
+
}
|
|
604
|
+
function gapGlyphs(gray, w, h, ink) {
|
|
605
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
606
|
+
let charH = 0;
|
|
607
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
608
|
+
if (charH < 10) return { glyphs: [], bandH: 0 };
|
|
609
|
+
const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
|
|
610
|
+
const bandT = Math.min(...tall.map((c) => c.y0)), bandB = Math.max(...tall.map((c) => c.y1)), bh = bandB - bandT;
|
|
611
|
+
const inBand = (c) => c.y1 > bandT && c.y0 < bandB;
|
|
612
|
+
const out = [];
|
|
613
|
+
for (const c of comps) {
|
|
614
|
+
if (!inBand(c)) continue;
|
|
615
|
+
const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
|
|
616
|
+
if (ch >= bh * 0.9 && cw <= bh * 0.15) continue;
|
|
617
|
+
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));
|
|
618
|
+
const apart = !others.some((o) => o.x0 < c.x1 + 1 && o.x1 > c.x0 - 1);
|
|
619
|
+
const abut = !apart && ch >= bh * 0.3 && ch <= bh * 0.65 && !others.some((o) => o.x0 < c.x1 && o.x1 > c.x0);
|
|
620
|
+
const nearlyApart = !others.some((o) => Math.min(o.x1, c.x1) - Math.max(o.x0, c.x0) > 2);
|
|
621
|
+
const spans = [], runs = [];
|
|
622
|
+
let n = 0;
|
|
623
|
+
for (let y = c.y0; y < c.y1; y++) {
|
|
624
|
+
let s = 0, r = 0, prev = false;
|
|
625
|
+
for (let x = c.x0; x < c.x1; x++) {
|
|
626
|
+
const on = label[y * w + x] === c.id;
|
|
627
|
+
if (on) {
|
|
628
|
+
s++;
|
|
629
|
+
if (!prev) r++;
|
|
630
|
+
}
|
|
631
|
+
prev = on;
|
|
632
|
+
}
|
|
633
|
+
spans.push(s);
|
|
634
|
+
runs.push(r);
|
|
635
|
+
n += s;
|
|
636
|
+
}
|
|
637
|
+
const fill = n / (cw * ch), cy = ((c.y0 + c.y1) / 2 - bandT) / bh;
|
|
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
|
+
let mark = null;
|
|
641
|
+
const speck = Math.max(3, bh * 0.08);
|
|
642
|
+
const dot = ch <= bh * 0.25 && cw <= bh * 0.25 && ch >= speck && cw >= speck && cw >= ch * 0.6 && cw <= ch * 1.6;
|
|
643
|
+
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;
|
|
644
|
+
if (!apart && !abut && !(nearlyApart && ch >= bh * 0.6)) {
|
|
645
|
+
if (small) out.push({ x0: c.x0, x1: c.x1, mark: "\xB7" });
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const bracket = () => {
|
|
649
|
+
const stem = (from, to) => {
|
|
650
|
+
let sx = 0, k = 0;
|
|
651
|
+
for (let y = from; y < to; y++) for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
|
|
652
|
+
sx += x - c.x0;
|
|
653
|
+
k++;
|
|
654
|
+
}
|
|
655
|
+
return k ? sx / k / cw : 0.5;
|
|
656
|
+
};
|
|
657
|
+
if (c.y0 <= bandT + bh * 0.15 && top >= 0.7 && bottom <= 0.5 && stem(c.y0 + band, c.y1) <= 0.35) return "\u300C";
|
|
658
|
+
if (c.y1 >= bandB - bh * 0.15 && bottom >= 0.7 && top <= 0.5 && stem(c.y0, c.y1 - band) >= 0.65) return "\u300D";
|
|
659
|
+
return null;
|
|
660
|
+
};
|
|
661
|
+
if (!apart && !abut) {
|
|
662
|
+
} else if (abut) {
|
|
663
|
+
if (fill < 0.5) mark = bracket();
|
|
664
|
+
} else if (dot && fill >= 0.6 && cy >= 0.3 && cy <= 0.7) mark = "\xB7";
|
|
665
|
+
else if (small) mark = "\xB7";
|
|
666
|
+
else if (dot && fill >= 0.6 && cy >= 0.75 && cw >= ch * 0.8) mark = ".";
|
|
667
|
+
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";
|
|
668
|
+
else if (ch <= bh * 0.15 && cw >= ch * 2 && cw <= bh * 0.8 && fill >= 0.7 && cy >= 0.35 && cy <= 0.75) mark = "-";
|
|
669
|
+
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";
|
|
670
|
+
else if (ch >= bh * 0.3 && ch <= bh * 0.65 && fill < 0.5) mark = bracket();
|
|
671
|
+
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";
|
|
672
|
+
if (!mark && ch >= bh * 0.6 && cw >= ch * 0.25 && cw <= ch * 0.7 && fill <= 0.4 && runs.every((r) => r <= 1)) {
|
|
673
|
+
const mx = (y) => {
|
|
674
|
+
let sx = 0, k = 0;
|
|
675
|
+
for (let x = c.x0; x < c.x1; x++) if (label[y * w + x] === c.id) {
|
|
676
|
+
sx += x;
|
|
677
|
+
k++;
|
|
678
|
+
}
|
|
679
|
+
return k ? sx / k : NaN;
|
|
680
|
+
};
|
|
681
|
+
if (mx(c.y0 + band) - mx(c.y1 - 1 - band) >= cw * 0.5) mark = "/";
|
|
682
|
+
}
|
|
683
|
+
if (mark) out.push({ x0: c.x0, x1: c.x1, mark });
|
|
684
|
+
}
|
|
685
|
+
return { glyphs: out, bandH: bh };
|
|
686
|
+
}
|
|
687
|
+
function ringBullet(gray, w, h, ink, cx) {
|
|
688
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
689
|
+
let charH = 0;
|
|
690
|
+
for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
|
|
691
|
+
if (charH < 10) return null;
|
|
692
|
+
const tall = comps.filter((c2) => c2.y1 - c2.y0 >= charH * 0.6);
|
|
693
|
+
const bandT = Math.min(...tall.map((c2) => c2.y0)), bh = Math.max(...tall.map((c2) => c2.y1)) - bandT;
|
|
694
|
+
let best = null, bestD = bh * 0.6;
|
|
695
|
+
for (const c2 of comps) {
|
|
696
|
+
const d = Math.abs((c2.x0 + c2.x1) / 2 - cx);
|
|
697
|
+
if (c2.y1 - c2.y0 >= bh * 0.3 && d < bestD) {
|
|
698
|
+
bestD = d;
|
|
699
|
+
best = c2;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (!best) return null;
|
|
703
|
+
const c = best, cw = c.x1 - c.x0, ch = c.y1 - c.y0;
|
|
704
|
+
if (cw < ch * 0.85 || cw > ch * 1.2) return null;
|
|
705
|
+
const runs = (xs, at) => {
|
|
706
|
+
let n2 = 0, len = 0, mx2 = 0, mn = Infinity;
|
|
707
|
+
for (const k of [...xs, -1]) {
|
|
708
|
+
if (k >= 0 && at(k)) {
|
|
709
|
+
if (!len) n2++;
|
|
710
|
+
len++;
|
|
711
|
+
mx2 = Math.max(mx2, len);
|
|
712
|
+
} else {
|
|
713
|
+
if (len) mn = Math.min(mn, len);
|
|
714
|
+
len = 0;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return { n: n2, mx: mx2, mn };
|
|
718
|
+
};
|
|
719
|
+
const my = c.y0 + c.y1 >> 1, mx = c.x0 + c.x1 >> 1;
|
|
720
|
+
const row = runs(Array.from({ length: cw }, (_, k) => c.x0 + k), (x) => label[my * w + x] === c.id);
|
|
721
|
+
const col = runs(Array.from({ length: ch }, (_, k) => c.y0 + k), (y) => label[y * w + mx] === c.id);
|
|
722
|
+
if (row.n !== 2 || col.n !== 2) return null;
|
|
723
|
+
let n = 0;
|
|
724
|
+
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++;
|
|
725
|
+
const fill = n / (cw * ch), rel = ch / bh;
|
|
726
|
+
if (rel >= 0.62 && (fill <= 0.23 ? row.mx <= cw * 0.1 : fill <= 0.33 && row.mn <= cw * 0.08)) return "\u25CB";
|
|
727
|
+
if (rel <= 0.58 && fill >= 0.24) return "\u3147";
|
|
728
|
+
if (rel >= 0.6 && rel <= 0.8 && fill >= 0.3 && ((c.y0 + c.y1) / 2 - bandT) / bh >= 0.55) return "o";
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
function dashRatio(gray, w, h, ink, cx) {
|
|
732
|
+
const { comps } = components(gray, w, h, ink);
|
|
733
|
+
let charH = 0;
|
|
734
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
735
|
+
if (charH < 10) return null;
|
|
736
|
+
const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
|
|
737
|
+
const bh = Math.max(...tall.map((c) => c.y1)) - Math.min(...tall.map((c) => c.y0));
|
|
738
|
+
const dash = comps.filter((c) => c.y1 - c.y0 <= bh * 0.25 && c.x1 - c.x0 >= (c.y1 - c.y0) * 2 && Math.abs((c.x0 + c.x1) / 2 - cx) <= bh * 0.6).sort((a, b) => Math.abs((a.x0 + a.x1) / 2 - cx) - Math.abs((b.x0 + b.x1) / 2 - cx))[0];
|
|
739
|
+
return dash ? (dash.x1 - dash.x0) / bh : null;
|
|
740
|
+
}
|
|
741
|
+
function discAt(gray, w, h, ink, cx) {
|
|
742
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
743
|
+
let charH = 0;
|
|
744
|
+
for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
|
|
745
|
+
if (charH < 10) return false;
|
|
746
|
+
const tall = comps.filter((c2) => c2.y1 - c2.y0 >= charH * 0.6);
|
|
747
|
+
const bh = Math.max(...tall.map((c2) => c2.y1)) - Math.min(...tall.map((c2) => c2.y0));
|
|
748
|
+
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];
|
|
749
|
+
if (!c) return false;
|
|
750
|
+
const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
|
|
751
|
+
if (ch < bh * 0.45 || ch > bh * 1.05 || cw < ch * 0.8 || cw > ch * 1.25) return false;
|
|
752
|
+
let n = 0;
|
|
753
|
+
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++;
|
|
754
|
+
const fill = n / (cw * ch);
|
|
755
|
+
return fill >= 0.65 && fill <= 0.9;
|
|
756
|
+
}
|
|
757
|
+
function serifOne(gray, w, h, ink, cx) {
|
|
758
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
759
|
+
let charH = 0;
|
|
760
|
+
for (const c2 of comps) charH = Math.max(charH, c2.y1 - c2.y0);
|
|
761
|
+
if (charH < 10) return false;
|
|
762
|
+
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];
|
|
763
|
+
if (!c) return false;
|
|
764
|
+
const ch = c.y1 - c.y0;
|
|
765
|
+
const span2 = (a, b) => {
|
|
766
|
+
let mn = Infinity, mx = -1;
|
|
767
|
+
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) {
|
|
768
|
+
mn = Math.min(mn, x);
|
|
769
|
+
mx = Math.max(mx, x);
|
|
770
|
+
}
|
|
771
|
+
return [mn, mx];
|
|
772
|
+
};
|
|
773
|
+
const [m0, m1] = span2(0.4, 0.6), [t0, t1] = span2(0, 0.2), [b0, b1] = span2(0.8, 1);
|
|
774
|
+
const sw = m1 - m0 + 1;
|
|
775
|
+
const even = (l, r) => l >= sw * 0.4 && r >= sw * 0.4 && Math.abs(l - r) <= Math.max(l, r) * 0.35;
|
|
776
|
+
return sw >= 2 && sw <= ch * 0.3 && even(m0 - t0, t1 - m1) && even(m0 - b0, b1 - m1);
|
|
777
|
+
}
|
|
778
|
+
function starRun(gray, w, h, ink, cx) {
|
|
779
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
780
|
+
let charH = 0;
|
|
781
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
782
|
+
if (charH < 10) return 0;
|
|
783
|
+
const tall = comps.filter((c) => c.y1 - c.y0 >= charH * 0.6);
|
|
784
|
+
const bandT = Math.min(...tall.map((c) => c.y0)), bh = Math.max(...tall.map((c) => c.y1)) - bandT;
|
|
785
|
+
const star = (c) => {
|
|
786
|
+
const cw = c.x1 - c.x0, ch = c.y1 - c.y0;
|
|
787
|
+
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;
|
|
788
|
+
let n = 0;
|
|
789
|
+
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++;
|
|
790
|
+
const fill = n / (cw * ch);
|
|
791
|
+
return fill >= 0.25 && fill <= 0.65;
|
|
792
|
+
};
|
|
793
|
+
const stars = comps.filter(star).sort((a, b) => a.x0 - b.x0);
|
|
794
|
+
const k = stars.findIndex((c) => cx >= c.x0 - (c.x1 - c.x0) && cx <= c.x1 + (c.x1 - c.x0));
|
|
795
|
+
if (k < 0) return 0;
|
|
796
|
+
let lo = k, hi = k;
|
|
797
|
+
while (lo > 0 && stars[lo].x0 - stars[lo - 1].x1 <= stars[lo].x1 - stars[lo].x0) lo--;
|
|
798
|
+
while (hi + 1 < stars.length && stars[hi + 1].x0 - stars[hi].x1 <= stars[hi].x1 - stars[hi].x0) hi++;
|
|
799
|
+
return hi - lo + 1;
|
|
800
|
+
}
|
|
801
|
+
var BRACKET_TAPER = 0.16;
|
|
802
|
+
var BRACKET_SOLID = 0.4;
|
|
803
|
+
var BRACKET_WIDE = 0.265;
|
|
804
|
+
var BRACKET_LENS = 0.35;
|
|
805
|
+
function bracketShape(f, close) {
|
|
806
|
+
const [near2, far] = close ? [f.bottom, f.top] : [f.top, f.bottom];
|
|
807
|
+
const paren = close ? ")" : "(";
|
|
808
|
+
if (near2 >= 0.7 && far <= 0.5) return f.taper < BRACKET_TAPER ? close ? "\u300D" : "\u300C" : paren;
|
|
809
|
+
if (near2 >= 0.7 && far >= 0.7 && f.taper >= BRACKET_TAPER) {
|
|
810
|
+
return f.fill >= BRACKET_SOLID && (f.cw >= f.ch * BRACKET_WIDE || f.taper >= BRACKET_LENS) ? close ? "\u3011" : "\u3010" : paren;
|
|
811
|
+
}
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
function leadingBullet(gray, w, h, ink, firstX) {
|
|
815
|
+
const { comps, label } = components(gray, w, h, ink);
|
|
816
|
+
let charH = 0;
|
|
817
|
+
for (const c of comps) charH = Math.max(charH, c.y1 - c.y0);
|
|
818
|
+
if (charH < 10) return null;
|
|
819
|
+
const cand = comps.filter((c) => c.y1 - c.y0 >= charH * 0.15).sort((a, b) => a.x0 - b.x0);
|
|
820
|
+
const first = cand[0];
|
|
821
|
+
if (!first) return null;
|
|
822
|
+
const cl = cand.filter((c) => c.x0 < first.x1 && c.x1 > first.x0);
|
|
823
|
+
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));
|
|
824
|
+
const cw = x1 - x0, ch = y1 - y0;
|
|
825
|
+
if (cw < ch * 0.8 || cw > ch * 1.25) return null;
|
|
826
|
+
const next = cand.find((c) => c.x0 >= x1);
|
|
827
|
+
if (next && next.x0 - x1 < ch * 0.3) return null;
|
|
828
|
+
let inkN = 0;
|
|
829
|
+
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++;
|
|
830
|
+
const fill = inkN / (cw * ch);
|
|
831
|
+
const rel = ch / charH;
|
|
832
|
+
const ring = (c) => {
|
|
833
|
+
const a = c.x1 - c.x0, b = c.y1 - c.y0;
|
|
834
|
+
return a >= b * 0.75 && a <= b * 1.33;
|
|
835
|
+
};
|
|
836
|
+
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 && ring(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));
|
|
837
|
+
const span2 = (y) => {
|
|
838
|
+
let n = 0;
|
|
839
|
+
for (let x = x0; x < x1; x++) if (cl.some((c) => label[y * w + x] === c.id)) n++;
|
|
840
|
+
return n;
|
|
841
|
+
};
|
|
842
|
+
const band = Math.max(1, Math.round(ch * 0.15));
|
|
843
|
+
const edgeFull = (from, dir) => {
|
|
844
|
+
let m = 0;
|
|
845
|
+
for (let k = 0; k < band; k++) m = Math.max(m, span2(from + dir * k));
|
|
846
|
+
return m >= cw * 0.8;
|
|
847
|
+
};
|
|
848
|
+
let midRuns = 0;
|
|
849
|
+
{
|
|
850
|
+
let prev = false;
|
|
851
|
+
const my = y0 + (ch >> 1);
|
|
852
|
+
for (let x = x0; x < x1; x++) {
|
|
853
|
+
const on = cl.some((c) => label[my * w + x] === c.id);
|
|
854
|
+
if (on && !prev) midRuns++;
|
|
855
|
+
prev = on;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const hollowSquare = cl.length === 1 && fill < 0.5 && midRuns === 2 && edgeFull(y0, 1) && edgeFull(y1 - 1, -1);
|
|
859
|
+
let mark = null;
|
|
860
|
+
if (hollowSquare && rel >= 0.6 && rel <= 0.92) mark = "\u25A1";
|
|
861
|
+
else if (nested && fill < 0.45 && rel >= 0.6) mark = "\u25CE";
|
|
862
|
+
else if (cl.length === 1 && fill >= 0.9 && rel >= 0.28 && rel <= 0.6) mark = "\u25AA";
|
|
863
|
+
else if (cl.length === 1 && fill >= 0.65 && fill <= 0.88 && rel >= 0.5 && rel <= 0.8) mark = "\u25CF";
|
|
864
|
+
if (!mark) return null;
|
|
865
|
+
return { mark, covers: firstX <= x1 + ch * 0.3 };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/ocr/text-gaps.ts
|
|
869
|
+
var evidence = /* @__PURE__ */ new WeakMap();
|
|
870
|
+
var NO_GAPS = [];
|
|
871
|
+
function rememberOcrTextGaps(item, gaps) {
|
|
872
|
+
if (gaps.length) evidence.set(item, { text: item.text, gaps });
|
|
873
|
+
}
|
|
874
|
+
function ocrTextGaps(item) {
|
|
875
|
+
const saved = evidence.get(item);
|
|
876
|
+
return saved?.text === item.text ? saved.gaps : NO_GAPS;
|
|
877
|
+
}
|
|
878
|
+
function copyOcrTextGaps(source, target, scale = 1, offsetX = 0) {
|
|
879
|
+
const gaps = ocrTextGaps(source);
|
|
880
|
+
if (!gaps.length || source.text !== target.text) return;
|
|
881
|
+
rememberOcrTextGaps(target, scale === 1 && offsetX === 0 ? gaps : gaps.map((g) => ({ ...g, left: g.left * scale + offsetX, right: g.right * scale + offsetX })));
|
|
882
|
+
}
|
|
883
|
+
function findOcrTextGaps(rgba, pageW, box, read, text, steps, stepPx) {
|
|
884
|
+
if (read.replace(/\s/g, "") !== text.replace(/\s/g, "") || [...read].length !== steps.length) return [];
|
|
885
|
+
const visible = [...read].flatMap((c, i) => /\s/.test(c) ? [] : [(steps[i] + 0.5) * stepPx]);
|
|
886
|
+
if (visible.length < 2) return [];
|
|
887
|
+
const gray = grayCrop(rgba, pageW, box), ink = inkStats(gray);
|
|
888
|
+
if (!ink.darkInk || ink.contrast < 35) return [];
|
|
889
|
+
const counts = new Uint32Array(box.w);
|
|
890
|
+
for (let y = 0; y < box.h; y++) for (let x = 0; x < box.w; x++) if (gray[y * box.w + x] <= ink.threshold) counts[x]++;
|
|
891
|
+
const out = [];
|
|
892
|
+
let offset = 0, index = 0;
|
|
893
|
+
for (const c of text) {
|
|
894
|
+
if (!/\s/.test(c)) {
|
|
895
|
+
if (index > 0) {
|
|
896
|
+
const from = Math.max(0, Math.floor(visible[index - 1]) + 1), end = Math.min(box.w, Math.ceil(visible[index]));
|
|
897
|
+
let start = from, left = from, right = from;
|
|
898
|
+
for (let x = from; x <= end; x++) {
|
|
899
|
+
if (x === end || counts[x] > Math.max(1, Math.floor(box.h * 0.03))) {
|
|
900
|
+
if (x - start > right - left) {
|
|
901
|
+
left = start;
|
|
902
|
+
right = x;
|
|
903
|
+
}
|
|
904
|
+
start = x + 1;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (right - left >= Math.max(3, box.h * 0.25)) out.push({ at: offset, left: box.x + left, right: box.x + right });
|
|
908
|
+
}
|
|
909
|
+
index++;
|
|
910
|
+
}
|
|
911
|
+
offset += c.length;
|
|
912
|
+
}
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
|
|
149
916
|
// src/pdf/line-extract.ts
|
|
150
917
|
import { OPS } from "pdfjs-dist/legacy/build/pdf.mjs";
|
|
151
918
|
var ORIENTATION_TOL = 2;
|
|
@@ -1246,7 +2013,17 @@ function dropHeadBandClipGrids(clipGrids, lineGrids) {
|
|
|
1246
2013
|
if (clipGrids.length === 0 || lineGrids.length === 0) return clipGrids;
|
|
1247
2014
|
const near2 = (a, b) => Math.abs(a - b) <= SHADE_CLIP_TOL;
|
|
1248
2015
|
const overlaps = (a, b) => Math.min(a.x2, b.x2) > Math.max(a.x1, b.x1) && Math.min(a.y2, b.y2) > Math.max(a.y1, b.y1);
|
|
1249
|
-
|
|
2016
|
+
const kept = clipGrids.filter((c) => c.clipParent || !c.cells?.length || !lineGrids.some((l) => l.rowYs.length > c.rowYs.length && l.colXs.length === c.colXs.length && c.colXs.every((x, k) => near2(x, l.colXs[k])) && c.rowYs.every((y, k) => near2(y, l.rowYs[k])) && !clipGrids.some((o) => o !== c && overlaps(o.bbox, l.bbox))));
|
|
2017
|
+
return kept.filter((c) => !lineGrids.some((l) => headBandTiles(kept, l, near2, overlaps).includes(c)));
|
|
2018
|
+
}
|
|
2019
|
+
function headBandTiles(clips, l, near2, overlaps) {
|
|
2020
|
+
const onRow = (y) => l.rowYs.some((r) => near2(r, y));
|
|
2021
|
+
const band = clips.filter((c) => !c.clipParent && c.cells?.length && near2(c.bbox.y2, l.bbox.y2) && c.bbox.y1 > l.bbox.y1 + SHADE_CLIP_TOL && c.bbox.x1 >= l.bbox.x1 - SHADE_CLIP_TOL && c.bbox.x2 <= l.bbox.x2 + SHADE_CLIP_TOL && c.rowYs.every(onRow)).sort((a, b) => a.bbox.x1 - b.bbox.x1);
|
|
2022
|
+
if (band.length < 2 || !near2(band[0].bbox.x1, l.bbox.x1) || !near2(band[band.length - 1].bbox.x2, l.bbox.x2)) return [];
|
|
2023
|
+
if (band.some((c, k) => k > 0 && c.bbox.x1 > band[k - 1].bbox.x2 + SHADE_CLIP_TOL)) return [];
|
|
2024
|
+
const bottom = Math.min(...band.map((c) => c.bbox.y1));
|
|
2025
|
+
const body = { ...l.bbox, y2: bottom };
|
|
2026
|
+
return clips.some((o) => !band.includes(o) && overlaps(o.bbox, body)) ? [] : band;
|
|
1250
2027
|
}
|
|
1251
2028
|
function enforceMinWidth(colXs, minWidth, narrowRuns = true) {
|
|
1252
2029
|
if (colXs.length <= 2) return colXs;
|
|
@@ -3698,6 +4475,59 @@ function joinClusterCellLines(lines, box, lex) {
|
|
|
3698
4475
|
return out;
|
|
3699
4476
|
}
|
|
3700
4477
|
|
|
4478
|
+
// src/pdf/ocr-column-cuts.ts
|
|
4479
|
+
var OCR_NUMERIC_CELL = /^[+-]?\d+(?:[,.]\d+)*%?$/;
|
|
4480
|
+
var OCR_GROUPED_NUMBER = /^[+-]?\d{1,3}(?:\s+\d{3})+(?:[.,]\d+)?%?$/;
|
|
4481
|
+
function supportedOcrColumnCuts(rows) {
|
|
4482
|
+
const cuts = /* @__PURE__ */ new Map();
|
|
4483
|
+
for (const head of rows[0] ?? []) {
|
|
4484
|
+
if (head.items.length !== 1 || head.items[0].fontName !== "ocr") continue;
|
|
4485
|
+
const candidates = [];
|
|
4486
|
+
for (const gap of ocrTextGaps(head.items[0])) {
|
|
4487
|
+
const text = head.items[0].text;
|
|
4488
|
+
if (!/\p{L}/u.test(text.slice(0, gap.at)) || !/\p{L}/u.test(text.slice(gap.at))) continue;
|
|
4489
|
+
const intervals = [];
|
|
4490
|
+
for (const row of rows.slice(1)) {
|
|
4491
|
+
const cell = row.find((c) => c.col === head.col);
|
|
4492
|
+
if (!cell || cell.items.some((it) => it.fontName !== "ocr")) continue;
|
|
4493
|
+
let interval;
|
|
4494
|
+
if (cell.items.length === 1) {
|
|
4495
|
+
const it = cell.items[0];
|
|
4496
|
+
interval = ocrTextGaps(it).find((g) => isOcrNumericSplit(it.text, g.at));
|
|
4497
|
+
} else if (isOcrNumericPair(cell.items)) {
|
|
4498
|
+
const [a, b] = [...cell.items].sort((a2, b2) => a2.x - b2.x);
|
|
4499
|
+
interval = { left: a.x + a.w / 2, right: b.x + b.w / 2 };
|
|
4500
|
+
}
|
|
4501
|
+
if (interval) {
|
|
4502
|
+
const left = Math.max(gap.left, interval.left), right = Math.min(gap.right, interval.right);
|
|
4503
|
+
if (right > left) intervals.push({ left, right });
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
const events = intervals.flatMap((i) => [{ x: i.left, delta: 1 }, { x: i.right, delta: -1 }]).sort((a, b) => a.x - b.x);
|
|
4507
|
+
let active = 0, bestCount = 1, bestWidth = 0, cut;
|
|
4508
|
+
for (let i = 0; i < events.length; ) {
|
|
4509
|
+
const x = events[i].x;
|
|
4510
|
+
while (i < events.length && events[i].x === x) active += events[i++].delta;
|
|
4511
|
+
const width = i < events.length ? events[i].x - x : 0;
|
|
4512
|
+
if (active >= 2 && width > 0 && (active > bestCount || active === bestCount && width > bestWidth)) {
|
|
4513
|
+
bestCount = active;
|
|
4514
|
+
bestWidth = width;
|
|
4515
|
+
cut = x + width / 2;
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
if (cut !== void 0) candidates.push(cut);
|
|
4519
|
+
}
|
|
4520
|
+
if (candidates.length === 1) cuts.set(head.col, candidates[0]);
|
|
4521
|
+
}
|
|
4522
|
+
return cuts;
|
|
4523
|
+
}
|
|
4524
|
+
function isOcrNumericPair(items) {
|
|
4525
|
+
return items.length === 2 && items.every((it) => it.fontName === "ocr" && OCR_NUMERIC_CELL.test(it.text.trim())) && !OCR_GROUPED_NUMBER.test([...items].sort((a, b) => a.x - b.x).map((it) => it.text.trim()).join(" "));
|
|
4526
|
+
}
|
|
4527
|
+
function isOcrNumericSplit(text, at) {
|
|
4528
|
+
return !OCR_GROUPED_NUMBER.test(text.trim()) && /\s/.test((text[at - 1] ?? "") + (text[at] ?? "")) && OCR_NUMERIC_CELL.test(text.slice(0, at).trim()) && OCR_NUMERIC_CELL.test(text.slice(at).trim());
|
|
4529
|
+
}
|
|
4530
|
+
|
|
3701
4531
|
// src/pdf/cluster-detector.ts
|
|
3702
4532
|
var Y_TOL = 3;
|
|
3703
4533
|
var COL_CLUSTER_TOL = 15;
|
|
@@ -4462,17 +5292,21 @@ function assignRowItems(items, columns, numCols, headerFace) {
|
|
|
4462
5292
|
return result;
|
|
4463
5293
|
}
|
|
4464
5294
|
function buildClusterTable(rows, columns, pageNum, lex) {
|
|
4465
|
-
const numCols = columns.length;
|
|
4466
5295
|
const numRows = rows.length;
|
|
4467
|
-
if (numRows < MIN_ROWS ||
|
|
5296
|
+
if (numRows < MIN_ROWS || columns.length < MIN_COLS) return null;
|
|
5297
|
+
const headFaces = new Set(rows[0]?.items.map((i) => i.fontName));
|
|
5298
|
+
const headerFace = headFaces.size === 1 && rows.slice(1).every((row) => row.items.every((i) => !headFaces.has(i.fontName))) ? [...headFaces][0] : void 0;
|
|
5299
|
+
const assigned = rows.map((row, r) => assignRowItems(row.items, columns, columns.length, r === 0 ? headerFace : void 0));
|
|
5300
|
+
const cuts = supportedOcrColumnCuts(assigned);
|
|
5301
|
+
const starts = columns.map((_, c) => c + [...cuts.keys()].filter((k) => k < c).length);
|
|
5302
|
+
const numCols = columns.length + cuts.size;
|
|
4468
5303
|
const cells = Array.from(
|
|
4469
5304
|
{ length: numRows },
|
|
4470
5305
|
() => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
|
|
4471
5306
|
);
|
|
4472
5307
|
const usedItems = /* @__PURE__ */ new Set();
|
|
4473
5308
|
const lines = cells.map((row) => row.map(() => []));
|
|
4474
|
-
const
|
|
4475
|
-
const headerFace = headFaces.size === 1 && rows.slice(1).every((row) => row.items.every((i) => !headFaces.has(i.fontName))) ? [...headFaces][0] : void 0;
|
|
5309
|
+
const origins = /* @__PURE__ */ new Map();
|
|
4476
5310
|
for (let r = 0; r < numRows; r++) {
|
|
4477
5311
|
const row = rows[r];
|
|
4478
5312
|
if (row.items.length === 1 && numCols > 1) {
|
|
@@ -4480,15 +5314,39 @@ function buildClusterTable(rows, columns, pageNum, lex) {
|
|
|
4480
5314
|
usedItems.add(row.items[0]);
|
|
4481
5315
|
continue;
|
|
4482
5316
|
}
|
|
4483
|
-
const assignments =
|
|
4484
|
-
|
|
5317
|
+
const assignments = assigned[r].flatMap(({ col, items }) => {
|
|
5318
|
+
const target = starts[col], cut = cuts.get(col);
|
|
5319
|
+
if (cut === void 0) return [{ col: target, items, span: 1 }];
|
|
5320
|
+
const gapAt = (it) => ocrTextGaps(it).find((g) => g.left < cut && g.right > cut && (r === 0 || isOcrNumericSplit(it.text, g.at)));
|
|
5321
|
+
const groupedNumber = OCR_GROUPED_NUMBER.test([...items].sort((a, b) => a.x - b.x).map((it) => it.text.trim()).join(" "));
|
|
5322
|
+
if (groupedNumber || !isOcrNumericPair(items) && items.some((it) => it.x < cut && it.x + it.w > cut && !gapAt(it))) return [{ col: target, items, span: 2 }];
|
|
5323
|
+
const parts = [[], []];
|
|
5324
|
+
for (const it of items) {
|
|
5325
|
+
const g = gapAt(it);
|
|
5326
|
+
if (!g) {
|
|
5327
|
+
parts[it.x + it.w / 2 < cut ? 0 : 1].push(it);
|
|
5328
|
+
continue;
|
|
5329
|
+
}
|
|
5330
|
+
const fragments = [
|
|
5331
|
+
{ ...it, text: it.text.slice(0, g.at).trimEnd(), w: g.left - it.x },
|
|
5332
|
+
{ ...it, text: it.text.slice(g.at).trimStart(), x: g.right, w: it.x + it.w - g.right }
|
|
5333
|
+
];
|
|
5334
|
+
fragments.forEach((fragment, side) => {
|
|
5335
|
+
origins.set(fragment, it);
|
|
5336
|
+
parts[side].push(fragment);
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
return parts.flatMap((part, side) => part.length ? [{ col: target + side, items: part, span: 1 }] : []);
|
|
5340
|
+
});
|
|
5341
|
+
for (const { col, items, span: span2 } of assignments) {
|
|
5342
|
+
cells[r][col].colSpan = span2;
|
|
4485
5343
|
const cell = lines[r][col];
|
|
4486
5344
|
if (cell.length) cell[cell.length - 1].spaceAfter = true;
|
|
4487
5345
|
cell.push(...clusterCellLines(items));
|
|
4488
|
-
for (const item of items) usedItems.add(item);
|
|
5346
|
+
for (const item of items) usedItems.add(origins.get(item) ?? item);
|
|
4489
5347
|
}
|
|
4490
5348
|
}
|
|
4491
|
-
const boxes =
|
|
5349
|
+
const boxes = Array.from({ length: numCols }, (_, c) => {
|
|
4492
5350
|
const col = lines.flatMap((row) => row[c]);
|
|
4493
5351
|
return { x1: Math.min(...col.map((l) => l.left)), x2: Math.max(...col.map((l) => l.right)) };
|
|
4494
5352
|
});
|
|
@@ -4511,7 +5369,7 @@ function buildClusterTable(rows, columns, pageNum, lex) {
|
|
|
4511
5369
|
if (/^[○●▶\-·]/.test(contentText)) continue;
|
|
4512
5370
|
for (let pr = r - 1; pr >= 0; pr--) {
|
|
4513
5371
|
if (cells[pr].some((c) => c.text.trim())) {
|
|
4514
|
-
if (cells[pr]
|
|
5372
|
+
if (cells[pr].some((c) => c.colSpan > 1)) break;
|
|
4515
5373
|
for (let c = 0; c < numCols; c++) {
|
|
4516
5374
|
if (!cells[r][c].text.trim()) continue;
|
|
4517
5375
|
lines[pr][c].push(...lines[r][c]);
|
|
@@ -8705,18 +9563,22 @@ function extractBlocksWithGrids(items, pageNum, pageWidth, pageHeight, grids, ho
|
|
|
8705
9563
|
let panels = null;
|
|
8706
9564
|
if (remaining.length > 0) {
|
|
8707
9565
|
remaining.sort((a, b) => b.y - a.y || a.x - b.x);
|
|
8708
|
-
const clusterItems = remaining.map((i) =>
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
9566
|
+
const clusterItems = remaining.map((i) => {
|
|
9567
|
+
const item = {
|
|
9568
|
+
text: i.text,
|
|
9569
|
+
x: i.x,
|
|
9570
|
+
y: i.y,
|
|
9571
|
+
w: i.w,
|
|
9572
|
+
h: i.h,
|
|
9573
|
+
fontSize: i.fontSize,
|
|
9574
|
+
fontName: i.fontName,
|
|
9575
|
+
hasSpaceBefore: i.hasSpaceBefore,
|
|
9576
|
+
syntheticSpace: i.syntheticSpace,
|
|
9577
|
+
spaceAfter: i.spaceAfter
|
|
9578
|
+
};
|
|
9579
|
+
copyOcrTextGaps(i, item);
|
|
9580
|
+
return item;
|
|
9581
|
+
});
|
|
8720
9582
|
const proseColumns = findTwoColumnProseCutX(clusterItems) !== null || detectPersistentColumnGutter(remaining.map((i) => ({ x: i.x, y: i.y, w: i.w, h: i.h > 0 ? i.h : i.fontSize }))) !== null;
|
|
8721
9583
|
const clusterResults = (proseColumns ? [] : detectClusterTables(clusterItems, pageNum, void 0, lex)).filter((cr) => {
|
|
8722
9584
|
const b = cr.bbox;
|
|
@@ -9032,18 +9894,22 @@ function extractPageBlocksFallback(items, pageNum, fullPage = false, detectTable
|
|
|
9032
9894
|
if (cards) return cards.flatMap((group) => extractPageBlocksFallback(group, pageNum, false, detectTables, lex));
|
|
9033
9895
|
}
|
|
9034
9896
|
const blocks = [];
|
|
9035
|
-
const clusterItems = items.map((i) =>
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9897
|
+
const clusterItems = items.map((i) => {
|
|
9898
|
+
const item = {
|
|
9899
|
+
text: i.text,
|
|
9900
|
+
x: i.x,
|
|
9901
|
+
y: i.y,
|
|
9902
|
+
w: i.w,
|
|
9903
|
+
h: i.h,
|
|
9904
|
+
fontSize: i.fontSize,
|
|
9905
|
+
fontName: i.fontName,
|
|
9906
|
+
hasSpaceBefore: i.hasSpaceBefore,
|
|
9907
|
+
syntheticSpace: i.syntheticSpace,
|
|
9908
|
+
spaceAfter: i.spaceAfter
|
|
9909
|
+
};
|
|
9910
|
+
copyOcrTextGaps(i, item);
|
|
9911
|
+
return item;
|
|
9912
|
+
});
|
|
9047
9913
|
const textRects = items.map((i) => ({ x: i.x, y: i.y, w: i.w, h: i.h > 0 ? i.h : i.fontSize }));
|
|
9048
9914
|
const imagePanels = fullPage ? splitImagePanels(items, figures) : null;
|
|
9049
9915
|
if (imagePanels) return imagePanels.flatMap((group) => extractPageBlocksFallback(group, pageNum, false, detectTables, lex));
|
|
@@ -9264,6 +10130,29 @@ export {
|
|
|
9264
10130
|
ensureOcrModelsFor,
|
|
9265
10131
|
ocrModelsCached,
|
|
9266
10132
|
parseCharacterDict,
|
|
10133
|
+
grayCrop,
|
|
10134
|
+
inkStats,
|
|
10135
|
+
splitRowBands,
|
|
10136
|
+
leadingTriangle,
|
|
10137
|
+
leaderRuns,
|
|
10138
|
+
inkBounds,
|
|
10139
|
+
bracketFeatures,
|
|
10140
|
+
romanStems,
|
|
10141
|
+
circledAt,
|
|
10142
|
+
quoteHead,
|
|
10143
|
+
edgeTrim,
|
|
10144
|
+
tallInkCount,
|
|
10145
|
+
gapGlyphs,
|
|
10146
|
+
ringBullet,
|
|
10147
|
+
dashRatio,
|
|
10148
|
+
discAt,
|
|
10149
|
+
serifOne,
|
|
10150
|
+
starRun,
|
|
10151
|
+
bracketShape,
|
|
10152
|
+
leadingBullet,
|
|
10153
|
+
rememberOcrTextGaps,
|
|
10154
|
+
copyOcrTextGaps,
|
|
10155
|
+
findOcrTextGaps,
|
|
9267
10156
|
extractLines,
|
|
9268
10157
|
extractImageRegions,
|
|
9269
10158
|
NO_EDGES,
|
|
@@ -9318,4 +10207,4 @@ export {
|
|
|
9318
10207
|
UNRULED_REGION_TABLES,
|
|
9319
10208
|
mergeOcrImageRegions
|
|
9320
10209
|
};
|
|
9321
|
-
//# sourceMappingURL=chunk-
|
|
10210
|
+
//# sourceMappingURL=chunk-DPB6ZVQ5.js.map
|