@embedpdf/engine-core 3.0.0-next.0 → 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.
Files changed (56) 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-DDxHpJQU.d.cts} +538 -41
  6. package/dist/{AnnotationListSnapshot-W0RCsyDE.d.ts → AnnotationListSnapshot-DDxHpJQU.d.ts} +538 -41
  7. package/dist/{DocumentSecurityService-Df79fgpe.d.cts → DocumentSecurityService-BV4_YXkU.d.cts} +33 -2
  8. package/dist/{DocumentSecurityService-B-tULKNY.d.ts → DocumentSecurityService-o9aeTHwT.d.ts} +33 -2
  9. package/dist/{Engine-D9loFZc1.d.cts → Engine-D9je6GdU.d.cts} +50 -19
  10. package/dist/{Engine-B7Oe7f5i.d.ts → Engine-DOoVem7_.d.ts} +50 -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-B_KufGJ9.js} +547 -169
  16. package/dist/builders-B_KufGJ9.js.map +1 -0
  17. package/dist/{builders-BvikWRuO.cjs → builders-Cp_rlGob.cjs} +603 -225
  18. package/dist/builders-Cp_rlGob.cjs.map +1 -0
  19. package/dist/conformance.cjs +822 -198
  20. package/dist/conformance.cjs.map +1 -1
  21. package/dist/conformance.d.cts +124 -6
  22. package/dist/conformance.d.ts +124 -6
  23. package/dist/conformance.js +777 -157
  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 +36 -18
  30. package/dist/index.d.cts +2 -2
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +4 -4
  33. package/dist/runtime.cjs +41 -23
  34. package/dist/runtime.cjs.map +1 -1
  35. package/dist/runtime.d.cts +103 -5
  36. package/dist/runtime.d.ts +103 -5
  37. package/dist/runtime.js +5 -5
  38. package/dist/{schemas-B_1VyFsm.cjs → schemas-D1pxEv3t.cjs} +138 -32
  39. package/dist/schemas-D1pxEv3t.cjs.map +1 -0
  40. package/dist/{schemas-9OpSSeM6.js → schemas-DAnu8tuW.js} +95 -13
  41. package/dist/schemas-DAnu8tuW.js.map +1 -0
  42. package/dist/wire.cjs +118 -3
  43. package/dist/wire.cjs.map +1 -1
  44. package/dist/wire.d.cts +139 -27
  45. package/dist/wire.d.ts +139 -27
  46. package/dist/wire.js +111 -4
  47. package/dist/wire.js.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/builders-BvikWRuO.cjs.map +0 -1
  50. package/dist/builders-C548U47M.js.map +0 -1
  51. package/dist/fold-CB4fAOOy.js.map +0 -1
  52. package/dist/fold-IaMph3CO.cjs.map +0 -1
  53. package/dist/schemas-9OpSSeM6.js.map +0 -1
  54. package/dist/schemas-B_1VyFsm.cjs.map +0 -1
  55. package/dist/subtype-BvAzLCGC.js.map +0 -1
  56. package/dist/subtype-qejjVr6h.cjs.map +0 -1
@@ -1,120 +1,5 @@
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
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 grouping.
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/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
- */
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
- * 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.
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 searchRectsForRange(snapshot, charStart, charCount) {
1035
- if (charCount <= 0) return [];
1036
- const lo = charStart;
1037
- const hi = charStart + charCount - 1;
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
- const runEnd = run.charStart + run.glyphs.length - 1;
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 count = 0;
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 (count > 0 && right > left && top > bottom) subRuns.push({
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: count,
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
- count = 0;
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 = 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;
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
- count++;
1376
+ charCountAcc++;
1083
1377
  widthSum += b.right - b.left;
1084
1378
  prevRight = b.right;
1085
1379
  }
1086
1380
  flush();
1087
1381
  }
1088
- return mergeAdjacentRects(subRuns);
1382
+ return mergeAdjacentSubRuns(subRuns).map((m) => materializeSegment(layout, m));
1089
1383
  }
1090
- function mergeAdjacentRects(runs) {
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 = union(cur, run.rect);
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 = run.rect;
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 { 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 };
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-C548U47M.js.map
1745
+ //# sourceMappingURL=builders-B_KufGJ9.js.map