@json-to-office/jto-ops 6.2.0 → 6.8.0
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/dist/index.d.ts +66 -3
- package/dist/index.js +380 -52
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -2184,43 +2184,77 @@ function indexDocument(pages, exclude = /* @__PURE__ */ new Set()) {
|
|
|
2184
2184
|
const fragments = [];
|
|
2185
2185
|
const positions = [];
|
|
2186
2186
|
let cursor = 0;
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2187
|
+
const push = (pageIndex, index) => {
|
|
2188
|
+
const word = pages[pageIndex].words[index];
|
|
2189
|
+
const key = `${pageIndex}:${index}`;
|
|
2190
|
+
const fragment = exclude.has(key) ? "" : normalizeForMatch(word.text);
|
|
2191
|
+
refs.push({ pageIndex, word: index });
|
|
2192
|
+
fragments.push(fragment);
|
|
2193
|
+
positions.push(cursor);
|
|
2194
|
+
cursor += fragment.length;
|
|
2195
|
+
};
|
|
2196
|
+
const orders = pages.map((page) => pageOrder(page.words, page.lines));
|
|
2197
|
+
orders.forEach((order, pageIndex) => {
|
|
2198
|
+
for (const index of order.upright) push(pageIndex, index);
|
|
2197
2199
|
});
|
|
2198
|
-
|
|
2200
|
+
const rotatedFrom = cursor;
|
|
2201
|
+
orders.forEach((order, pageIndex) => {
|
|
2202
|
+
for (const index of order.rotated) push(pageIndex, index);
|
|
2203
|
+
});
|
|
2204
|
+
return {
|
|
2205
|
+
pages,
|
|
2206
|
+
refs,
|
|
2207
|
+
fragments,
|
|
2208
|
+
positions,
|
|
2209
|
+
stream: fragments.join(""),
|
|
2210
|
+
rotatedFrom
|
|
2211
|
+
};
|
|
2199
2212
|
}
|
|
2200
|
-
function readingOrder(words2) {
|
|
2213
|
+
function readingOrder(words2, lines = []) {
|
|
2214
|
+
const { upright, rotated } = pageOrder(words2, lines);
|
|
2215
|
+
return [...upright, ...rotated];
|
|
2216
|
+
}
|
|
2217
|
+
function pageOrder(words2, lines = []) {
|
|
2218
|
+
const rotated = rotatedWords(words2, lines);
|
|
2219
|
+
return {
|
|
2220
|
+
upright: uprightOrder(
|
|
2221
|
+
words2,
|
|
2222
|
+
words2.map((_, i) => i).filter((i) => !rotated.has(i))
|
|
2223
|
+
),
|
|
2224
|
+
rotated: [...rotated].sort((a, b) => a - b)
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
function rotatedWords(words2, lines) {
|
|
2201
2228
|
const rotated = new Set(
|
|
2202
2229
|
words2.map((_, i) => i).filter((i) => {
|
|
2203
2230
|
const w = words2[i];
|
|
2204
2231
|
return w.text.length > 2 && w.yMax - w.yMin > 1.5 * (w.xMax - w.xMin);
|
|
2205
2232
|
})
|
|
2206
2233
|
);
|
|
2207
|
-
const
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2234
|
+
for (const line of lines) {
|
|
2235
|
+
if (line.words.length < 2) continue;
|
|
2236
|
+
const stacked = line.words.slice(1).every((index, n) => {
|
|
2237
|
+
const a = words2[line.words[n]];
|
|
2238
|
+
const b = words2[index];
|
|
2239
|
+
const across = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
|
|
2240
|
+
const along = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
|
|
2241
|
+
return across > 0 && along <= 0;
|
|
2242
|
+
});
|
|
2243
|
+
if (stacked) for (const index of line.words) rotated.add(index);
|
|
2216
2244
|
}
|
|
2217
|
-
return
|
|
2245
|
+
return rotated;
|
|
2218
2246
|
}
|
|
2219
2247
|
function median(values) {
|
|
2220
2248
|
const sorted = [...values].sort((a, b) => a - b);
|
|
2221
2249
|
const mid = sorted.length >> 1;
|
|
2222
2250
|
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
2223
2251
|
}
|
|
2252
|
+
var ROW_SIZE_RATIO = 2;
|
|
2253
|
+
function comparableSize(a, b) {
|
|
2254
|
+
const ha = a.yMax - a.yMin;
|
|
2255
|
+
const hb = b.yMax - b.yMin;
|
|
2256
|
+
return Math.max(ha, hb) <= ROW_SIZE_RATIO * Math.min(ha, hb);
|
|
2257
|
+
}
|
|
2224
2258
|
function groupRows(words2, indices) {
|
|
2225
2259
|
const rows = [];
|
|
2226
2260
|
const byY = [...indices].sort(
|
|
@@ -2229,7 +2263,8 @@ function groupRows(words2, indices) {
|
|
|
2229
2263
|
for (const i of byY) {
|
|
2230
2264
|
const word = words2[i];
|
|
2231
2265
|
const row = rows[rows.length - 1];
|
|
2232
|
-
|
|
2266
|
+
const peer = row?.words.find((w) => comparableSize(words2[w], word));
|
|
2267
|
+
if (row && peer !== void 0 && sameRow(words2[peer], word)) {
|
|
2233
2268
|
row.words.push(i);
|
|
2234
2269
|
row.yMin = Math.min(row.yMin, word.yMin);
|
|
2235
2270
|
row.yMax = Math.max(row.yMax, word.yMax);
|
|
@@ -2267,20 +2302,25 @@ function uprightOrder(words2, indices) {
|
|
|
2267
2302
|
const fragments = rows.map(fragmentsOf);
|
|
2268
2303
|
const boxes = grouped;
|
|
2269
2304
|
const order = [];
|
|
2305
|
+
const read = /* @__PURE__ */ new Set();
|
|
2306
|
+
const readColumn = (column) => {
|
|
2307
|
+
column.fragments.sort(
|
|
2308
|
+
(a, b) => words2[a.words[0]].yMin - words2[b.words[0]].yMin || a.xMin - b.xMin
|
|
2309
|
+
);
|
|
2310
|
+
for (const f of column.fragments) {
|
|
2311
|
+
order.push(...f.words);
|
|
2312
|
+
read.add(f);
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2270
2315
|
let cluster = null;
|
|
2271
2316
|
const flush = () => {
|
|
2272
2317
|
if (!cluster) return;
|
|
2273
2318
|
if (cluster.columns.length < 2) {
|
|
2274
2319
|
for (const r of cluster.rows)
|
|
2275
|
-
for (const f of fragments[r]) order.push(...f.words);
|
|
2320
|
+
for (const f of fragments[r]) if (!read.has(f)) order.push(...f.words);
|
|
2276
2321
|
} else {
|
|
2277
2322
|
cluster.columns.sort((a, b) => a.xMin - b.xMin);
|
|
2278
|
-
for (const column of cluster.columns)
|
|
2279
|
-
column.fragments.sort(
|
|
2280
|
-
(a, b) => words2[a.words[0]].yMin - words2[b.words[0]].yMin || a.xMin - b.xMin
|
|
2281
|
-
);
|
|
2282
|
-
for (const f of column.fragments) order.push(...f.words);
|
|
2283
|
-
}
|
|
2323
|
+
for (const column of cluster.columns) readColumn(column);
|
|
2284
2324
|
}
|
|
2285
2325
|
cluster = null;
|
|
2286
2326
|
};
|
|
@@ -2294,6 +2334,16 @@ function uprightOrder(words2, indices) {
|
|
|
2294
2334
|
if (!adjacent || wide) flush();
|
|
2295
2335
|
if (!cluster) cluster = { rows: [], columns: [] };
|
|
2296
2336
|
cluster.rows.push(r);
|
|
2337
|
+
const landing = /* @__PURE__ */ new Map();
|
|
2338
|
+
for (const f of fragments[r]) {
|
|
2339
|
+
const hit = overlapping(cluster.columns, f)[0];
|
|
2340
|
+
if (hit) landing.set(hit, (landing.get(hit) ?? 0) + 1);
|
|
2341
|
+
}
|
|
2342
|
+
for (const [column, count] of landing) {
|
|
2343
|
+
if (count < 2) continue;
|
|
2344
|
+
readColumn(column);
|
|
2345
|
+
cluster.columns.splice(cluster.columns.indexOf(column), 1);
|
|
2346
|
+
}
|
|
2297
2347
|
for (const f of fragments[r]) {
|
|
2298
2348
|
const hit = overlapping(cluster.columns, f)[0];
|
|
2299
2349
|
if (hit) {
|
|
@@ -2361,6 +2411,7 @@ function occurrence(index, at, end) {
|
|
|
2361
2411
|
const parts = partsOf(index, at, end);
|
|
2362
2412
|
return {
|
|
2363
2413
|
at,
|
|
2414
|
+
end,
|
|
2364
2415
|
pageIndex: parts[0].pageIndex,
|
|
2365
2416
|
endPageIndex: parts[parts.length - 1].pageIndex,
|
|
2366
2417
|
parts
|
|
@@ -2386,7 +2437,11 @@ function findOccurrences(index, segments) {
|
|
|
2386
2437
|
end = next + segments[s].length;
|
|
2387
2438
|
}
|
|
2388
2439
|
if (!complete || !alignedToFragments(index, at, end)) continue;
|
|
2389
|
-
|
|
2440
|
+
const hit = occurrence(index, at, end);
|
|
2441
|
+
if (at < index.rotatedFrom ? end > index.rotatedFrom : hit.pageIndex !== hit.endPageIndex) {
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
hits.push(hit);
|
|
2390
2445
|
searchFrom = end;
|
|
2391
2446
|
}
|
|
2392
2447
|
return hits;
|
|
@@ -2394,7 +2449,7 @@ function findOccurrences(index, segments) {
|
|
|
2394
2449
|
var MIN_NEEDLE_LENGTH = 3;
|
|
2395
2450
|
var MIN_PARTIAL_PREFIX = 16;
|
|
2396
2451
|
var CHROME_BAND = 0.2;
|
|
2397
|
-
function longestRenderedPrefix(index, needle) {
|
|
2452
|
+
function longestRenderedPrefix(index, needle, page, region) {
|
|
2398
2453
|
if (needle.length < MIN_PARTIAL_PREFIX) return void 0;
|
|
2399
2454
|
const anchor = needle.slice(0, MIN_PARTIAL_PREFIX);
|
|
2400
2455
|
let best;
|
|
@@ -2407,14 +2462,36 @@ function longestRenderedPrefix(index, needle) {
|
|
|
2407
2462
|
if (first >= index.fragments.length || index.positions[first] !== at) {
|
|
2408
2463
|
continue;
|
|
2409
2464
|
}
|
|
2465
|
+
if (page !== void 0 && index.refs[first].pageIndex !== page) continue;
|
|
2466
|
+
const rotated = at >= index.rotatedFrom;
|
|
2467
|
+
const limit = Math.min(
|
|
2468
|
+
page !== void 0 || rotated ? pageEnd(index, first) : index.stream.length,
|
|
2469
|
+
rotated ? index.stream.length : index.rotatedFrom
|
|
2470
|
+
);
|
|
2471
|
+
if (at + MIN_PARTIAL_PREFIX > limit) continue;
|
|
2410
2472
|
let length = MIN_PARTIAL_PREFIX;
|
|
2411
|
-
while (length < needle.length && index.stream.charCodeAt(at + length) === needle.charCodeAt(length)) {
|
|
2473
|
+
while (length < needle.length && at + length < limit && index.stream.charCodeAt(at + length) === needle.charCodeAt(length)) {
|
|
2412
2474
|
length++;
|
|
2413
2475
|
}
|
|
2476
|
+
if (region && !touchesRegion(partsOf(index, at, at + length), region)) {
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2414
2479
|
if (!best || length > best.length) best = { length, at };
|
|
2415
2480
|
}
|
|
2416
2481
|
return best;
|
|
2417
2482
|
}
|
|
2483
|
+
function pageEnd(index, from) {
|
|
2484
|
+
const page = index.refs[from].pageIndex;
|
|
2485
|
+
let i = from;
|
|
2486
|
+
while (i < index.refs.length && index.refs[i].pageIndex === page) i++;
|
|
2487
|
+
return i < index.positions.length ? index.positions[i] : index.stream.length;
|
|
2488
|
+
}
|
|
2489
|
+
var REGION_TOLERANCE_PT = 2;
|
|
2490
|
+
function touchesRegion(parts, region) {
|
|
2491
|
+
return parts.some(
|
|
2492
|
+
(part) => part.xMax >= region.xMin - REGION_TOLERANCE_PT && part.xMin <= region.xMax + REGION_TOLERANCE_PT && part.yMax >= region.yMin - REGION_TOLERANCE_PT && part.yMin <= region.yMax + REGION_TOLERANCE_PT
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2418
2495
|
function sameRow(a, b) {
|
|
2419
2496
|
const overlap = Math.min(a.yMax, b.yMax) - Math.max(a.yMin, b.yMin);
|
|
2420
2497
|
const shorter = Math.min(a.yMax - a.yMin, b.yMax - b.yMin);
|
|
@@ -2470,6 +2547,96 @@ function pageNumberRows(pages, claimed) {
|
|
|
2470
2547
|
)
|
|
2471
2548
|
);
|
|
2472
2549
|
}
|
|
2550
|
+
function insideRegion(occurrences, region) {
|
|
2551
|
+
let best;
|
|
2552
|
+
for (const o of occurrences) {
|
|
2553
|
+
let inside = 0;
|
|
2554
|
+
let total = 0;
|
|
2555
|
+
for (const part of o.parts) {
|
|
2556
|
+
const w = Math.min(part.xMax, region.xMax) - Math.max(part.xMin, region.xMin);
|
|
2557
|
+
const h = Math.min(part.yMax, region.yMax) - Math.max(part.yMin, region.yMin);
|
|
2558
|
+
if (w > 0 && h > 0) inside += w * h;
|
|
2559
|
+
total += (part.xMax - part.xMin) * (part.yMax - part.yMin);
|
|
2560
|
+
}
|
|
2561
|
+
const share = total > 0 ? inside / total : 0;
|
|
2562
|
+
if (share > 0 && (!best || share > best.share)) {
|
|
2563
|
+
best = { occurrence: o, share };
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
return best?.occurrence;
|
|
2567
|
+
}
|
|
2568
|
+
function overprintedOccurrence(index, page, region, needle, claimed) {
|
|
2569
|
+
const words2 = index.pages[page]?.words;
|
|
2570
|
+
if (!words2) return void 0;
|
|
2571
|
+
const fragmentOf = /* @__PURE__ */ new Map();
|
|
2572
|
+
index.refs.forEach((ref, i) => {
|
|
2573
|
+
if (ref.pageIndex !== page || index.fragments[i] === "") return;
|
|
2574
|
+
const w = words2[ref.word];
|
|
2575
|
+
const centre = (w.xMin + w.xMax) / 2;
|
|
2576
|
+
if (centre < region.xMin || centre > region.xMax) return;
|
|
2577
|
+
if (w.yMax < region.yMin) return;
|
|
2578
|
+
fragmentOf.set(ref.word, i);
|
|
2579
|
+
});
|
|
2580
|
+
const sequence = groupRows(words2, [...fragmentOf.keys()]).flatMap(
|
|
2581
|
+
(row) => [...row.words].sort((a, b) => words2[a].xMin - words2[b].xMin)
|
|
2582
|
+
);
|
|
2583
|
+
let reach = /* @__PURE__ */ new Map();
|
|
2584
|
+
let best;
|
|
2585
|
+
for (const word of sequence) {
|
|
2586
|
+
const i = fragmentOf.get(word);
|
|
2587
|
+
const text = index.fragments[i];
|
|
2588
|
+
const usable = !claimed(index.positions[i]);
|
|
2589
|
+
const next = /* @__PURE__ */ new Map();
|
|
2590
|
+
const offer = (consumed, path9) => {
|
|
2591
|
+
const held = next.get(consumed);
|
|
2592
|
+
if (!held || path9.skips < held.skips) next.set(consumed, path9);
|
|
2593
|
+
};
|
|
2594
|
+
const inside = words2[word].yMin + words2[word].yMax <= 2 * region.yMax && words2[word].yMin + words2[word].yMax >= 2 * region.yMin;
|
|
2595
|
+
if (usable && inside && needle.startsWith(text)) {
|
|
2596
|
+
offer(text.length, { skips: 0, words: [word] });
|
|
2597
|
+
}
|
|
2598
|
+
for (const [consumed, path9] of reach) {
|
|
2599
|
+
if (usable && needle.startsWith(text, consumed)) {
|
|
2600
|
+
offer(consumed + text.length, {
|
|
2601
|
+
skips: path9.skips,
|
|
2602
|
+
words: [...path9.words, word]
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
offer(consumed, { skips: path9.skips + 1, words: path9.words });
|
|
2606
|
+
}
|
|
2607
|
+
const done = next.get(needle.length);
|
|
2608
|
+
if (done && (!best || done.skips < best.skips)) best = done;
|
|
2609
|
+
next.delete(needle.length);
|
|
2610
|
+
reach = next;
|
|
2611
|
+
}
|
|
2612
|
+
if (!best || best.skips > best.words.length) return void 0;
|
|
2613
|
+
const first = fragmentOf.get(best.words[0]);
|
|
2614
|
+
const part = {
|
|
2615
|
+
pageIndex: page,
|
|
2616
|
+
words: best.words,
|
|
2617
|
+
xMin: Math.min(...best.words.map((w) => words2[w].xMin)),
|
|
2618
|
+
yMin: Math.min(...best.words.map((w) => words2[w].yMin)),
|
|
2619
|
+
xMax: Math.max(...best.words.map((w) => words2[w].xMax)),
|
|
2620
|
+
yMax: Math.max(...best.words.map((w) => words2[w].yMax))
|
|
2621
|
+
};
|
|
2622
|
+
const at = index.positions[first];
|
|
2623
|
+
return {
|
|
2624
|
+
occurrence: {
|
|
2625
|
+
at,
|
|
2626
|
+
end: at,
|
|
2627
|
+
pageIndex: page,
|
|
2628
|
+
endPageIndex: page,
|
|
2629
|
+
parts: [part]
|
|
2630
|
+
},
|
|
2631
|
+
claims: best.words.map((w) => {
|
|
2632
|
+
const i = fragmentOf.get(w);
|
|
2633
|
+
return {
|
|
2634
|
+
at: index.positions[i],
|
|
2635
|
+
end: index.positions[i] + index.fragments[i].length
|
|
2636
|
+
};
|
|
2637
|
+
})
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2473
2640
|
function assignInventory(pages, inventory) {
|
|
2474
2641
|
const results = /* @__PURE__ */ new Map();
|
|
2475
2642
|
const chromeWords = /* @__PURE__ */ new Set();
|
|
@@ -2509,7 +2676,8 @@ function assignInventory(pages, inventory) {
|
|
|
2509
2676
|
for (const row of pageNumberRows(pages, chromeWords))
|
|
2510
2677
|
claimRow(row.pageIndex, row.box);
|
|
2511
2678
|
const body = indexDocument(pages, chromeWords);
|
|
2512
|
-
const
|
|
2679
|
+
const claims = [];
|
|
2680
|
+
const claimsOver = (o) => claims.filter((claim) => o.at < claim.end && claim.at < o.end);
|
|
2513
2681
|
let cursor = 0;
|
|
2514
2682
|
for (const entry of inventory) {
|
|
2515
2683
|
if (entry.repeats) continue;
|
|
@@ -2519,10 +2687,31 @@ function assignInventory(pages, inventory) {
|
|
|
2519
2687
|
results.set(entry, { entry, needle, status: "skipped", occurrences: [] });
|
|
2520
2688
|
continue;
|
|
2521
2689
|
}
|
|
2522
|
-
let all = findOccurrences(body, segments)
|
|
2690
|
+
let all = findOccurrences(body, segments).filter(
|
|
2691
|
+
(o) => (entry.page === void 0 || o.pageIndex === entry.page && o.endPageIndex === entry.page) && (!entry.region || touchesRegion(o.parts, entry.region))
|
|
2692
|
+
);
|
|
2523
2693
|
let partial;
|
|
2694
|
+
let sparse;
|
|
2695
|
+
if (all.length === 0 && segments.length === 1 && entry.page !== void 0 && entry.region) {
|
|
2696
|
+
const overprinted = overprintedOccurrence(
|
|
2697
|
+
body,
|
|
2698
|
+
entry.page,
|
|
2699
|
+
entry.region,
|
|
2700
|
+
needle,
|
|
2701
|
+
(offset) => claims.some((claim) => claim.at <= offset && offset < claim.end)
|
|
2702
|
+
);
|
|
2703
|
+
if (overprinted) {
|
|
2704
|
+
all = [overprinted.occurrence];
|
|
2705
|
+
sparse = overprinted.claims;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2524
2708
|
if (all.length === 0 && segments.length === 1 && !entry.optional) {
|
|
2525
|
-
const prefix = longestRenderedPrefix(
|
|
2709
|
+
const prefix = longestRenderedPrefix(
|
|
2710
|
+
body,
|
|
2711
|
+
needle,
|
|
2712
|
+
entry.page,
|
|
2713
|
+
entry.region
|
|
2714
|
+
);
|
|
2526
2715
|
if (prefix) {
|
|
2527
2716
|
all = [occurrence(body, prefix.at, prefix.at + prefix.length)];
|
|
2528
2717
|
partial = { matchedChars: prefix.length, totalChars: needle.length };
|
|
@@ -2537,18 +2726,22 @@ function assignInventory(pages, inventory) {
|
|
|
2537
2726
|
});
|
|
2538
2727
|
continue;
|
|
2539
2728
|
}
|
|
2540
|
-
let free = all.filter((o) =>
|
|
2729
|
+
let free = all.filter((o) => claimsOver(o).length === 0);
|
|
2541
2730
|
if (free.length === 0 && !entry.optional) {
|
|
2542
|
-
const held = all.find((o) =>
|
|
2731
|
+
const held = all.find((o) => {
|
|
2732
|
+
const over = claimsOver(o);
|
|
2733
|
+
return over.length > 0 && over.every((claim) => claim.entry.optional);
|
|
2734
|
+
});
|
|
2543
2735
|
if (held) {
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2736
|
+
for (const claim of claimsOver(held)) {
|
|
2737
|
+
claims.splice(claims.indexOf(claim), 1);
|
|
2738
|
+
results.set(claim.entry, {
|
|
2739
|
+
entry: claim.entry,
|
|
2740
|
+
needle: results.get(claim.entry)?.needle ?? "",
|
|
2741
|
+
status: "skipped",
|
|
2742
|
+
occurrences: []
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2552
2745
|
free = [held];
|
|
2553
2746
|
}
|
|
2554
2747
|
}
|
|
@@ -2561,9 +2754,10 @@ function assignInventory(pages, inventory) {
|
|
|
2561
2754
|
});
|
|
2562
2755
|
continue;
|
|
2563
2756
|
}
|
|
2564
|
-
const chosen = free.find((o) => o.at >= cursor) ?? free[0];
|
|
2565
|
-
|
|
2566
|
-
|
|
2757
|
+
const chosen = (entry.region && insideRegion(free, entry.region)) ?? free.find((o) => o.at >= cursor) ?? free[0];
|
|
2758
|
+
for (const range of sparse ?? [chosen])
|
|
2759
|
+
claims.push({ at: range.at, end: range.end, entry });
|
|
2760
|
+
if (chosen.at < body.rotatedFrom) cursor = chosen.at;
|
|
2567
2761
|
results.set(entry, {
|
|
2568
2762
|
entry,
|
|
2569
2763
|
needle,
|
|
@@ -2595,6 +2789,11 @@ function renderedDraftFor(fact) {
|
|
|
2595
2789
|
}
|
|
2596
2790
|
var VISIBLE_SPILL_PT = 2;
|
|
2597
2791
|
var OVERLAP_FRACTION = 0.3;
|
|
2792
|
+
var WORD_BOX_SLACK = 0.2;
|
|
2793
|
+
function inkBand(word) {
|
|
2794
|
+
const slack = WORD_BOX_SLACK * Math.max(0, word.yMax - word.yMin);
|
|
2795
|
+
return { yMin: word.yMin + slack, yMax: word.yMax - slack };
|
|
2796
|
+
}
|
|
2598
2797
|
function finding(draft) {
|
|
2599
2798
|
const { mapping, page, context, ...rest } = draft;
|
|
2600
2799
|
return {
|
|
@@ -2610,12 +2809,15 @@ function round(value) {
|
|
|
2610
2809
|
return Math.round(value * 10) / 10;
|
|
2611
2810
|
}
|
|
2612
2811
|
function intersects(a, b) {
|
|
2812
|
+
const inkA = inkBand(a);
|
|
2813
|
+
const inkB = inkBand(b);
|
|
2613
2814
|
const w = Math.min(a.xMax, b.xMax) - Math.max(a.xMin, b.xMin);
|
|
2614
|
-
const h = Math.min(
|
|
2815
|
+
const h = Math.min(inkA.yMax, inkB.yMax) - Math.max(inkA.yMin, inkB.yMin);
|
|
2615
2816
|
return w > 0 && h > 0 ? w * h : 0;
|
|
2616
2817
|
}
|
|
2617
2818
|
function area(w) {
|
|
2618
|
-
|
|
2819
|
+
const ink = inkBand(w);
|
|
2820
|
+
return Math.max(0, w.xMax - w.xMin) * Math.max(0, ink.yMax - ink.yMin);
|
|
2619
2821
|
}
|
|
2620
2822
|
function excerpt(text, max = 60) {
|
|
2621
2823
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
@@ -2731,14 +2933,51 @@ function draftRenderedFindings(input) {
|
|
|
2731
2933
|
}
|
|
2732
2934
|
for (const match of matches) {
|
|
2733
2935
|
const box = match.entry.box;
|
|
2936
|
+
const region = match.entry.region;
|
|
2734
2937
|
if (!box || match.status !== "mapped") continue;
|
|
2735
2938
|
for (const o of match.occurrences) {
|
|
2939
|
+
const boxHeight = box.heightPt;
|
|
2940
|
+
const byEdges = region !== void 0 && boxHeight !== void 0;
|
|
2941
|
+
if (region && boxHeight !== void 0) {
|
|
2942
|
+
const words2 = o.parts.flatMap(
|
|
2943
|
+
(part) => part.words.map((w) => pages[part.pageIndex].words[w])
|
|
2944
|
+
);
|
|
2945
|
+
const inkTop = Math.min(...words2.map((w) => inkBand(w).yMin));
|
|
2946
|
+
const inkBottom = Math.max(...words2.map((w) => inkBand(w).yMax));
|
|
2947
|
+
const edges = [
|
|
2948
|
+
{ edge: "bottom", over: inkBottom - region.yMax, at: inkBottom },
|
|
2949
|
+
{ edge: "top", over: region.yMin - inkTop, at: inkTop }
|
|
2950
|
+
].sort((a, b) => b.over - a.over);
|
|
2951
|
+
const [worst2] = edges;
|
|
2952
|
+
if (worst2.over > VISIBLE_SPILL_PT) {
|
|
2953
|
+
findings.push(
|
|
2954
|
+
finding({
|
|
2955
|
+
ruleId: "rendered/spill",
|
|
2956
|
+
mapping: "mapped",
|
|
2957
|
+
page: o.pageIndex + 1,
|
|
2958
|
+
path: match.entry.path,
|
|
2959
|
+
message: `"${excerpt(match.entry.text)}" runs ${round(worst2.over)} pt past the ${worst2.edge} of its ${round(boxHeight)} pt box on page ${o.pageIndex + 1}.`,
|
|
2960
|
+
suggestion: "Shorten the text, reduce its size, or enlarge the box; the renderer let it spill past the edge the author drew.",
|
|
2961
|
+
evidence: {
|
|
2962
|
+
summary: "Rendered ink against the edge of the declared box",
|
|
2963
|
+
actual: round(worst2.at),
|
|
2964
|
+
expected: round(
|
|
2965
|
+
worst2.edge === "bottom" ? region.yMax : region.yMin
|
|
2966
|
+
),
|
|
2967
|
+
unit: "pt",
|
|
2968
|
+
values: { edge: worst2.edge, overPt: round(worst2.over) }
|
|
2969
|
+
}
|
|
2970
|
+
})
|
|
2971
|
+
);
|
|
2972
|
+
continue;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2736
2975
|
const extent = unionBox(o.parts);
|
|
2737
2976
|
const rendered = {
|
|
2738
2977
|
heightPt: extent.yMax - extent.yMin,
|
|
2739
2978
|
widthPt: extent.xMax - extent.xMin
|
|
2740
2979
|
};
|
|
2741
|
-
const worst = ["heightPt", "widthPt"].filter((axis) => box[axis] !== void 0).map((axis) => ({
|
|
2980
|
+
const worst = ["heightPt", "widthPt"].filter((axis) => box[axis] !== void 0).filter((axis) => !(byEdges && axis === "heightPt")).map((axis) => ({
|
|
2742
2981
|
axis,
|
|
2743
2982
|
declared: box[axis],
|
|
2744
2983
|
rendered: rendered[axis],
|
|
@@ -3152,6 +3391,93 @@ function analyzeRenderedDocument(input, options = {}) {
|
|
|
3152
3391
|
};
|
|
3153
3392
|
}
|
|
3154
3393
|
|
|
3394
|
+
// src/rendered-inventory.ts
|
|
3395
|
+
function renderedInventoryFromFacts(format, facts) {
|
|
3396
|
+
const entries = [];
|
|
3397
|
+
for (const fact of facts) {
|
|
3398
|
+
if (format === "docx" && fact.kind === "docx/text") {
|
|
3399
|
+
const frame = fact.frame;
|
|
3400
|
+
entries.push({
|
|
3401
|
+
path: fact.path,
|
|
3402
|
+
text: String(fact.text),
|
|
3403
|
+
role: fact.role,
|
|
3404
|
+
...typeof fact.level === "number" && { level: fact.level },
|
|
3405
|
+
...fact.repeats === true && { repeats: true },
|
|
3406
|
+
...fact.optional === true && { optional: true },
|
|
3407
|
+
...frame && {
|
|
3408
|
+
box: {
|
|
3409
|
+
...frame.widthPt !== void 0 && { widthPt: frame.widthPt },
|
|
3410
|
+
...frame.heightPt !== void 0 && { heightPt: frame.heightPt }
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
} else if (format === "pptx" && fact.kind === "pptx/text") {
|
|
3415
|
+
if (fact.slideHidden === true) continue;
|
|
3416
|
+
const turned = typeof fact.rotationDeg === "number" && (fact.rotationDeg % 360 + 360) % 360 !== 0;
|
|
3417
|
+
if (turned) {
|
|
3418
|
+
entries.push({
|
|
3419
|
+
path: fact.path,
|
|
3420
|
+
text: String(fact.text),
|
|
3421
|
+
role: "slide-text",
|
|
3422
|
+
optional: true,
|
|
3423
|
+
...typeof fact.page === "number" && { page: fact.page }
|
|
3424
|
+
});
|
|
3425
|
+
continue;
|
|
3426
|
+
}
|
|
3427
|
+
const { boxXPt: x, boxYPt: y } = fact;
|
|
3428
|
+
const widthPt = fact.boxWidthPt;
|
|
3429
|
+
const heightPt = fact.boxHeightPt;
|
|
3430
|
+
entries.push({
|
|
3431
|
+
path: fact.path,
|
|
3432
|
+
text: String(fact.text),
|
|
3433
|
+
role: "slide-text",
|
|
3434
|
+
// A slide is its own page and its boxes are where the deck put
|
|
3435
|
+
// them, so the text is looked for there before anywhere else.
|
|
3436
|
+
...typeof fact.page === "number" && { page: fact.page },
|
|
3437
|
+
...typeof x === "number" && typeof y === "number" && typeof widthPt === "number" && typeof heightPt === "number" && {
|
|
3438
|
+
region: {
|
|
3439
|
+
xMin: x,
|
|
3440
|
+
yMin: y,
|
|
3441
|
+
xMax: x + widthPt,
|
|
3442
|
+
yMax: y + heightPt
|
|
3443
|
+
}
|
|
3444
|
+
},
|
|
3445
|
+
...(typeof widthPt === "number" || typeof heightPt === "number") && {
|
|
3446
|
+
box: {
|
|
3447
|
+
...typeof widthPt === "number" && { widthPt },
|
|
3448
|
+
...typeof heightPt === "number" && { heightPt }
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
return entries;
|
|
3455
|
+
}
|
|
3456
|
+
function requestedFontsFromFacts(format, facts, resolvedFonts) {
|
|
3457
|
+
const kind = format === "docx" ? "docx/font-family" : "pptx/font-family";
|
|
3458
|
+
const declared = new Set(
|
|
3459
|
+
resolvedFonts.filter((font) => font.declared).map((font) => font.family.trim().toLowerCase())
|
|
3460
|
+
);
|
|
3461
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3462
|
+
const requested = [];
|
|
3463
|
+
const add = (family, path9) => {
|
|
3464
|
+
if (typeof family !== "string" || family.trim() === "") return;
|
|
3465
|
+
const key = family.trim().toLowerCase();
|
|
3466
|
+
if (seen.has(key)) return;
|
|
3467
|
+
seen.add(key);
|
|
3468
|
+
requested.push({
|
|
3469
|
+
family: family.trim(),
|
|
3470
|
+
...path9 && { path: path9 },
|
|
3471
|
+
...declared.has(key) && { declared: true }
|
|
3472
|
+
});
|
|
3473
|
+
};
|
|
3474
|
+
for (const fact of facts) {
|
|
3475
|
+
if (fact.kind === kind) add(fact.family, fact.path);
|
|
3476
|
+
}
|
|
3477
|
+
for (const font of resolvedFonts) if (font.declared) add(font.family);
|
|
3478
|
+
return requested;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3155
3481
|
// src/block-matrix.ts
|
|
3156
3482
|
import {
|
|
3157
3483
|
blockDependencies,
|
|
@@ -3598,7 +3924,9 @@ export {
|
|
|
3598
3924
|
pdffontsAvailable,
|
|
3599
3925
|
pdftotextAvailable,
|
|
3600
3926
|
readingOrder,
|
|
3927
|
+
renderedInventoryFromFacts,
|
|
3601
3928
|
reportCaseDocument,
|
|
3929
|
+
requestedFontsFromFacts,
|
|
3602
3930
|
runWithDiagnosticSink,
|
|
3603
3931
|
stderrDiagnosticSink,
|
|
3604
3932
|
widestNumber
|