@embedpdf/engine-core 3.0.0-next.1 → 3.0.0-next.10
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/{AbortError-CjSasIlL.js → AbortError-8-9JAX1Y.js} +2 -2
- package/dist/{AbortError-CjSasIlL.js.map → AbortError-8-9JAX1Y.js.map} +1 -1
- package/dist/{AbortError-ByBflvEl.cjs → AbortError-CRSooKsk.cjs} +2 -2
- package/dist/{AbortError-ByBflvEl.cjs.map → AbortError-CRSooKsk.cjs.map} +1 -1
- package/dist/{AnnotationListSnapshot-W0RCsyDE.d.cts → AnnotationListSnapshot-DDxHpJQU.d.cts} +538 -41
- package/dist/{AnnotationListSnapshot-W0RCsyDE.d.ts → AnnotationListSnapshot-DDxHpJQU.d.ts} +538 -41
- package/dist/{DocumentSecurityService-Df79fgpe.d.cts → DocumentSecurityService-BV4_YXkU.d.cts} +33 -2
- package/dist/{DocumentSecurityService-B-tULKNY.d.ts → DocumentSecurityService-o9aeTHwT.d.ts} +33 -2
- package/dist/{Engine-D9loFZc1.d.cts → Engine-D9je6GdU.d.cts} +50 -19
- package/dist/{Engine-B7Oe7f5i.d.ts → Engine-DOoVem7_.d.ts} +50 -19
- package/dist/{subtype-qejjVr6h.cjs → PageInsertBlankInput-CzShuSJI.cjs} +184 -1
- package/dist/PageInsertBlankInput-CzShuSJI.cjs.map +1 -0
- package/dist/{subtype-BvAzLCGC.js → PageInsertBlankInput-WKY0tr6S.js} +149 -2
- package/dist/PageInsertBlankInput-WKY0tr6S.js.map +1 -0
- package/dist/{builders-C548U47M.js → builders-B_KufGJ9.js} +547 -169
- package/dist/builders-B_KufGJ9.js.map +1 -0
- package/dist/{builders-BvikWRuO.cjs → builders-Cp_rlGob.cjs} +603 -225
- package/dist/builders-Cp_rlGob.cjs.map +1 -0
- package/dist/conformance.cjs +822 -198
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +124 -6
- package/dist/conformance.d.ts +124 -6
- package/dist/conformance.js +777 -157
- package/dist/conformance.js.map +1 -1
- package/dist/{fold-CB4fAOOy.js → fold-BfyxxwS6.js} +162 -3
- package/dist/fold-BfyxxwS6.js.map +1 -0
- package/dist/{fold-IaMph3CO.cjs → fold-C7I5GpBz.cjs} +235 -4
- package/dist/fold-C7I5GpBz.cjs.map +1 -0
- package/dist/index.cjs +36 -18
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/runtime.cjs +41 -23
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +103 -5
- package/dist/runtime.d.ts +103 -5
- package/dist/runtime.js +5 -5
- package/dist/{schemas-B_1VyFsm.cjs → schemas-D1pxEv3t.cjs} +138 -32
- package/dist/schemas-D1pxEv3t.cjs.map +1 -0
- package/dist/{schemas-9OpSSeM6.js → schemas-DAnu8tuW.js} +95 -13
- package/dist/schemas-DAnu8tuW.js.map +1 -0
- package/dist/wire.cjs +89 -3
- package/dist/wire.cjs.map +1 -1
- package/dist/wire.d.cts +99 -15
- package/dist/wire.d.ts +99 -15
- package/dist/wire.js +83 -4
- package/dist/wire.js.map +1 -1
- package/package.json +1 -1
- package/dist/builders-BvikWRuO.cjs.map +0 -1
- package/dist/builders-C548U47M.js.map +0 -1
- package/dist/fold-CB4fAOOy.js.map +0 -1
- package/dist/fold-IaMph3CO.cjs.map +0 -1
- package/dist/schemas-9OpSSeM6.js.map +0 -1
- package/dist/schemas-B_1VyFsm.cjs.map +0 -1
- package/dist/subtype-BvAzLCGC.js.map +0 -1
- package/dist/subtype-qejjVr6h.cjs.map +0 -1
|
@@ -1,120 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
//#region src/geometry/convert.ts
|
|
4
|
-
/**
|
|
5
|
-
* Normalize a `PdfRect` to the y-up invariant (`left <= right`, `bottom <= top`),
|
|
6
|
-
* regardless of how the PDF ordered the corners (a `/Rect` array or `FS_RECTF`
|
|
7
|
-
* may carry them in any order). THE enforcement point for the "PdfRect is always
|
|
8
|
-
* normalized" rule: every producer that reads a raw box from PDFium passes it
|
|
9
|
-
* through here, so the invariant holds once, centrally — downstream `width =
|
|
10
|
-
* right - left` is never negative.
|
|
11
|
-
*/
|
|
12
|
-
function normalizePdfRect(r) {
|
|
13
|
-
return {
|
|
14
|
-
left: Math.min(r.left, r.right),
|
|
15
|
-
right: Math.max(r.left, r.right),
|
|
16
|
-
bottom: Math.min(r.top, r.bottom),
|
|
17
|
-
top: Math.max(r.top, r.bottom)
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
function pdfRectWidth(r) {
|
|
21
|
-
return r.right - r.left;
|
|
22
|
-
}
|
|
23
|
-
function pdfRectHeight(r) {
|
|
24
|
-
return r.top - r.bottom;
|
|
25
|
-
}
|
|
26
|
-
function pdfRectSize(r) {
|
|
27
|
-
return {
|
|
28
|
-
width: r.right - r.left,
|
|
29
|
-
height: r.top - r.bottom
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/** Convert edges -> origin+size, staying y-up (origin at bottom-left). */
|
|
33
|
-
function pdfRectToOriginSize(r) {
|
|
34
|
-
return {
|
|
35
|
-
x: r.left,
|
|
36
|
-
y: r.bottom,
|
|
37
|
-
width: r.right - r.left,
|
|
38
|
-
height: r.top - r.bottom
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
/** Convert y-up origin+size back to edges. */
|
|
42
|
-
function pdfRectFromOriginSize(o) {
|
|
43
|
-
return {
|
|
44
|
-
left: o.x,
|
|
45
|
-
bottom: o.y,
|
|
46
|
-
right: o.x + o.width,
|
|
47
|
-
top: o.y + o.height
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Enclosing axis-aligned box of a quad, in PDF user space. Orientation
|
|
52
|
-
* agnostic (correct for rotated/skewed quads) — this is what highlight
|
|
53
|
-
* rendering and hit-testing want.
|
|
54
|
-
*/
|
|
55
|
-
function pdfQuadBounds(q) {
|
|
56
|
-
const xs = [
|
|
57
|
-
q.p1.x,
|
|
58
|
-
q.p2.x,
|
|
59
|
-
q.p3.x,
|
|
60
|
-
q.p4.x
|
|
61
|
-
];
|
|
62
|
-
const ys = [
|
|
63
|
-
q.p1.y,
|
|
64
|
-
q.p2.y,
|
|
65
|
-
q.p3.y,
|
|
66
|
-
q.p4.y
|
|
67
|
-
];
|
|
68
|
-
return {
|
|
69
|
-
left: Math.min(...xs),
|
|
70
|
-
right: Math.max(...xs),
|
|
71
|
-
bottom: Math.min(...ys),
|
|
72
|
-
top: Math.max(...ys)
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Axis-aligned named-corner interpretation of a quad.
|
|
77
|
-
*
|
|
78
|
-
* VALID ONLY for upright (non-rotated, non-skewed) quads: it is derived from
|
|
79
|
-
* the enclosing bounds, so for rotated/skewed quads it returns the bounding
|
|
80
|
-
* box corners, not the true geometric corners. For arbitrary quads use the
|
|
81
|
-
* positional `p1..p4` or `pdfQuadBounds` directly.
|
|
82
|
-
*/
|
|
83
|
-
function pdfQuadCorners(q) {
|
|
84
|
-
const b = pdfQuadBounds(q);
|
|
85
|
-
return {
|
|
86
|
-
topLeft: {
|
|
87
|
-
x: b.left,
|
|
88
|
-
y: b.top
|
|
89
|
-
},
|
|
90
|
-
topRight: {
|
|
91
|
-
x: b.right,
|
|
92
|
-
y: b.top
|
|
93
|
-
},
|
|
94
|
-
bottomLeft: {
|
|
95
|
-
x: b.left,
|
|
96
|
-
y: b.bottom
|
|
97
|
-
},
|
|
98
|
-
bottomRight: {
|
|
99
|
-
x: b.right,
|
|
100
|
-
y: b.bottom
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Build a quad from named corners, in PDFium `FS_QUADPOINTSF` slot order
|
|
106
|
-
* (`p1 = topLeft`, `p2 = topRight`, `p3 = bottomLeft`, `p4 = bottomRight`).
|
|
107
|
-
* The inverse of `pdfQuadCorners` for upright quads.
|
|
108
|
-
*/
|
|
109
|
-
function pdfQuadFromCorners(c) {
|
|
110
|
-
return {
|
|
111
|
-
p1: c.topLeft,
|
|
112
|
-
p2: c.topRight,
|
|
113
|
-
p3: c.bottomLeft,
|
|
114
|
-
p4: c.bottomRight
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
1
|
+
import { i as EngineError, n as foldText, r as toOriginalRange, u as isRotatedGeometryRun } from "./fold-BfyxxwS6.js";
|
|
2
|
+
import { f as EngineErrorCode } from "./PageInsertBlankInput-WKY0tr6S.js";
|
|
118
3
|
//#region src/dto/PageRender.ts
|
|
119
4
|
function createPageImageHandle(result, blobSource) {
|
|
120
5
|
return {
|
|
@@ -524,6 +409,164 @@ function buildThreads(annotations) {
|
|
|
524
409
|
return threads;
|
|
525
410
|
}
|
|
526
411
|
//#endregion
|
|
412
|
+
//#region src/annotation/comments.ts
|
|
413
|
+
/** Subtypes that never participate in comment threads. `unsupported`
|
|
414
|
+
* covers `/Popup` dictionaries (raw code 16) and unreadable foreign
|
|
415
|
+
* blobs — neither makes a meaningful comment card. */
|
|
416
|
+
const EXCLUDED_SUBTYPES = /* @__PURE__ */ new Set([
|
|
417
|
+
"widget",
|
|
418
|
+
"link",
|
|
419
|
+
"unsupported"
|
|
420
|
+
]);
|
|
421
|
+
const REVIEW_STATES = /* @__PURE__ */ new Set([
|
|
422
|
+
"accepted",
|
|
423
|
+
"rejected",
|
|
424
|
+
"cancelled",
|
|
425
|
+
"completed",
|
|
426
|
+
"none"
|
|
427
|
+
]);
|
|
428
|
+
const MARKED_STATES = /* @__PURE__ */ new Set(["marked", "unmarked"]);
|
|
429
|
+
const nonEmpty = (v) => typeof v === "string" && v !== "";
|
|
430
|
+
/**
|
|
431
|
+
* An ISO 32000 §12.5.6.3 state annotation: a text annotation carrying a
|
|
432
|
+
* non-empty `/State` or `/StateModel`. Classification never relies on
|
|
433
|
+
* flags — producers disagree on how state annotations are flagged.
|
|
434
|
+
*/
|
|
435
|
+
function isStateAnnotation(a) {
|
|
436
|
+
return a.subtype === "text" && (nonEmpty(a.state) || nonEmpty(a.stateModel));
|
|
437
|
+
}
|
|
438
|
+
function buildCommentThreads(annotations, opts = {}) {
|
|
439
|
+
const eligible = annotations.filter((a) => !EXCLUDED_SUBTYPES.has(a.subtype));
|
|
440
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
441
|
+
for (const a of eligible) {
|
|
442
|
+
const key = refKey(a.ref);
|
|
443
|
+
if (!byKey.has(key)) byKey.set(key, a);
|
|
444
|
+
if (a.nm) {
|
|
445
|
+
const aliasKey = refKey({
|
|
446
|
+
kind: "nm",
|
|
447
|
+
pageObjectNumber: a.pageObjectNumber,
|
|
448
|
+
nm: a.nm
|
|
449
|
+
});
|
|
450
|
+
if (!byKey.has(aliasKey)) byKey.set(aliasKey, a);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const children = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const a of eligible) {
|
|
455
|
+
if (!a.inReplyTo) continue;
|
|
456
|
+
const parent = byKey.get(refKey(a.inReplyTo));
|
|
457
|
+
if (!parent) continue;
|
|
458
|
+
const parentKey = refKey(parent.ref);
|
|
459
|
+
const list = children.get(parentKey);
|
|
460
|
+
if (list) list.push(a);
|
|
461
|
+
else children.set(parentKey, [a]);
|
|
462
|
+
}
|
|
463
|
+
const visited = /* @__PURE__ */ new Set();
|
|
464
|
+
const threads = [];
|
|
465
|
+
const walk = (root) => {
|
|
466
|
+
const replies = [];
|
|
467
|
+
const groupedParts = [];
|
|
468
|
+
const states = [];
|
|
469
|
+
visited.add(refKey(root.ref));
|
|
470
|
+
const stack = [...children.get(refKey(root.ref)) ?? []];
|
|
471
|
+
while (stack.length > 0) {
|
|
472
|
+
const a = stack.pop();
|
|
473
|
+
const key = refKey(a.ref);
|
|
474
|
+
if (visited.has(key)) continue;
|
|
475
|
+
visited.add(key);
|
|
476
|
+
if (isStateAnnotation(a)) states.push(a);
|
|
477
|
+
else if (classifyRelation(a) === "grouped-subordinate") groupedParts.push(a);
|
|
478
|
+
else replies.push(a);
|
|
479
|
+
stack.push(...children.get(key) ?? []);
|
|
480
|
+
}
|
|
481
|
+
replies.sort(chronological);
|
|
482
|
+
states.sort(chronological);
|
|
483
|
+
threads.push({
|
|
484
|
+
root,
|
|
485
|
+
pageObjectNumber: root.pageObjectNumber,
|
|
486
|
+
replies,
|
|
487
|
+
groupedParts,
|
|
488
|
+
review: computeReview(states, opts)
|
|
489
|
+
});
|
|
490
|
+
};
|
|
491
|
+
for (const a of eligible) if (!a.inReplyTo && !isStateAnnotation(a)) walk(a);
|
|
492
|
+
for (const a of eligible) {
|
|
493
|
+
if (visited.has(refKey(a.ref))) continue;
|
|
494
|
+
if (isStateAnnotation(a)) continue;
|
|
495
|
+
walk(a);
|
|
496
|
+
}
|
|
497
|
+
return threads;
|
|
498
|
+
}
|
|
499
|
+
/** `created ?? modified` ascending; undated last; z-order tiebreak. */
|
|
500
|
+
function chronological(a, b) {
|
|
501
|
+
const at = a.created ?? a.modified;
|
|
502
|
+
const bt = b.created ?? b.modified;
|
|
503
|
+
if (at !== null && bt !== null && at !== bt) return at < bt ? -1 : 1;
|
|
504
|
+
if (at !== null && bt === null) return -1;
|
|
505
|
+
if (at === null && bt !== null) return 1;
|
|
506
|
+
return a.index - b.index;
|
|
507
|
+
}
|
|
508
|
+
function computeReview(states, opts) {
|
|
509
|
+
const byReviewer = {};
|
|
510
|
+
const markedLatest = {};
|
|
511
|
+
let lastChange = null;
|
|
512
|
+
for (const a of states) {
|
|
513
|
+
const status = toReviewStatus(a);
|
|
514
|
+
if (!status) continue;
|
|
515
|
+
if (status.stateModel === "marked") {
|
|
516
|
+
if (status.by !== null && isNewer(status, markedLatest[status.by])) markedLatest[status.by] = status;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (status.by !== null && isNewer(status, byReviewer[status.by])) byReviewer[status.by] = status;
|
|
520
|
+
if (isNewer(status, lastChange)) lastChange = status;
|
|
521
|
+
}
|
|
522
|
+
const markedBy = Object.keys(markedLatest).filter((key) => markedLatest[key].state === "marked").sort();
|
|
523
|
+
const review = {
|
|
524
|
+
byReviewer,
|
|
525
|
+
lastChange,
|
|
526
|
+
markedBy,
|
|
527
|
+
statusRefs: states.map((a) => a.ref)
|
|
528
|
+
};
|
|
529
|
+
if (opts.currentUserId !== void 0) review.mine = byReviewer[opts.currentUserId] ?? null;
|
|
530
|
+
return review;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Derive a `ReviewStatus` from one state annotation, or null when nothing
|
|
534
|
+
* classifiable can be derived. ISO defaulting happens HERE, not in the
|
|
535
|
+
* engine reader: a known model with an absent state means `none` /
|
|
536
|
+
* `unmarked`; an absent model infers from a known state; a custom model
|
|
537
|
+
* with no state has no derivable status.
|
|
538
|
+
*/
|
|
539
|
+
function toReviewStatus(a) {
|
|
540
|
+
if (a.subtype !== "text") return null;
|
|
541
|
+
const rawState = nonEmpty(a.state) ? a.state : null;
|
|
542
|
+
let stateModel = nonEmpty(a.stateModel) ? a.stateModel : null;
|
|
543
|
+
if (stateModel === null && rawState !== null) if (REVIEW_STATES.has(rawState)) stateModel = "review";
|
|
544
|
+
else if (MARKED_STATES.has(rawState)) stateModel = "marked";
|
|
545
|
+
else return null;
|
|
546
|
+
let state = rawState;
|
|
547
|
+
if (state === null) if (stateModel === "review") state = "none";
|
|
548
|
+
else if (stateModel === "marked") state = "unmarked";
|
|
549
|
+
else return null;
|
|
550
|
+
if (stateModel === null) return null;
|
|
551
|
+
return {
|
|
552
|
+
state,
|
|
553
|
+
stateModel,
|
|
554
|
+
by: a.userId ?? (nonEmpty(a.author) ? a.author : null),
|
|
555
|
+
at: a.modified ?? a.created ?? null,
|
|
556
|
+
ref: a.ref
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/** `a` newer than (or supersedes) `b`. `at` decides; dated beats undated.
|
|
560
|
+
* States arrive pre-sorted by `chronological` (date, then z-order), so on
|
|
561
|
+
* equal or missing dates the later entry wins via `>=` / `true`. */
|
|
562
|
+
function isNewer(a, b) {
|
|
563
|
+
if (!b) return true;
|
|
564
|
+
if (a.at !== null && b.at !== null) return a.at >= b.at;
|
|
565
|
+
if (a.at !== null) return true;
|
|
566
|
+
if (b.at !== null) return false;
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
527
570
|
//#region src/mutation/AnnotationMutationImpactPolicy.ts
|
|
528
571
|
function changesAnnotationList(_kind) {
|
|
529
572
|
return true;
|
|
@@ -547,14 +590,19 @@ function invalidatesWeakIndexRefs(kind, weakStateBefore) {
|
|
|
547
590
|
const EPSILON = .001;
|
|
548
591
|
/**
|
|
549
592
|
* Keys that never affect `/AP` on any kind: the discriminator, behavioral
|
|
550
|
-
* flags, reply relationships, and
|
|
593
|
+
* flags, reply relationships, grouping, and the conversation-plane
|
|
594
|
+
* entries (`/Subj` subject line, `/State` + `/StateModel` review status —
|
|
595
|
+
* dictionary-only per ISO 32000 §12.5.6.3, never painted).
|
|
551
596
|
*/
|
|
552
597
|
const INERT_KEYS = /* @__PURE__ */ new Set([
|
|
553
598
|
"subtype",
|
|
554
599
|
"flags",
|
|
555
600
|
"inReplyTo",
|
|
556
601
|
"replyType",
|
|
557
|
-
"groupId"
|
|
602
|
+
"groupId",
|
|
603
|
+
"subject",
|
|
604
|
+
"state",
|
|
605
|
+
"stateModel"
|
|
558
606
|
]);
|
|
559
607
|
/**
|
|
560
608
|
* Kinds whose `/AP` paints `/Contents`. Everywhere else `contents` is popup
|
|
@@ -1007,37 +1055,275 @@ function buildSnippet(text, range, context = 48) {
|
|
|
1007
1055
|
};
|
|
1008
1056
|
}
|
|
1009
1057
|
//#endregion
|
|
1010
|
-
//#region src/
|
|
1011
|
-
/**
|
|
1012
|
-
* Match-range → merged line rects, in PDF user space (y-up).
|
|
1013
|
-
*
|
|
1014
|
-
* This is the same line-merge text selection uses (adapted from Chromium's
|
|
1015
|
-
* pdf/pdfium/pdfium_range.cc `MergeAdjacentRects`, BSD-licensed, Copyright
|
|
1016
|
-
* 2010 The Chromium Authors), operating directly on the engine's
|
|
1017
|
-
* `PageGeometrySnapshot`: glyph loose boxes → sub-runs (split on big
|
|
1018
|
-
* intra-run gaps) → one rect per visual line. Search highlights therefore
|
|
1019
|
-
* look exactly like selections — never a rect per glyph (the v2 bug).
|
|
1020
|
-
*
|
|
1021
|
-
* It lives in engine-core because BOTH sides need it: the server computes
|
|
1022
|
-
* rects here for `'rects'`-mode responses (geometry never leaves the trust
|
|
1023
|
-
* boundary), and the local engine computes them in the worker.
|
|
1024
|
-
*/
|
|
1058
|
+
//#region src/text/layout.ts
|
|
1025
1059
|
const FLAG_EMPTY = 2;
|
|
1060
|
+
const isBoundary = (flags) => (flags & 3) !== 0;
|
|
1061
|
+
const isEmpty = (flags) => (flags & FLAG_EMPTY) !== 0;
|
|
1026
1062
|
const CHAR_DISTANCE_FACTOR = 2.5;
|
|
1027
1063
|
const FONT_SIZE_RATIO_THRESHOLD = 1.5;
|
|
1028
1064
|
const VERTICAL_OVERLAP_THRESHOLD = .8;
|
|
1065
|
+
const LINE_OVERLAP_THRESHOLD = .5;
|
|
1066
|
+
/** Baseline directions within ~0.5° share a frame. */
|
|
1067
|
+
const FRAME_DOT_TOLERANCE = Math.cos(.0087);
|
|
1068
|
+
const EDGE_EPSILON = 1e-6;
|
|
1069
|
+
const IDENTITY_FRAME = {
|
|
1070
|
+
baseline: {
|
|
1071
|
+
x: 1,
|
|
1072
|
+
y: 0
|
|
1073
|
+
},
|
|
1074
|
+
ascent: {
|
|
1075
|
+
x: 0,
|
|
1076
|
+
y: 1
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
const toFrame = (f, p) => ({
|
|
1080
|
+
x: p.x * f.baseline.x + p.y * f.baseline.y,
|
|
1081
|
+
y: p.x * f.ascent.x + p.y * f.ascent.y
|
|
1082
|
+
});
|
|
1083
|
+
const fromFrame = (f, p) => ({
|
|
1084
|
+
x: f.baseline.x * p.x + f.ascent.x * p.y,
|
|
1085
|
+
y: f.baseline.y * p.x + f.ascent.y * p.y
|
|
1086
|
+
});
|
|
1087
|
+
const ZERO_RECT = {
|
|
1088
|
+
left: 0,
|
|
1089
|
+
bottom: 0,
|
|
1090
|
+
right: 0,
|
|
1091
|
+
top: 0
|
|
1092
|
+
};
|
|
1093
|
+
/** Frame-local AABB of a page-space cell (exact under the frame's rotation;
|
|
1094
|
+
* shear residue is the documented in-frame envelope). */
|
|
1095
|
+
function frameBoxOfQuad(f, q) {
|
|
1096
|
+
const pts = [
|
|
1097
|
+
q.p1,
|
|
1098
|
+
q.p2,
|
|
1099
|
+
q.p3,
|
|
1100
|
+
q.p4
|
|
1101
|
+
].map((p) => toFrame(f, p));
|
|
1102
|
+
const xs = pts.map((p) => p.x);
|
|
1103
|
+
const ys = pts.map((p) => p.y);
|
|
1104
|
+
return {
|
|
1105
|
+
left: Math.min(...xs),
|
|
1106
|
+
bottom: Math.min(...ys),
|
|
1107
|
+
right: Math.max(...xs),
|
|
1108
|
+
top: Math.max(...ys)
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
/** Find-or-create the frame for a rotated run, keyed by the baseline
|
|
1112
|
+
* direction + ascent handedness of its first classifiable glyph's edges. */
|
|
1113
|
+
function frameForRun(frames, run) {
|
|
1114
|
+
const seed = run.glyphs.find((g) => !isEmpty(g.flags));
|
|
1115
|
+
if (!seed) return 0;
|
|
1116
|
+
const q = seed.looseQuad;
|
|
1117
|
+
const bx = q.p2.x - q.p1.x;
|
|
1118
|
+
const by = q.p2.y - q.p1.y;
|
|
1119
|
+
const len = Math.hypot(bx, by);
|
|
1120
|
+
if (len <= EDGE_EPSILON) return 0;
|
|
1121
|
+
const baseline = {
|
|
1122
|
+
x: bx / len,
|
|
1123
|
+
y: by / len
|
|
1124
|
+
};
|
|
1125
|
+
const ax = q.p1.x - q.p3.x;
|
|
1126
|
+
const ay = q.p1.y - q.p3.y;
|
|
1127
|
+
const sign = baseline.x * ay - baseline.y * ax >= 0 ? 1 : -1;
|
|
1128
|
+
const ascent = {
|
|
1129
|
+
x: -baseline.y * sign,
|
|
1130
|
+
y: baseline.x * sign
|
|
1131
|
+
};
|
|
1132
|
+
for (let i = 0; i < frames.length; i++) {
|
|
1133
|
+
const f = frames[i];
|
|
1134
|
+
const dot = f.baseline.x * baseline.x + f.baseline.y * baseline.y;
|
|
1135
|
+
const sameHandedness = f.ascent.x * ascent.x + f.ascent.y * ascent.y > 0;
|
|
1136
|
+
if (dot >= FRAME_DOT_TOLERANCE && sameHandedness) return i;
|
|
1137
|
+
}
|
|
1138
|
+
frames.push({
|
|
1139
|
+
baseline,
|
|
1140
|
+
ascent
|
|
1141
|
+
});
|
|
1142
|
+
return frames.length - 1;
|
|
1143
|
+
}
|
|
1029
1144
|
/**
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1145
|
+
* Flatten a page's geometry snapshot into the canonical layout. Upright runs
|
|
1146
|
+
* copy their wire boxes verbatim (frame 0 — byte-identical to the
|
|
1147
|
+
* pre-orientation behavior, wire run rects included); rotated runs project
|
|
1148
|
+
* their glyph cells into their cluster's frame.
|
|
1033
1149
|
*/
|
|
1034
|
-
function
|
|
1035
|
-
|
|
1036
|
-
const
|
|
1037
|
-
const
|
|
1038
|
-
const subRuns = [];
|
|
1150
|
+
function buildPageTextLayout(snapshot) {
|
|
1151
|
+
const glyphs = [];
|
|
1152
|
+
const runs = [];
|
|
1153
|
+
const frames = [IDENTITY_FRAME];
|
|
1039
1154
|
for (const run of snapshot.runs) {
|
|
1040
|
-
|
|
1155
|
+
if (!isRotatedGeometryRun(run)) {
|
|
1156
|
+
for (const g of run.glyphs) glyphs.push({
|
|
1157
|
+
loose: g.looseBox,
|
|
1158
|
+
tight: g.tightBox,
|
|
1159
|
+
flags: g.flags
|
|
1160
|
+
});
|
|
1161
|
+
runs.push({
|
|
1162
|
+
charStart: run.charStart,
|
|
1163
|
+
count: run.glyphs.length,
|
|
1164
|
+
rect: run.rect,
|
|
1165
|
+
fontSize: run.fontSize,
|
|
1166
|
+
frame: 0
|
|
1167
|
+
});
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
const frame = frameForRun(frames, run);
|
|
1171
|
+
const f = frames[frame];
|
|
1172
|
+
let runBox = null;
|
|
1173
|
+
for (const g of run.glyphs) {
|
|
1174
|
+
if (isEmpty(g.flags)) {
|
|
1175
|
+
glyphs.push({
|
|
1176
|
+
loose: ZERO_RECT,
|
|
1177
|
+
flags: g.flags
|
|
1178
|
+
});
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const loose = frameBoxOfQuad(f, g.looseQuad);
|
|
1182
|
+
const tight = g.tightQuad ? frameBoxOfQuad(f, g.tightQuad) : void 0;
|
|
1183
|
+
glyphs.push({
|
|
1184
|
+
loose,
|
|
1185
|
+
tight,
|
|
1186
|
+
flags: g.flags
|
|
1187
|
+
});
|
|
1188
|
+
runBox = runBox ? {
|
|
1189
|
+
left: Math.min(runBox.left, loose.left),
|
|
1190
|
+
bottom: Math.min(runBox.bottom, loose.bottom),
|
|
1191
|
+
right: Math.max(runBox.right, loose.right),
|
|
1192
|
+
top: Math.max(runBox.top, loose.top)
|
|
1193
|
+
} : loose;
|
|
1194
|
+
}
|
|
1195
|
+
runs.push({
|
|
1196
|
+
charStart: run.charStart,
|
|
1197
|
+
count: run.glyphs.length,
|
|
1198
|
+
rect: runBox ?? ZERO_RECT,
|
|
1199
|
+
fontSize: run.fontSize,
|
|
1200
|
+
frame
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
glyphs,
|
|
1205
|
+
runs,
|
|
1206
|
+
frames
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
const inRect = (r, p) => p.x >= r.left && p.x <= r.right && p.y >= r.bottom && p.y <= r.top;
|
|
1210
|
+
function avgGlyphHeight(layout) {
|
|
1211
|
+
let total = 0;
|
|
1212
|
+
let count = 0;
|
|
1213
|
+
for (const g of layout.glyphs) {
|
|
1214
|
+
if (isEmpty(g.flags)) continue;
|
|
1215
|
+
total += g.loose.top - g.loose.bottom;
|
|
1216
|
+
count++;
|
|
1217
|
+
}
|
|
1218
|
+
return count === 0 ? 0 : total / count;
|
|
1219
|
+
}
|
|
1220
|
+
/** The point expressed in every frame (index-aligned with `layout.frames`).
|
|
1221
|
+
* Frame 0 returns the point itself — no float round-trip on the fast path. */
|
|
1222
|
+
function pointPerFrame(layout, p) {
|
|
1223
|
+
return layout.frames.map((f, i) => i === 0 ? p : toFrame(f, p));
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The glyph (char) index at a page-space point, or null when nothing is near
|
|
1227
|
+
* (so callers can show the pointer cursor off-text). PDFium `GetIndexAtPos`:
|
|
1228
|
+
* exact tight-box containment first, then a tolerance pass (closest by
|
|
1229
|
+
* Manhattan distance within `toleranceFactor × average glyph height`). Each
|
|
1230
|
+
* run tests the point in ITS OWN frame, so rotated text hit-tests exactly.
|
|
1231
|
+
*/
|
|
1232
|
+
function textGlyphAt(layout, point, toleranceFactor = 1.5) {
|
|
1233
|
+
const pts = pointPerFrame(layout, point);
|
|
1234
|
+
for (const run of layout.runs) {
|
|
1235
|
+
const q = pts[run.frame];
|
|
1236
|
+
if (!inRect(run.rect, q)) continue;
|
|
1237
|
+
for (let i = 0; i < run.count; i++) {
|
|
1238
|
+
const g = layout.glyphs[run.charStart + i];
|
|
1239
|
+
if (inRect(g.tight ?? g.loose, q)) return run.charStart + i;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (toleranceFactor <= 0) return null;
|
|
1243
|
+
const half = avgGlyphHeight(layout) * toleranceFactor / 2;
|
|
1244
|
+
let best = -1;
|
|
1245
|
+
let bestDist = Infinity;
|
|
1246
|
+
for (const run of layout.runs) {
|
|
1247
|
+
const q = pts[run.frame];
|
|
1248
|
+
const r = run.rect;
|
|
1249
|
+
if (q.x < r.left - half || q.x > r.right + half || q.y < r.bottom - half || q.y > r.top + half) continue;
|
|
1250
|
+
for (let i = 0; i < run.count; i++) {
|
|
1251
|
+
const g = layout.glyphs[run.charStart + i];
|
|
1252
|
+
if (isEmpty(g.flags)) continue;
|
|
1253
|
+
const b = g.tight ?? g.loose;
|
|
1254
|
+
if (q.x < b.left - half || q.x > b.right + half || q.y < b.bottom - half || q.y > b.top + half) continue;
|
|
1255
|
+
const dx = Math.min(Math.abs(q.x - b.left), Math.abs(q.x - b.right));
|
|
1256
|
+
const dy = Math.min(Math.abs(q.y - b.bottom), Math.abs(q.y - b.top));
|
|
1257
|
+
if (dx + dy < bestDist) {
|
|
1258
|
+
bestDist = dx + dy;
|
|
1259
|
+
best = run.charStart + i;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return best >= 0 ? best : null;
|
|
1264
|
+
}
|
|
1265
|
+
/** Double-click: the word around `glyph` (walk to space/empty glyphs both ways). */
|
|
1266
|
+
function expandTextRangeToWord(layout, glyph) {
|
|
1267
|
+
const n = layout.glyphs.length;
|
|
1268
|
+
if (glyph < 0 || glyph >= n) return [glyph, glyph];
|
|
1269
|
+
let from = glyph;
|
|
1270
|
+
while (from > 0 && !isBoundary(layout.glyphs[from - 1].flags)) from--;
|
|
1271
|
+
let to = glyph;
|
|
1272
|
+
while (to < n - 1 && !isBoundary(layout.glyphs[to + 1].flags)) to++;
|
|
1273
|
+
return [from, to];
|
|
1274
|
+
}
|
|
1275
|
+
/** Triple-click: the full visual line — SAME-FRAME runs whose vertical extent
|
|
1276
|
+
* overlaps the anchor run's. A differently-oriented run is a line boundary. */
|
|
1277
|
+
function expandTextRangeToLine(layout, glyph) {
|
|
1278
|
+
const ri = layout.runs.findIndex((r) => glyph >= r.charStart && glyph < r.charStart + r.count);
|
|
1279
|
+
if (ri < 0) return [glyph, glyph];
|
|
1280
|
+
const anchor = layout.runs[ri];
|
|
1281
|
+
const bottom = anchor.rect.bottom;
|
|
1282
|
+
const top = anchor.rect.top;
|
|
1283
|
+
let from = anchor.charStart;
|
|
1284
|
+
let to = anchor.charStart + anchor.count - 1;
|
|
1285
|
+
for (let r = ri - 1; r >= 0; r--) {
|
|
1286
|
+
const run = layout.runs[r];
|
|
1287
|
+
if (isZero(run.rect)) continue;
|
|
1288
|
+
if (run.frame !== anchor.frame) break;
|
|
1289
|
+
if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
|
|
1290
|
+
from = run.charStart;
|
|
1291
|
+
}
|
|
1292
|
+
for (let r = ri + 1; r < layout.runs.length; r++) {
|
|
1293
|
+
const run = layout.runs[r];
|
|
1294
|
+
if (isZero(run.rect)) continue;
|
|
1295
|
+
if (run.frame !== anchor.frame) break;
|
|
1296
|
+
if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
|
|
1297
|
+
to = run.charStart + run.count - 1;
|
|
1298
|
+
}
|
|
1299
|
+
return [from, to];
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* A single glyph's oriented cell in page space, or null for degenerate
|
|
1303
|
+
* glyphs — the anchor for caret placement at a selection boundary.
|
|
1304
|
+
*/
|
|
1305
|
+
function textGlyphQuad(layout, glyph) {
|
|
1306
|
+
const g = layout.glyphs[glyph];
|
|
1307
|
+
if (!g || isEmpty(g.flags)) return null;
|
|
1308
|
+
const b = g.loose;
|
|
1309
|
+
if (b.right <= b.left || b.top <= b.bottom) return null;
|
|
1310
|
+
const run = layout.runs.find((r) => glyph >= r.charStart && glyph < r.charStart + r.count);
|
|
1311
|
+
const f = layout.frames[run?.frame ?? 0];
|
|
1312
|
+
return quadOfFrameRect(f, run?.frame ?? 0, b);
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Merged visual-line segments for the half-open char range
|
|
1316
|
+
* `[charStart, charStart + charCount)` — THE canonical segmentation. Chars
|
|
1317
|
+
* outside the layout are ignored.
|
|
1318
|
+
*/
|
|
1319
|
+
function textSegmentsForRange(layout, charStart, charCount) {
|
|
1320
|
+
if (charCount <= 0 || layout.glyphs.length === 0) return [];
|
|
1321
|
+
const lo = Math.max(0, charStart);
|
|
1322
|
+
const hi = Math.min(layout.glyphs.length - 1, charStart + charCount - 1);
|
|
1323
|
+
if (hi < lo) return [];
|
|
1324
|
+
const subRuns = [];
|
|
1325
|
+
for (const run of layout.runs) {
|
|
1326
|
+
const runEnd = run.charStart + run.count - 1;
|
|
1041
1327
|
if (runEnd < lo || run.charStart > hi) continue;
|
|
1042
1328
|
const s = Math.max(lo, run.charStart);
|
|
1043
1329
|
const e = Math.min(hi, runEnd);
|
|
@@ -1045,63 +1331,141 @@ function searchRectsForRange(snapshot, charStart, charCount) {
|
|
|
1045
1331
|
let right = -Infinity;
|
|
1046
1332
|
let bottom = Infinity;
|
|
1047
1333
|
let top = -Infinity;
|
|
1048
|
-
let
|
|
1334
|
+
let charCountAcc = 0;
|
|
1049
1335
|
let widthSum = 0;
|
|
1050
1336
|
let prevRight = -Infinity;
|
|
1337
|
+
let firstX = 0;
|
|
1338
|
+
let lastX = 0;
|
|
1051
1339
|
const flush = () => {
|
|
1052
|
-
if (
|
|
1340
|
+
if (charCountAcc > 0 && left !== Infinity) subRuns.push({
|
|
1053
1341
|
rect: {
|
|
1054
1342
|
left,
|
|
1055
1343
|
bottom,
|
|
1056
1344
|
right,
|
|
1057
1345
|
top
|
|
1058
1346
|
},
|
|
1059
|
-
charCount:
|
|
1060
|
-
fontSize: run.fontSize
|
|
1347
|
+
charCount: charCountAcc,
|
|
1348
|
+
fontSize: run.fontSize,
|
|
1349
|
+
frame: run.frame,
|
|
1350
|
+
firstX,
|
|
1351
|
+
lastX
|
|
1061
1352
|
});
|
|
1062
1353
|
left = Infinity;
|
|
1063
1354
|
right = -Infinity;
|
|
1064
1355
|
bottom = Infinity;
|
|
1065
1356
|
top = -Infinity;
|
|
1066
|
-
|
|
1357
|
+
charCountAcc = 0;
|
|
1067
1358
|
widthSum = 0;
|
|
1068
1359
|
prevRight = -Infinity;
|
|
1069
1360
|
};
|
|
1070
1361
|
for (let ci = s; ci <= e; ci++) {
|
|
1071
|
-
const g =
|
|
1072
|
-
if ((g.flags
|
|
1073
|
-
const b = g.
|
|
1074
|
-
if (
|
|
1075
|
-
const avg = widthSum /
|
|
1362
|
+
const g = layout.glyphs[ci];
|
|
1363
|
+
if (isEmpty(g.flags)) continue;
|
|
1364
|
+
const b = g.loose;
|
|
1365
|
+
if (charCountAcc > 0 && prevRight > -Infinity) {
|
|
1366
|
+
const avg = widthSum / charCountAcc;
|
|
1076
1367
|
if (avg > 0 && Math.abs(b.left - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
|
|
1077
1368
|
}
|
|
1369
|
+
const centerX = (b.left + b.right) / 2;
|
|
1370
|
+
if (charCountAcc === 0) firstX = centerX;
|
|
1371
|
+
lastX = centerX;
|
|
1078
1372
|
left = Math.min(left, b.left);
|
|
1079
1373
|
right = Math.max(right, b.right);
|
|
1080
1374
|
bottom = Math.min(bottom, b.bottom);
|
|
1081
1375
|
top = Math.max(top, b.top);
|
|
1082
|
-
|
|
1376
|
+
charCountAcc++;
|
|
1083
1377
|
widthSum += b.right - b.left;
|
|
1084
1378
|
prevRight = b.right;
|
|
1085
1379
|
}
|
|
1086
1380
|
flush();
|
|
1087
1381
|
}
|
|
1088
|
-
return
|
|
1382
|
+
return mergeAdjacentSubRuns(subRuns).map((m) => materializeSegment(layout, m));
|
|
1089
1383
|
}
|
|
1090
|
-
function
|
|
1384
|
+
function mergeAdjacentSubRuns(runs) {
|
|
1091
1385
|
const out = [];
|
|
1092
1386
|
let prev = null;
|
|
1093
1387
|
let cur = null;
|
|
1094
1388
|
for (const run of runs) {
|
|
1095
|
-
if (prev && cur && shouldMerge(prev, run)) cur =
|
|
1389
|
+
if (prev && cur && prev.frame === run.frame && shouldMerge(prev, run)) cur = {
|
|
1390
|
+
rect: {
|
|
1391
|
+
left: Math.min(cur.rect.left, run.rect.left),
|
|
1392
|
+
bottom: Math.min(cur.rect.bottom, run.rect.bottom),
|
|
1393
|
+
right: Math.max(cur.rect.right, run.rect.right),
|
|
1394
|
+
top: Math.max(cur.rect.top, run.rect.top)
|
|
1395
|
+
},
|
|
1396
|
+
frame: cur.frame,
|
|
1397
|
+
firstX: cur.firstX,
|
|
1398
|
+
lastX: run.lastX
|
|
1399
|
+
};
|
|
1096
1400
|
else {
|
|
1097
1401
|
if (cur) out.push(cur);
|
|
1098
|
-
cur =
|
|
1402
|
+
cur = {
|
|
1403
|
+
rect: run.rect,
|
|
1404
|
+
frame: run.frame,
|
|
1405
|
+
firstX: run.firstX,
|
|
1406
|
+
lastX: run.lastX
|
|
1407
|
+
};
|
|
1099
1408
|
}
|
|
1100
1409
|
prev = run;
|
|
1101
1410
|
}
|
|
1102
|
-
if (cur) out.push(cur);
|
|
1411
|
+
if (cur && cur.rect.right > cur.rect.left && cur.rect.top > cur.rect.bottom) out.push(cur);
|
|
1103
1412
|
return out;
|
|
1104
1413
|
}
|
|
1414
|
+
/** Segment quad for a frame-local rect (frame 0: the rect's own corners,
|
|
1415
|
+
* no float round-trip). Slot order US, UE, LS, LE; frame is y-up, so the
|
|
1416
|
+
* upper corners sit at the frame TOP. */
|
|
1417
|
+
function quadOfFrameRect(f, frameIndex, r) {
|
|
1418
|
+
if (frameIndex === 0) return {
|
|
1419
|
+
p1: {
|
|
1420
|
+
x: r.left,
|
|
1421
|
+
y: r.top
|
|
1422
|
+
},
|
|
1423
|
+
p2: {
|
|
1424
|
+
x: r.right,
|
|
1425
|
+
y: r.top
|
|
1426
|
+
},
|
|
1427
|
+
p3: {
|
|
1428
|
+
x: r.left,
|
|
1429
|
+
y: r.bottom
|
|
1430
|
+
},
|
|
1431
|
+
p4: {
|
|
1432
|
+
x: r.right,
|
|
1433
|
+
y: r.bottom
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
return {
|
|
1437
|
+
p1: fromFrame(f, {
|
|
1438
|
+
x: r.left,
|
|
1439
|
+
y: r.top
|
|
1440
|
+
}),
|
|
1441
|
+
p2: fromFrame(f, {
|
|
1442
|
+
x: r.right,
|
|
1443
|
+
y: r.top
|
|
1444
|
+
}),
|
|
1445
|
+
p3: fromFrame(f, {
|
|
1446
|
+
x: r.left,
|
|
1447
|
+
y: r.bottom
|
|
1448
|
+
}),
|
|
1449
|
+
p4: fromFrame(f, {
|
|
1450
|
+
x: r.right,
|
|
1451
|
+
y: r.bottom
|
|
1452
|
+
})
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
function materializeSegment(layout, m) {
|
|
1456
|
+
const frame = layout.frames[m.frame];
|
|
1457
|
+
const quad = quadOfFrameRect(frame, m.frame, m.rect);
|
|
1458
|
+
return {
|
|
1459
|
+
quad,
|
|
1460
|
+
rect: m.frame === 0 ? m.rect : {
|
|
1461
|
+
left: Math.min(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x),
|
|
1462
|
+
bottom: Math.min(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y),
|
|
1463
|
+
right: Math.max(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x),
|
|
1464
|
+
top: Math.max(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y)
|
|
1465
|
+
},
|
|
1466
|
+
advance: m.lastX >= m.firstX ? 1 : -1
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1105
1469
|
function shouldMerge(a, b) {
|
|
1106
1470
|
if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
|
|
1107
1471
|
if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
|
|
@@ -1114,14 +1478,6 @@ function shouldMerge(a, b) {
|
|
|
1114
1478
|
const bL = b.rect.left - bw;
|
|
1115
1479
|
return aL < b.rect.right + bw && aR > bL;
|
|
1116
1480
|
}
|
|
1117
|
-
function union(a, b) {
|
|
1118
|
-
return {
|
|
1119
|
-
left: Math.min(a.left, b.left),
|
|
1120
|
-
bottom: Math.min(a.bottom, b.bottom),
|
|
1121
|
-
right: Math.max(a.right, b.right),
|
|
1122
|
-
top: Math.max(a.top, b.top)
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
1481
|
function verticalOverlap(a, b) {
|
|
1126
1482
|
const ah = a.top - a.bottom;
|
|
1127
1483
|
const bh = b.top - b.bottom;
|
|
@@ -1130,6 +1486,28 @@ function verticalOverlap(a, b) {
|
|
|
1130
1486
|
if (u === ah || u === bh) return 1;
|
|
1131
1487
|
return Math.max(0, Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom)) / u;
|
|
1132
1488
|
}
|
|
1489
|
+
const isZero = (r) => r.right === r.left && r.top === r.bottom;
|
|
1490
|
+
function overlapV(bottom1, top1, bottom2, top2) {
|
|
1491
|
+
const u = Math.max(top1, top2) - Math.min(bottom1, bottom2);
|
|
1492
|
+
if (u === 0) return false;
|
|
1493
|
+
return Math.max(0, Math.min(top1, top2) - Math.max(bottom1, bottom2)) / u >= LINE_OVERLAP_THRESHOLD;
|
|
1494
|
+
}
|
|
1495
|
+
//#endregion
|
|
1496
|
+
//#region src/search/rects.ts
|
|
1497
|
+
/**
|
|
1498
|
+
* Match-range → canonical visual-line segments. Thin wrappers over the ONE
|
|
1499
|
+
* text layout (`src/text/layout.ts`) — search owns matching and cursors,
|
|
1500
|
+
* never segmentation, so a match highlights exactly like a selection of the
|
|
1501
|
+
* same characters. No frame or merge logic lives here.
|
|
1502
|
+
*/
|
|
1503
|
+
/** Canonical segments for `[charStart, charStart + charCount)`. */
|
|
1504
|
+
function searchSegmentsForRange(snapshot, charStart, charCount) {
|
|
1505
|
+
return textSegmentsForRange(buildPageTextLayout(snapshot), charStart, charCount);
|
|
1506
|
+
}
|
|
1507
|
+
/** The segments' AABBs — for consumers that genuinely want boxes. */
|
|
1508
|
+
function searchRectsForRange(snapshot, charStart, charCount) {
|
|
1509
|
+
return searchSegmentsForRange(snapshot, charStart, charCount).map((s) => s.rect);
|
|
1510
|
+
}
|
|
1133
1511
|
//#endregion
|
|
1134
1512
|
//#region src/search/epoch.ts
|
|
1135
1513
|
/**
|
|
@@ -1362,6 +1740,6 @@ function materializePdfPermissions(b) {
|
|
|
1362
1740
|
return [...out];
|
|
1363
1741
|
}
|
|
1364
1742
|
//#endregion
|
|
1365
|
-
export {
|
|
1743
|
+
export { createPageImageHandle as $, appearanceImpactOf as A, normalizeAnnotationDraft as B, validateSearchRegex as C, wordBefore as D, wordAt as E, buildCommentThreads as F, NO_ANNOTATION_FLAGS as G, normalizeAttachmentFileSource as H, isStateAnnotation as I, revisionTokensEqual as J, UNKNOWN_WEAK_ANNOTATION_STATE as K, buildThreads as L, changesAnnotationList as M, invalidatesWeakIndexRefs as N, decodeFieldRefKey as O, shiftsExistingAnnotationIndices as P, DEFAULT_PDF_SAVE_MODE as Q, classifyRelation as R, validateSearchQuery as S, matchLiteral as T, sniffBinaryMetadata as U, normalizeAnnotationPatch as V, resolveBinarySource as W, encodeStableIdKey as X, decodeStableIdKey as Y, isValidPageObjectNumber as Z, SEARCH_SNIPPET_CONTEXT as _, PDF_BITS as a, SEARCH_REGEX_MAX_LENGTH as b, searchContentEpoch as c, buildPageTextLayout as d, expandTextRangeToLine as f, textSegmentsForRange as g, textGlyphQuad as h, pdfPermissions as i, semanticEqual as j, encodeFieldRefKey as k, searchRectsForRange as l, textGlyphAt as m, collab as n, decodePdfBits as o, expandTextRangeToWord as p, knownWeakAnnotationState as q, materializePdfPermissions as r, canonicalSearchQuery as s, caps as t, searchSegmentsForRange as u, buildSnippet as v, foldOptionsFor as w, matchRegex as x, matchPageText as y, refKey as z };
|
|
1366
1744
|
|
|
1367
|
-
//# sourceMappingURL=builders-
|
|
1745
|
+
//# sourceMappingURL=builders-B_KufGJ9.js.map
|