@embedpdf/engine-core 3.0.0-next.1 → 3.0.0-next.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/{AbortError-CjSasIlL.js → AbortError-8-9JAX1Y.js} +2 -2
  2. package/dist/{AbortError-CjSasIlL.js.map → AbortError-8-9JAX1Y.js.map} +1 -1
  3. package/dist/{AbortError-ByBflvEl.cjs → AbortError-CRSooKsk.cjs} +2 -2
  4. package/dist/{AbortError-ByBflvEl.cjs.map → AbortError-CRSooKsk.cjs.map} +1 -1
  5. package/dist/{AnnotationListSnapshot-W0RCsyDE.d.cts → AnnotationListSnapshot-C1U3XUxP.d.cts} +846 -127
  6. package/dist/{AnnotationListSnapshot-W0RCsyDE.d.ts → AnnotationListSnapshot-C1U3XUxP.d.ts} +846 -127
  7. package/dist/{DocumentSecurityService-Df79fgpe.d.cts → DocumentSecurityService-6rhztZlz.d.cts} +33 -2
  8. package/dist/{DocumentSecurityService-B-tULKNY.d.ts → DocumentSecurityService-7nL9aY75.d.ts} +33 -2
  9. package/dist/{Engine-B7Oe7f5i.d.ts → Engine-CWnAKccD.d.ts} +62 -19
  10. package/dist/{Engine-D9loFZc1.d.cts → Engine-D3868xIU.d.cts} +62 -19
  11. package/dist/{subtype-qejjVr6h.cjs → PageInsertBlankInput-CzShuSJI.cjs} +184 -1
  12. package/dist/PageInsertBlankInput-CzShuSJI.cjs.map +1 -0
  13. package/dist/{subtype-BvAzLCGC.js → PageInsertBlankInput-WKY0tr6S.js} +149 -2
  14. package/dist/PageInsertBlankInput-WKY0tr6S.js.map +1 -0
  15. package/dist/{builders-C548U47M.js → builders-Cdz34nbF.js} +566 -164
  16. package/dist/builders-Cdz34nbF.js.map +1 -0
  17. package/dist/{builders-BvikWRuO.cjs → builders-Do6_4KnS.cjs} +628 -220
  18. package/dist/builders-Do6_4KnS.cjs.map +1 -0
  19. package/dist/conformance.cjs +1039 -204
  20. package/dist/conformance.cjs.map +1 -1
  21. package/dist/conformance.d.cts +130 -6
  22. package/dist/conformance.d.ts +130 -6
  23. package/dist/conformance.js +994 -163
  24. package/dist/conformance.js.map +1 -1
  25. package/dist/{fold-CB4fAOOy.js → fold-BfyxxwS6.js} +162 -3
  26. package/dist/fold-BfyxxwS6.js.map +1 -0
  27. package/dist/{fold-IaMph3CO.cjs → fold-C7I5GpBz.cjs} +235 -4
  28. package/dist/fold-C7I5GpBz.cjs.map +1 -0
  29. package/dist/index.cjs +38 -19
  30. package/dist/index.d.cts +2 -2
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +5 -5
  33. package/dist/{resolver-DiPMTfcy.js → resolver-CHG4waXL.js} +2 -1
  34. package/dist/resolver-CHG4waXL.js.map +1 -0
  35. package/dist/{resolver-DAZWVoJL.cjs → resolver-D-jumvgM.cjs} +2 -1
  36. package/dist/resolver-D-jumvgM.cjs.map +1 -0
  37. package/dist/runtime.cjs +43 -24
  38. package/dist/runtime.cjs.map +1 -1
  39. package/dist/runtime.d.cts +103 -5
  40. package/dist/runtime.d.ts +103 -5
  41. package/dist/runtime.js +6 -6
  42. package/dist/{schemas-B_1VyFsm.cjs → schemas-CS5iPpX5.cjs} +305 -111
  43. package/dist/schemas-CS5iPpX5.cjs.map +1 -0
  44. package/dist/{schemas-9OpSSeM6.js → schemas-XGrNVMDd.js} +250 -92
  45. package/dist/schemas-XGrNVMDd.js.map +1 -0
  46. package/dist/wire.cjs +131 -4
  47. package/dist/wire.cjs.map +1 -1
  48. package/dist/wire.d.cts +277 -20
  49. package/dist/wire.d.ts +277 -20
  50. package/dist/wire.js +120 -5
  51. package/dist/wire.js.map +1 -1
  52. package/package.json +1 -1
  53. package/dist/builders-BvikWRuO.cjs.map +0 -1
  54. package/dist/builders-C548U47M.js.map +0 -1
  55. package/dist/fold-CB4fAOOy.js.map +0 -1
  56. package/dist/fold-IaMph3CO.cjs.map +0 -1
  57. package/dist/resolver-DAZWVoJL.cjs.map +0 -1
  58. package/dist/resolver-DiPMTfcy.js.map +0 -1
  59. package/dist/schemas-9OpSSeM6.js.map +0 -1
  60. package/dist/schemas-B_1VyFsm.cjs.map +0 -1
  61. package/dist/subtype-BvAzLCGC.js.map +0 -1
  62. package/dist/subtype-qejjVr6h.cjs.map +0 -1
@@ -1,119 +1,28 @@
1
- import { o as EngineErrorCode } from "./subtype-BvAzLCGC.js";
2
- import { i as EngineError, n as foldText, r as toOriginalRange } from "./fold-CB4fAOOy.js";
3
- //#region src/geometry/convert.ts
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";
3
+ //#region src/dto/PdfAction.ts
4
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.
5
+ * The one Table-240 decode, shared by the reader, the schema's producers,
6
+ * and tests — so bit semantics cannot fork.
11
7
  */
12
- function normalizePdfRect(r) {
8
+ const decodeSubmitFormFlags = (raw) => {
9
+ const bit = (n) => (raw & 1 << n - 1) !== 0;
10
+ const format = bit(9) ? "pdf" : bit(6) ? "xfdf" : bit(3) ? "html" : "fdf";
13
11
  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)
12
+ raw,
13
+ exclude: bit(1),
14
+ includeNoValueFields: bit(2),
15
+ format,
16
+ method: bit(4) && (format === "html" || format === "pdf") ? "get" : "post",
17
+ submitCoordinates: bit(5),
18
+ includeAppendSaves: bit(7),
19
+ includeAnnotations: bit(8),
20
+ canonicalFormat: bit(10),
21
+ exclNonUserAnnots: bit(11),
22
+ exclFKey: bit(12),
23
+ embedForm: bit(14)
18
24
  };
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
- }
25
+ };
117
26
  //#endregion
118
27
  //#region src/dto/PageRender.ts
119
28
  function createPageImageHandle(result, blobSource) {
@@ -524,6 +433,164 @@ function buildThreads(annotations) {
524
433
  return threads;
525
434
  }
526
435
  //#endregion
436
+ //#region src/annotation/comments.ts
437
+ /** Subtypes that never participate in comment threads. `unsupported`
438
+ * covers `/Popup` dictionaries (raw code 16) and unreadable foreign
439
+ * blobs — neither makes a meaningful comment card. */
440
+ const EXCLUDED_SUBTYPES = /* @__PURE__ */ new Set([
441
+ "widget",
442
+ "link",
443
+ "unsupported"
444
+ ]);
445
+ const REVIEW_STATES = /* @__PURE__ */ new Set([
446
+ "accepted",
447
+ "rejected",
448
+ "cancelled",
449
+ "completed",
450
+ "none"
451
+ ]);
452
+ const MARKED_STATES = /* @__PURE__ */ new Set(["marked", "unmarked"]);
453
+ const nonEmpty = (v) => typeof v === "string" && v !== "";
454
+ /**
455
+ * An ISO 32000 §12.5.6.3 state annotation: a text annotation carrying a
456
+ * non-empty `/State` or `/StateModel`. Classification never relies on
457
+ * flags — producers disagree on how state annotations are flagged.
458
+ */
459
+ function isStateAnnotation(a) {
460
+ return a.subtype === "text" && (nonEmpty(a.state) || nonEmpty(a.stateModel));
461
+ }
462
+ function buildCommentThreads(annotations, opts = {}) {
463
+ const eligible = annotations.filter((a) => !EXCLUDED_SUBTYPES.has(a.subtype));
464
+ const byKey = /* @__PURE__ */ new Map();
465
+ for (const a of eligible) {
466
+ const key = refKey(a.ref);
467
+ if (!byKey.has(key)) byKey.set(key, a);
468
+ if (a.nm) {
469
+ const aliasKey = refKey({
470
+ kind: "nm",
471
+ pageObjectNumber: a.pageObjectNumber,
472
+ nm: a.nm
473
+ });
474
+ if (!byKey.has(aliasKey)) byKey.set(aliasKey, a);
475
+ }
476
+ }
477
+ const children = /* @__PURE__ */ new Map();
478
+ for (const a of eligible) {
479
+ if (!a.inReplyTo) continue;
480
+ const parent = byKey.get(refKey(a.inReplyTo));
481
+ if (!parent) continue;
482
+ const parentKey = refKey(parent.ref);
483
+ const list = children.get(parentKey);
484
+ if (list) list.push(a);
485
+ else children.set(parentKey, [a]);
486
+ }
487
+ const visited = /* @__PURE__ */ new Set();
488
+ const threads = [];
489
+ const walk = (root) => {
490
+ const replies = [];
491
+ const groupedParts = [];
492
+ const states = [];
493
+ visited.add(refKey(root.ref));
494
+ const stack = [...children.get(refKey(root.ref)) ?? []];
495
+ while (stack.length > 0) {
496
+ const a = stack.pop();
497
+ const key = refKey(a.ref);
498
+ if (visited.has(key)) continue;
499
+ visited.add(key);
500
+ if (isStateAnnotation(a)) states.push(a);
501
+ else if (classifyRelation(a) === "grouped-subordinate") groupedParts.push(a);
502
+ else replies.push(a);
503
+ stack.push(...children.get(key) ?? []);
504
+ }
505
+ replies.sort(chronological);
506
+ states.sort(chronological);
507
+ threads.push({
508
+ root,
509
+ pageObjectNumber: root.pageObjectNumber,
510
+ replies,
511
+ groupedParts,
512
+ review: computeReview(states, opts)
513
+ });
514
+ };
515
+ for (const a of eligible) if (!a.inReplyTo && !isStateAnnotation(a)) walk(a);
516
+ for (const a of eligible) {
517
+ if (visited.has(refKey(a.ref))) continue;
518
+ if (isStateAnnotation(a)) continue;
519
+ walk(a);
520
+ }
521
+ return threads;
522
+ }
523
+ /** `created ?? modified` ascending; undated last; z-order tiebreak. */
524
+ function chronological(a, b) {
525
+ const at = a.created ?? a.modified;
526
+ const bt = b.created ?? b.modified;
527
+ if (at !== null && bt !== null && at !== bt) return at < bt ? -1 : 1;
528
+ if (at !== null && bt === null) return -1;
529
+ if (at === null && bt !== null) return 1;
530
+ return a.index - b.index;
531
+ }
532
+ function computeReview(states, opts) {
533
+ const byReviewer = {};
534
+ const markedLatest = {};
535
+ let lastChange = null;
536
+ for (const a of states) {
537
+ const status = toReviewStatus(a);
538
+ if (!status) continue;
539
+ if (status.stateModel === "marked") {
540
+ if (status.by !== null && isNewer(status, markedLatest[status.by])) markedLatest[status.by] = status;
541
+ continue;
542
+ }
543
+ if (status.by !== null && isNewer(status, byReviewer[status.by])) byReviewer[status.by] = status;
544
+ if (isNewer(status, lastChange)) lastChange = status;
545
+ }
546
+ const markedBy = Object.keys(markedLatest).filter((key) => markedLatest[key].state === "marked").sort();
547
+ const review = {
548
+ byReviewer,
549
+ lastChange,
550
+ markedBy,
551
+ statusRefs: states.map((a) => a.ref)
552
+ };
553
+ if (opts.currentUserId !== void 0) review.mine = byReviewer[opts.currentUserId] ?? null;
554
+ return review;
555
+ }
556
+ /**
557
+ * Derive a `ReviewStatus` from one state annotation, or null when nothing
558
+ * classifiable can be derived. ISO defaulting happens HERE, not in the
559
+ * engine reader: a known model with an absent state means `none` /
560
+ * `unmarked`; an absent model infers from a known state; a custom model
561
+ * with no state has no derivable status.
562
+ */
563
+ function toReviewStatus(a) {
564
+ if (a.subtype !== "text") return null;
565
+ const rawState = nonEmpty(a.state) ? a.state : null;
566
+ let stateModel = nonEmpty(a.stateModel) ? a.stateModel : null;
567
+ if (stateModel === null && rawState !== null) if (REVIEW_STATES.has(rawState)) stateModel = "review";
568
+ else if (MARKED_STATES.has(rawState)) stateModel = "marked";
569
+ else return null;
570
+ let state = rawState;
571
+ if (state === null) if (stateModel === "review") state = "none";
572
+ else if (stateModel === "marked") state = "unmarked";
573
+ else return null;
574
+ if (stateModel === null) return null;
575
+ return {
576
+ state,
577
+ stateModel,
578
+ by: a.userId ?? (nonEmpty(a.author) ? a.author : null),
579
+ at: a.modified ?? a.created ?? null,
580
+ ref: a.ref
581
+ };
582
+ }
583
+ /** `a` newer than (or supersedes) `b`. `at` decides; dated beats undated.
584
+ * States arrive pre-sorted by `chronological` (date, then z-order), so on
585
+ * equal or missing dates the later entry wins via `>=` / `true`. */
586
+ function isNewer(a, b) {
587
+ if (!b) return true;
588
+ if (a.at !== null && b.at !== null) return a.at >= b.at;
589
+ if (a.at !== null) return true;
590
+ if (b.at !== null) return false;
591
+ return true;
592
+ }
593
+ //#endregion
527
594
  //#region src/mutation/AnnotationMutationImpactPolicy.ts
528
595
  function changesAnnotationList(_kind) {
529
596
  return true;
@@ -547,14 +614,19 @@ function invalidatesWeakIndexRefs(kind, weakStateBefore) {
547
614
  const EPSILON = .001;
548
615
  /**
549
616
  * Keys that never affect `/AP` on any kind: the discriminator, behavioral
550
- * flags, reply relationships, and grouping.
617
+ * flags, reply relationships, grouping, and the conversation-plane
618
+ * entries (`/Subj` subject line, `/State` + `/StateModel` review status —
619
+ * dictionary-only per ISO 32000 §12.5.6.3, never painted).
551
620
  */
552
621
  const INERT_KEYS = /* @__PURE__ */ new Set([
553
622
  "subtype",
554
623
  "flags",
555
624
  "inReplyTo",
556
625
  "replyType",
557
- "groupId"
626
+ "groupId",
627
+ "subject",
628
+ "state",
629
+ "stateModel"
558
630
  ]);
559
631
  /**
560
632
  * Kinds whose `/AP` paints `/Contents`. Everywhere else `contents` is popup
@@ -1007,37 +1079,275 @@ function buildSnippet(text, range, context = 48) {
1007
1079
  };
1008
1080
  }
1009
1081
  //#endregion
1010
- //#region src/search/rects.ts
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
- */
1082
+ //#region src/text/layout.ts
1025
1083
  const FLAG_EMPTY = 2;
1084
+ const isBoundary = (flags) => (flags & 3) !== 0;
1085
+ const isEmpty = (flags) => (flags & FLAG_EMPTY) !== 0;
1026
1086
  const CHAR_DISTANCE_FACTOR = 2.5;
1027
1087
  const FONT_SIZE_RATIO_THRESHOLD = 1.5;
1028
1088
  const VERTICAL_OVERLAP_THRESHOLD = .8;
1089
+ const LINE_OVERLAP_THRESHOLD = .5;
1090
+ /** Baseline directions within ~0.5° share a frame. */
1091
+ const FRAME_DOT_TOLERANCE = Math.cos(.0087);
1092
+ const EDGE_EPSILON = 1e-6;
1093
+ const IDENTITY_FRAME = {
1094
+ baseline: {
1095
+ x: 1,
1096
+ y: 0
1097
+ },
1098
+ ascent: {
1099
+ x: 0,
1100
+ y: 1
1101
+ }
1102
+ };
1103
+ const toFrame = (f, p) => ({
1104
+ x: p.x * f.baseline.x + p.y * f.baseline.y,
1105
+ y: p.x * f.ascent.x + p.y * f.ascent.y
1106
+ });
1107
+ const fromFrame = (f, p) => ({
1108
+ x: f.baseline.x * p.x + f.ascent.x * p.y,
1109
+ y: f.baseline.y * p.x + f.ascent.y * p.y
1110
+ });
1111
+ const ZERO_RECT = {
1112
+ left: 0,
1113
+ bottom: 0,
1114
+ right: 0,
1115
+ top: 0
1116
+ };
1117
+ /** Frame-local AABB of a page-space cell (exact under the frame's rotation;
1118
+ * shear residue is the documented in-frame envelope). */
1119
+ function frameBoxOfQuad(f, q) {
1120
+ const pts = [
1121
+ q.p1,
1122
+ q.p2,
1123
+ q.p3,
1124
+ q.p4
1125
+ ].map((p) => toFrame(f, p));
1126
+ const xs = pts.map((p) => p.x);
1127
+ const ys = pts.map((p) => p.y);
1128
+ return {
1129
+ left: Math.min(...xs),
1130
+ bottom: Math.min(...ys),
1131
+ right: Math.max(...xs),
1132
+ top: Math.max(...ys)
1133
+ };
1134
+ }
1135
+ /** Find-or-create the frame for a rotated run, keyed by the baseline
1136
+ * direction + ascent handedness of its first classifiable glyph's edges. */
1137
+ function frameForRun(frames, run) {
1138
+ const seed = run.glyphs.find((g) => !isEmpty(g.flags));
1139
+ if (!seed) return 0;
1140
+ const q = seed.looseQuad;
1141
+ const bx = q.p2.x - q.p1.x;
1142
+ const by = q.p2.y - q.p1.y;
1143
+ const len = Math.hypot(bx, by);
1144
+ if (len <= EDGE_EPSILON) return 0;
1145
+ const baseline = {
1146
+ x: bx / len,
1147
+ y: by / len
1148
+ };
1149
+ const ax = q.p1.x - q.p3.x;
1150
+ const ay = q.p1.y - q.p3.y;
1151
+ const sign = baseline.x * ay - baseline.y * ax >= 0 ? 1 : -1;
1152
+ const ascent = {
1153
+ x: -baseline.y * sign,
1154
+ y: baseline.x * sign
1155
+ };
1156
+ for (let i = 0; i < frames.length; i++) {
1157
+ const f = frames[i];
1158
+ const dot = f.baseline.x * baseline.x + f.baseline.y * baseline.y;
1159
+ const sameHandedness = f.ascent.x * ascent.x + f.ascent.y * ascent.y > 0;
1160
+ if (dot >= FRAME_DOT_TOLERANCE && sameHandedness) return i;
1161
+ }
1162
+ frames.push({
1163
+ baseline,
1164
+ ascent
1165
+ });
1166
+ return frames.length - 1;
1167
+ }
1029
1168
  /**
1030
- * Merged line rects for the text-page char range
1031
- * `[charStart, charStart + charCount)`. Ranges index the same space as
1032
- * `PageGeometryRun.charStart`; chars outside the snapshot are ignored.
1169
+ * Flatten a page's geometry snapshot into the canonical layout. Upright runs
1170
+ * copy their wire boxes verbatim (frame 0 — byte-identical to the
1171
+ * pre-orientation behavior, wire run rects included); rotated runs project
1172
+ * their glyph cells into their cluster's frame.
1033
1173
  */
1034
- function searchRectsForRange(snapshot, charStart, charCount) {
1035
- if (charCount <= 0) return [];
1036
- const lo = charStart;
1037
- const hi = charStart + charCount - 1;
1038
- const subRuns = [];
1174
+ function buildPageTextLayout(snapshot) {
1175
+ const glyphs = [];
1176
+ const runs = [];
1177
+ const frames = [IDENTITY_FRAME];
1039
1178
  for (const run of snapshot.runs) {
1040
- const runEnd = run.charStart + run.glyphs.length - 1;
1179
+ if (!isRotatedGeometryRun(run)) {
1180
+ for (const g of run.glyphs) glyphs.push({
1181
+ loose: g.looseBox,
1182
+ tight: g.tightBox,
1183
+ flags: g.flags
1184
+ });
1185
+ runs.push({
1186
+ charStart: run.charStart,
1187
+ count: run.glyphs.length,
1188
+ rect: run.rect,
1189
+ fontSize: run.fontSize,
1190
+ frame: 0
1191
+ });
1192
+ continue;
1193
+ }
1194
+ const frame = frameForRun(frames, run);
1195
+ const f = frames[frame];
1196
+ let runBox = null;
1197
+ for (const g of run.glyphs) {
1198
+ if (isEmpty(g.flags)) {
1199
+ glyphs.push({
1200
+ loose: ZERO_RECT,
1201
+ flags: g.flags
1202
+ });
1203
+ continue;
1204
+ }
1205
+ const loose = frameBoxOfQuad(f, g.looseQuad);
1206
+ const tight = g.tightQuad ? frameBoxOfQuad(f, g.tightQuad) : void 0;
1207
+ glyphs.push({
1208
+ loose,
1209
+ tight,
1210
+ flags: g.flags
1211
+ });
1212
+ runBox = runBox ? {
1213
+ left: Math.min(runBox.left, loose.left),
1214
+ bottom: Math.min(runBox.bottom, loose.bottom),
1215
+ right: Math.max(runBox.right, loose.right),
1216
+ top: Math.max(runBox.top, loose.top)
1217
+ } : loose;
1218
+ }
1219
+ runs.push({
1220
+ charStart: run.charStart,
1221
+ count: run.glyphs.length,
1222
+ rect: runBox ?? ZERO_RECT,
1223
+ fontSize: run.fontSize,
1224
+ frame
1225
+ });
1226
+ }
1227
+ return {
1228
+ glyphs,
1229
+ runs,
1230
+ frames
1231
+ };
1232
+ }
1233
+ const inRect = (r, p) => p.x >= r.left && p.x <= r.right && p.y >= r.bottom && p.y <= r.top;
1234
+ function avgGlyphHeight(layout) {
1235
+ let total = 0;
1236
+ let count = 0;
1237
+ for (const g of layout.glyphs) {
1238
+ if (isEmpty(g.flags)) continue;
1239
+ total += g.loose.top - g.loose.bottom;
1240
+ count++;
1241
+ }
1242
+ return count === 0 ? 0 : total / count;
1243
+ }
1244
+ /** The point expressed in every frame (index-aligned with `layout.frames`).
1245
+ * Frame 0 returns the point itself — no float round-trip on the fast path. */
1246
+ function pointPerFrame(layout, p) {
1247
+ return layout.frames.map((f, i) => i === 0 ? p : toFrame(f, p));
1248
+ }
1249
+ /**
1250
+ * The glyph (char) index at a page-space point, or null when nothing is near
1251
+ * (so callers can show the pointer cursor off-text). PDFium `GetIndexAtPos`:
1252
+ * exact tight-box containment first, then a tolerance pass (closest by
1253
+ * Manhattan distance within `toleranceFactor × average glyph height`). Each
1254
+ * run tests the point in ITS OWN frame, so rotated text hit-tests exactly.
1255
+ */
1256
+ function textGlyphAt(layout, point, toleranceFactor = 1.5) {
1257
+ const pts = pointPerFrame(layout, point);
1258
+ for (const run of layout.runs) {
1259
+ const q = pts[run.frame];
1260
+ if (!inRect(run.rect, q)) continue;
1261
+ for (let i = 0; i < run.count; i++) {
1262
+ const g = layout.glyphs[run.charStart + i];
1263
+ if (inRect(g.tight ?? g.loose, q)) return run.charStart + i;
1264
+ }
1265
+ }
1266
+ if (toleranceFactor <= 0) return null;
1267
+ const half = avgGlyphHeight(layout) * toleranceFactor / 2;
1268
+ let best = -1;
1269
+ let bestDist = Infinity;
1270
+ for (const run of layout.runs) {
1271
+ const q = pts[run.frame];
1272
+ const r = run.rect;
1273
+ if (q.x < r.left - half || q.x > r.right + half || q.y < r.bottom - half || q.y > r.top + half) continue;
1274
+ for (let i = 0; i < run.count; i++) {
1275
+ const g = layout.glyphs[run.charStart + i];
1276
+ if (isEmpty(g.flags)) continue;
1277
+ const b = g.tight ?? g.loose;
1278
+ if (q.x < b.left - half || q.x > b.right + half || q.y < b.bottom - half || q.y > b.top + half) continue;
1279
+ const dx = Math.min(Math.abs(q.x - b.left), Math.abs(q.x - b.right));
1280
+ const dy = Math.min(Math.abs(q.y - b.bottom), Math.abs(q.y - b.top));
1281
+ if (dx + dy < bestDist) {
1282
+ bestDist = dx + dy;
1283
+ best = run.charStart + i;
1284
+ }
1285
+ }
1286
+ }
1287
+ return best >= 0 ? best : null;
1288
+ }
1289
+ /** Double-click: the word around `glyph` (walk to space/empty glyphs both ways). */
1290
+ function expandTextRangeToWord(layout, glyph) {
1291
+ const n = layout.glyphs.length;
1292
+ if (glyph < 0 || glyph >= n) return [glyph, glyph];
1293
+ let from = glyph;
1294
+ while (from > 0 && !isBoundary(layout.glyphs[from - 1].flags)) from--;
1295
+ let to = glyph;
1296
+ while (to < n - 1 && !isBoundary(layout.glyphs[to + 1].flags)) to++;
1297
+ return [from, to];
1298
+ }
1299
+ /** Triple-click: the full visual line — SAME-FRAME runs whose vertical extent
1300
+ * overlaps the anchor run's. A differently-oriented run is a line boundary. */
1301
+ function expandTextRangeToLine(layout, glyph) {
1302
+ const ri = layout.runs.findIndex((r) => glyph >= r.charStart && glyph < r.charStart + r.count);
1303
+ if (ri < 0) return [glyph, glyph];
1304
+ const anchor = layout.runs[ri];
1305
+ const bottom = anchor.rect.bottom;
1306
+ const top = anchor.rect.top;
1307
+ let from = anchor.charStart;
1308
+ let to = anchor.charStart + anchor.count - 1;
1309
+ for (let r = ri - 1; r >= 0; r--) {
1310
+ const run = layout.runs[r];
1311
+ if (isZero(run.rect)) continue;
1312
+ if (run.frame !== anchor.frame) break;
1313
+ if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
1314
+ from = run.charStart;
1315
+ }
1316
+ for (let r = ri + 1; r < layout.runs.length; r++) {
1317
+ const run = layout.runs[r];
1318
+ if (isZero(run.rect)) continue;
1319
+ if (run.frame !== anchor.frame) break;
1320
+ if (!overlapV(run.rect.bottom, run.rect.top, bottom, top)) break;
1321
+ to = run.charStart + run.count - 1;
1322
+ }
1323
+ return [from, to];
1324
+ }
1325
+ /**
1326
+ * A single glyph's oriented cell in page space, or null for degenerate
1327
+ * glyphs — the anchor for caret placement at a selection boundary.
1328
+ */
1329
+ function textGlyphQuad(layout, glyph) {
1330
+ const g = layout.glyphs[glyph];
1331
+ if (!g || isEmpty(g.flags)) return null;
1332
+ const b = g.loose;
1333
+ if (b.right <= b.left || b.top <= b.bottom) return null;
1334
+ const run = layout.runs.find((r) => glyph >= r.charStart && glyph < r.charStart + r.count);
1335
+ const f = layout.frames[run?.frame ?? 0];
1336
+ return quadOfFrameRect(f, run?.frame ?? 0, b);
1337
+ }
1338
+ /**
1339
+ * Merged visual-line segments for the half-open char range
1340
+ * `[charStart, charStart + charCount)` — THE canonical segmentation. Chars
1341
+ * outside the layout are ignored.
1342
+ */
1343
+ function textSegmentsForRange(layout, charStart, charCount) {
1344
+ if (charCount <= 0 || layout.glyphs.length === 0) return [];
1345
+ const lo = Math.max(0, charStart);
1346
+ const hi = Math.min(layout.glyphs.length - 1, charStart + charCount - 1);
1347
+ if (hi < lo) return [];
1348
+ const subRuns = [];
1349
+ for (const run of layout.runs) {
1350
+ const runEnd = run.charStart + run.count - 1;
1041
1351
  if (runEnd < lo || run.charStart > hi) continue;
1042
1352
  const s = Math.max(lo, run.charStart);
1043
1353
  const e = Math.min(hi, runEnd);
@@ -1045,63 +1355,141 @@ function searchRectsForRange(snapshot, charStart, charCount) {
1045
1355
  let right = -Infinity;
1046
1356
  let bottom = Infinity;
1047
1357
  let top = -Infinity;
1048
- let count = 0;
1358
+ let charCountAcc = 0;
1049
1359
  let widthSum = 0;
1050
1360
  let prevRight = -Infinity;
1361
+ let firstX = 0;
1362
+ let lastX = 0;
1051
1363
  const flush = () => {
1052
- if (count > 0 && right > left && top > bottom) subRuns.push({
1364
+ if (charCountAcc > 0 && left !== Infinity) subRuns.push({
1053
1365
  rect: {
1054
1366
  left,
1055
1367
  bottom,
1056
1368
  right,
1057
1369
  top
1058
1370
  },
1059
- charCount: count,
1060
- fontSize: run.fontSize
1371
+ charCount: charCountAcc,
1372
+ fontSize: run.fontSize,
1373
+ frame: run.frame,
1374
+ firstX,
1375
+ lastX
1061
1376
  });
1062
1377
  left = Infinity;
1063
1378
  right = -Infinity;
1064
1379
  bottom = Infinity;
1065
1380
  top = -Infinity;
1066
- count = 0;
1381
+ charCountAcc = 0;
1067
1382
  widthSum = 0;
1068
1383
  prevRight = -Infinity;
1069
1384
  };
1070
1385
  for (let ci = s; ci <= e; ci++) {
1071
- const g = run.glyphs[ci - run.charStart];
1072
- if ((g.flags & FLAG_EMPTY) !== 0) continue;
1073
- const b = g.looseBox;
1074
- if (count > 0 && prevRight > -Infinity) {
1075
- const avg = widthSum / count;
1386
+ const g = layout.glyphs[ci];
1387
+ if (isEmpty(g.flags)) continue;
1388
+ const b = g.loose;
1389
+ if (charCountAcc > 0 && prevRight > -Infinity) {
1390
+ const avg = widthSum / charCountAcc;
1076
1391
  if (avg > 0 && Math.abs(b.left - prevRight) > CHAR_DISTANCE_FACTOR * avg) flush();
1077
1392
  }
1393
+ const centerX = (b.left + b.right) / 2;
1394
+ if (charCountAcc === 0) firstX = centerX;
1395
+ lastX = centerX;
1078
1396
  left = Math.min(left, b.left);
1079
1397
  right = Math.max(right, b.right);
1080
1398
  bottom = Math.min(bottom, b.bottom);
1081
1399
  top = Math.max(top, b.top);
1082
- count++;
1400
+ charCountAcc++;
1083
1401
  widthSum += b.right - b.left;
1084
1402
  prevRight = b.right;
1085
1403
  }
1086
1404
  flush();
1087
1405
  }
1088
- return mergeAdjacentRects(subRuns);
1406
+ return mergeAdjacentSubRuns(subRuns).map((m) => materializeSegment(layout, m));
1089
1407
  }
1090
- function mergeAdjacentRects(runs) {
1408
+ function mergeAdjacentSubRuns(runs) {
1091
1409
  const out = [];
1092
1410
  let prev = null;
1093
1411
  let cur = null;
1094
1412
  for (const run of runs) {
1095
- if (prev && cur && shouldMerge(prev, run)) cur = union(cur, run.rect);
1413
+ if (prev && cur && prev.frame === run.frame && shouldMerge(prev, run)) cur = {
1414
+ rect: {
1415
+ left: Math.min(cur.rect.left, run.rect.left),
1416
+ bottom: Math.min(cur.rect.bottom, run.rect.bottom),
1417
+ right: Math.max(cur.rect.right, run.rect.right),
1418
+ top: Math.max(cur.rect.top, run.rect.top)
1419
+ },
1420
+ frame: cur.frame,
1421
+ firstX: cur.firstX,
1422
+ lastX: run.lastX
1423
+ };
1096
1424
  else {
1097
1425
  if (cur) out.push(cur);
1098
- cur = run.rect;
1426
+ cur = {
1427
+ rect: run.rect,
1428
+ frame: run.frame,
1429
+ firstX: run.firstX,
1430
+ lastX: run.lastX
1431
+ };
1099
1432
  }
1100
1433
  prev = run;
1101
1434
  }
1102
- if (cur) out.push(cur);
1435
+ if (cur && cur.rect.right > cur.rect.left && cur.rect.top > cur.rect.bottom) out.push(cur);
1103
1436
  return out;
1104
1437
  }
1438
+ /** Segment quad for a frame-local rect (frame 0: the rect's own corners,
1439
+ * no float round-trip). Slot order US, UE, LS, LE; frame is y-up, so the
1440
+ * upper corners sit at the frame TOP. */
1441
+ function quadOfFrameRect(f, frameIndex, r) {
1442
+ if (frameIndex === 0) return {
1443
+ p1: {
1444
+ x: r.left,
1445
+ y: r.top
1446
+ },
1447
+ p2: {
1448
+ x: r.right,
1449
+ y: r.top
1450
+ },
1451
+ p3: {
1452
+ x: r.left,
1453
+ y: r.bottom
1454
+ },
1455
+ p4: {
1456
+ x: r.right,
1457
+ y: r.bottom
1458
+ }
1459
+ };
1460
+ return {
1461
+ p1: fromFrame(f, {
1462
+ x: r.left,
1463
+ y: r.top
1464
+ }),
1465
+ p2: fromFrame(f, {
1466
+ x: r.right,
1467
+ y: r.top
1468
+ }),
1469
+ p3: fromFrame(f, {
1470
+ x: r.left,
1471
+ y: r.bottom
1472
+ }),
1473
+ p4: fromFrame(f, {
1474
+ x: r.right,
1475
+ y: r.bottom
1476
+ })
1477
+ };
1478
+ }
1479
+ function materializeSegment(layout, m) {
1480
+ const frame = layout.frames[m.frame];
1481
+ const quad = quadOfFrameRect(frame, m.frame, m.rect);
1482
+ return {
1483
+ quad,
1484
+ rect: m.frame === 0 ? m.rect : {
1485
+ left: Math.min(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x),
1486
+ bottom: Math.min(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y),
1487
+ right: Math.max(quad.p1.x, quad.p2.x, quad.p3.x, quad.p4.x),
1488
+ top: Math.max(quad.p1.y, quad.p2.y, quad.p3.y, quad.p4.y)
1489
+ },
1490
+ advance: m.lastX >= m.firstX ? 1 : -1
1491
+ };
1492
+ }
1105
1493
  function shouldMerge(a, b) {
1106
1494
  if (a.fontSize != null && b.fontSize != null && a.fontSize > 0 && b.fontSize > 0) {
1107
1495
  if (Math.max(a.fontSize, b.fontSize) / Math.min(a.fontSize, b.fontSize) > FONT_SIZE_RATIO_THRESHOLD) return false;
@@ -1114,14 +1502,6 @@ function shouldMerge(a, b) {
1114
1502
  const bL = b.rect.left - bw;
1115
1503
  return aL < b.rect.right + bw && aR > bL;
1116
1504
  }
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
1505
  function verticalOverlap(a, b) {
1126
1506
  const ah = a.top - a.bottom;
1127
1507
  const bh = b.top - b.bottom;
@@ -1130,6 +1510,28 @@ function verticalOverlap(a, b) {
1130
1510
  if (u === ah || u === bh) return 1;
1131
1511
  return Math.max(0, Math.min(a.top, b.top) - Math.max(a.bottom, b.bottom)) / u;
1132
1512
  }
1513
+ const isZero = (r) => r.right === r.left && r.top === r.bottom;
1514
+ function overlapV(bottom1, top1, bottom2, top2) {
1515
+ const u = Math.max(top1, top2) - Math.min(bottom1, bottom2);
1516
+ if (u === 0) return false;
1517
+ return Math.max(0, Math.min(top1, top2) - Math.max(bottom1, bottom2)) / u >= LINE_OVERLAP_THRESHOLD;
1518
+ }
1519
+ //#endregion
1520
+ //#region src/search/rects.ts
1521
+ /**
1522
+ * Match-range → canonical visual-line segments. Thin wrappers over the ONE
1523
+ * text layout (`src/text/layout.ts`) — search owns matching and cursors,
1524
+ * never segmentation, so a match highlights exactly like a selection of the
1525
+ * same characters. No frame or merge logic lives here.
1526
+ */
1527
+ /** Canonical segments for `[charStart, charStart + charCount)`. */
1528
+ function searchSegmentsForRange(snapshot, charStart, charCount) {
1529
+ return textSegmentsForRange(buildPageTextLayout(snapshot), charStart, charCount);
1530
+ }
1531
+ /** The segments' AABBs — for consumers that genuinely want boxes. */
1532
+ function searchRectsForRange(snapshot, charStart, charCount) {
1533
+ return searchSegmentsForRange(snapshot, charStart, charCount).map((s) => s.rect);
1534
+ }
1133
1535
  //#endregion
1134
1536
  //#region src/search/epoch.ts
1135
1537
  /**
@@ -1362,6 +1764,6 @@ function materializePdfPermissions(b) {
1362
1764
  return [...out];
1363
1765
  }
1364
1766
  //#endregion
1365
- export { pdfRectWidth as $, refKey as A, decodeStableIdKey as B, appearanceImpactOf as C, shiftsExistingAnnotationIndices as D, invalidatesWeakIndexRefs as E, resolveBinarySource as F, normalizePdfRect as G, isValidPageObjectNumber as H, NO_ANNOTATION_FLAGS as I, pdfQuadFromCorners as J, pdfQuadBounds as K, UNKNOWN_WEAK_ANNOTATION_STATE as L, normalizeAnnotationPatch as M, normalizeAttachmentFileSource as N, buildThreads as O, sniffBinaryMetadata as P, pdfRectToOriginSize as Q, knownWeakAnnotationState as R, encodeFieldRefKey as S, changesAnnotationList as T, DEFAULT_PDF_SAVE_MODE as U, encodeStableIdKey as V, createPageImageHandle as W, pdfRectHeight as X, pdfRectFromOriginSize as Y, pdfRectSize as Z, foldOptionsFor as _, PDF_BITS as a, wordBefore as b, searchContentEpoch as c, buildSnippet as d, matchPageText as f, validateSearchRegex as g, validateSearchQuery as h, pdfPermissions as i, normalizeAnnotationDraft as j, classifyRelation as k, searchRectsForRange as l, matchRegex as m, collab as n, decodePdfBits as o, SEARCH_REGEX_MAX_LENGTH as p, pdfQuadCorners as q, materializePdfPermissions as r, canonicalSearchQuery as s, caps as t, SEARCH_SNIPPET_CONTEXT as u, matchLiteral as v, semanticEqual as w, decodeFieldRefKey as x, wordAt as y, revisionTokensEqual as z };
1767
+ 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, decodeSubmitFormFlags as et, 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
1768
 
1367
- //# sourceMappingURL=builders-C548U47M.js.map
1769
+ //# sourceMappingURL=builders-Cdz34nbF.js.map