@embedpdf/engine-services 3.0.0-next.3 → 3.0.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AbortError, EMPTY_TRANSFER, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, UNKNOWN_WEAK_ANNOTATION_STATE, appearanceImpactOf, buildSnippet, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, revisionTokensEqual, searchRectsForRange, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
1
+ import { AbortError, EMPTY_TRANSFER, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, UNKNOWN_WEAK_ANNOTATION_STATE, appearanceImpactOf, buildPageTextLayout, buildSnippet, charMapViolation, charRangeForTextOffsets, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, pdfQuadBounds, revisionTokensEqual, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, textSegmentsForRange, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
2
2
  import { NULL_PTR } from "@embedpdf/engine-runtime";
3
3
  //#region src/runtime/lifecycle/bootstrap.ts
4
4
  const initialized = /* @__PURE__ */ new WeakSet();
@@ -1163,13 +1163,34 @@ var MetadataMutator = class {
1163
1163
  };
1164
1164
  //#endregion
1165
1165
  //#region src/runtime/memory/structs.ts
1166
+ /** Generated from `EPDF_CHAR_GEOMETRY`; pinned by native static_asserts. */
1167
+ const EPDF_CHAR_GEOMETRY_LAYOUT = {
1168
+ bytes: 124,
1169
+ offsets: {
1170
+ looseBox: 0,
1171
+ tightBox: 16,
1172
+ looseQuad: 32,
1173
+ tightQuad: 64,
1174
+ matrix: 96,
1175
+ flags: 120
1176
+ },
1177
+ flags: {
1178
+ hasTightBox: 1,
1179
+ hasLooseQuad: 2,
1180
+ hasTightQuad: 4,
1181
+ upright: 8,
1182
+ space: 16,
1183
+ empty: 32,
1184
+ synthesized: 64
1185
+ }
1186
+ };
1166
1187
  /** Decode an `FS_RECTF` struct that a native call wrote into `ptr`. */
1167
- function readRectF(mem, ptr) {
1188
+ function readRectF(mem, ptr, byteOffset = 0) {
1168
1189
  return {
1169
- left: readF32(mem, ptr, 0),
1170
- top: readF32(mem, ptr, 4),
1171
- right: readF32(mem, ptr, 8),
1172
- bottom: readF32(mem, ptr, 12)
1190
+ left: readF32(mem, ptr, byteOffset),
1191
+ top: readF32(mem, ptr, byteOffset + 4),
1192
+ right: readF32(mem, ptr, byteOffset + 8),
1193
+ bottom: readF32(mem, ptr, byteOffset + 12)
1173
1194
  };
1174
1195
  }
1175
1196
  /** Decode an `FS_SIZEF` struct that a native call wrote into `ptr`. */
@@ -2153,6 +2174,49 @@ function readQuadPoints(fn, mem, annotPtr) {
2153
2174
  });
2154
2175
  }
2155
2176
  //#endregion
2177
+ //#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
2178
+ /**
2179
+ * Read-side twin of `writers/.../writeAnnotationTransformMetadata.ts`. Decodes
2180
+ * the EmbedPDF rotation keys from /EMBD_Metadata back onto the DTO:
2181
+ *
2182
+ * - `Rotation` — degrees, PDF convention. Returned for EVERY rotatable
2183
+ * kind (box AND vertex); the plugin converts it back to its CW-content
2184
+ * `rot` once at the repository seam.
2185
+ * - `UnrotatedRect` — the logical box. BOX kinds only (square/circle/
2186
+ * free-text); a vertex annotation never carries one (its points are the
2187
+ * visual), so vertex readers don't ask for it.
2188
+ *
2189
+ * Both are absent on un-rotated annotations (and on everything authored before
2190
+ * rotation shipped), so both reads return `undefined` and the DTO field stays
2191
+ * unset.
2192
+ */
2193
+ const KEY_ROTATION$1 = "Rotation";
2194
+ const KEY_UNROTATED_RECT$1 = "UnrotatedRect";
2195
+ /** The advisory/portable rotation scalar (degrees, PDF convention), or
2196
+ * `undefined` when absent or zero. */
2197
+ function readAnnotationRotation(fn, mem, annotPtr) {
2198
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
2199
+ return withScratch(mem, 4, (buf) => {
2200
+ if (!fn.EPDFAnnot_GetEmbedMetadataNumber(annotPtr, KEY_ROTATION$1, buf)) return void 0;
2201
+ const v = readF32(mem, buf);
2202
+ return v ? v : void 0;
2203
+ });
2204
+ }
2205
+ /** The logical (unrotated) box of a rotated BOX annotation, or `undefined`. */
2206
+ function readAnnotationUnrotatedRect(fn, mem, annotPtr) {
2207
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
2208
+ return withScratch(mem, 16, (buf) => {
2209
+ if (!fn.EPDFAnnot_GetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT$1, buf)) return void 0;
2210
+ const r = readRectF(mem, buf);
2211
+ return {
2212
+ left: r.left,
2213
+ bottom: r.bottom,
2214
+ right: r.right,
2215
+ top: r.top
2216
+ };
2217
+ });
2218
+ }
2219
+ //#endregion
2156
2220
  //#region src/features/annotations/internal/textEditIntent.ts
2157
2221
  /** Adobe-compatible `/IT` names, normalized at the engine-core boundary. */
2158
2222
  const IT_REPLACE = "Replace";
@@ -2187,13 +2251,17 @@ function readCaret(fn, mem, annotPtr, base) {
2187
2251
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
2188
2252
  const rd = readRectangleDifferences(fn, mem, annotPtr);
2189
2253
  const intent = caretIntentFromName(readIntent(fn, mem, annotPtr));
2254
+ const rotation = readAnnotationRotation(fn, mem, annotPtr);
2255
+ const unrotatedRect = readAnnotationUnrotatedRect(fn, mem, annotPtr);
2190
2256
  return {
2191
2257
  ...base,
2192
2258
  subtype: "caret",
2193
2259
  intent,
2194
2260
  color,
2195
2261
  opacity,
2196
- rectDifferences: rd
2262
+ rectDifferences: rd,
2263
+ ...rotation != null ? { rotation } : {},
2264
+ ...unrotatedRect ? { unrotatedRect } : {}
2197
2265
  };
2198
2266
  }
2199
2267
  //#endregion
@@ -2611,49 +2679,6 @@ function readFilledStyleExtras(fn, mem, annotPtr) {
2611
2679
  };
2612
2680
  }
2613
2681
  //#endregion
2614
- //#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
2615
- /**
2616
- * Read-side twin of `writers/.../writeAnnotationTransformMetadata.ts`. Decodes
2617
- * the EmbedPDF rotation keys from /EMBD_Metadata back onto the DTO:
2618
- *
2619
- * - `Rotation` — degrees, PDF convention. Returned for EVERY rotatable
2620
- * kind (box AND vertex); the plugin converts it back to its CW-content
2621
- * `rot` once at the repository seam.
2622
- * - `UnrotatedRect` — the logical box. BOX kinds only (square/circle/
2623
- * free-text); a vertex annotation never carries one (its points are the
2624
- * visual), so vertex readers don't ask for it.
2625
- *
2626
- * Both are absent on un-rotated annotations (and on everything authored before
2627
- * rotation shipped), so both reads return `undefined` and the DTO field stays
2628
- * unset.
2629
- */
2630
- const KEY_ROTATION$1 = "Rotation";
2631
- const KEY_UNROTATED_RECT$1 = "UnrotatedRect";
2632
- /** The advisory/portable rotation scalar (degrees, PDF convention), or
2633
- * `undefined` when absent or zero. */
2634
- function readAnnotationRotation(fn, mem, annotPtr) {
2635
- if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
2636
- return withScratch(mem, 4, (buf) => {
2637
- if (!fn.EPDFAnnot_GetEmbedMetadataNumber(annotPtr, KEY_ROTATION$1, buf)) return void 0;
2638
- const v = readF32(mem, buf);
2639
- return v ? v : void 0;
2640
- });
2641
- }
2642
- /** The logical (unrotated) box of a rotated BOX annotation, or `undefined`. */
2643
- function readAnnotationUnrotatedRect(fn, mem, annotPtr) {
2644
- if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
2645
- return withScratch(mem, 16, (buf) => {
2646
- if (!fn.EPDFAnnot_GetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT$1, buf)) return void 0;
2647
- const r = readRectF(mem, buf);
2648
- return {
2649
- left: r.left,
2650
- bottom: r.bottom,
2651
- right: r.right,
2652
- top: r.top
2653
- };
2654
- });
2655
- }
2656
- //#endregion
2657
2682
  //#region src/features/annotations/internal/read/readFreeTextAnnotation.ts
2658
2683
  /** Default `/DA` colour (black) when an annotation has no default appearance. */
2659
2684
  const DEFAULT_FREETEXT_COLOR$1 = {
@@ -4130,17 +4155,21 @@ const ANNOT_SUBTYPE_WIDGET = 20;
4130
4155
  /** `FPDF_ANNOT_FREETEXT` — free-text annotation subtype code. */
4131
4156
  const ANNOT_SUBTYPE_FREETEXT = 3;
4132
4157
  /**
4133
- * BOX-family subtypes (free-text 3, square 5, circle 6, stamp 13): the kinds
4134
- * whose v3 writers put rotation in the AP `/Matrix` + `/EMBD_Metadata`
4135
- * `/UnrotatedRect`. Only these are eligible for rotation-stripped appearance
4136
- * rendering — vertex kinds (line/polyline/polygon/ink) pre-rotate their
4137
- * geometry, so their rasters must stay on the classic path.
4158
+ * BOX-family subtypes (free-text 3, square 5, circle 6, stamp 13, caret 14):
4159
+ * the kinds whose v3 writers put rotation in the AP `/Matrix` +
4160
+ * `/EMBD_Metadata` `/UnrotatedRect`. Only these are eligible for
4161
+ * rotation-stripped appearance rendering — vertex kinds
4162
+ * (line/polyline/polygon/ink) pre-rotate their geometry, so their rasters must
4163
+ * stay on the classic path. This set MUST cover every kind whose reader
4164
+ * surfaces the rotation pair: `fromDTO` (plugin-annotation repository) mirrors
4165
+ * this exact condition to re-apply the stripped rotation as `apRot`.
4138
4166
  */
4139
4167
  const BOX_FAMILY_SUBTYPES = /* @__PURE__ */ new Set([
4140
4168
  3,
4141
4169
  5,
4142
4170
  6,
4143
- 13
4171
+ 13,
4172
+ 14
4144
4173
  ]);
4145
4174
  /**
4146
4175
  * Maps an `AnnotationAppearanceMode` onto the PDFium appearance-mode int and
@@ -4829,102 +4858,281 @@ function readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, index, revis
4829
4858
  return reader(fn, mem, annotPtr, base, subtypeCode, ctx);
4830
4859
  }
4831
4860
  //#endregion
4832
- //#region src/features/annotations/internal/write/writeCaretAnnotation.ts
4833
- /** Default `/C` colour when a caret draft omits it (engine-wide default mark). */
4834
- const DEFAULT_CARET_COLOR = {
4835
- r: 255,
4836
- g: 0,
4837
- b: 0
4838
- };
4839
- /** Default opacity, set explicitly so reads always round-trip the same value. */
4840
- const DEFAULT_OPACITY$4 = 1;
4861
+ //#region src/features/annotations/internal/write/writeEmbedMetadata.ts
4841
4862
  /**
4842
- * Apply a caret draft to a freshly-created annotation. Caret carries no
4843
- * geometry of its own beyond `/Rect`. Order:
4844
- * 1. base author-metadata (contents/nm/flags)
4845
- * 2. `/Rect` (required — supplied by the caller; never derived)
4846
- * 3. `/C` color + `/CA` opacity
4847
- * 4. `/RD` rectangle differences (optional)
4863
+ * Write the EmbedPDF-namespaced /EMBD_Metadata annotation dictionary.
4864
+ *
4865
+ * This module owns ONLY the vendor-extension dictionary. Standard PDF
4866
+ * base fields (/T, /M, /NM, /Contents) are handled by
4867
+ * `writers/annotations/base.ts` — /EMBD_Metadata is a separate
4868
+ * dictionary nested under the annotation that carries cloud-side
4869
+ * identity bookkeeping which has no PDF-spec equivalent.
4870
+ *
4871
+ * Schema (v1):
4872
+ * /EMBD_Metadata <<
4873
+ * /SchemaVersion 1 % EmbedPDF metadata schema version
4874
+ * /UserID (acme/alice) % identity that created the annotation
4875
+ * /GroupID (engineering) % group the annotation belongs to
4876
+ * /CreatedBy (acme/alice) % same as UserID on create
4877
+ * /UpdatedBy (acme/alice) % last editor; refreshed on every update
4878
+ * >>
4879
+ *
4880
+ * Read side: see `readers/annotations/embed-metadata.ts` (commit 12).
4881
+ *
4882
+ * PDFium bindings used: the `EPDFAnnot_*EmbedMetadata*` extension API
4883
+ * shipped in the EmbedPDF PDFium fork. Values are written as UTF-16LE
4884
+ * strings via the same `mem.writeU16String` pattern the base writer
4885
+ * uses for /T.
4848
4886
  */
4849
- function applyCaretDraft(fn, mem, annotPtr, draft) {
4850
- applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
4851
- setAnnotRect(fn, mem, annotPtr, draft.rect);
4852
- setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_CARET_COLOR);
4853
- setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$4);
4854
- if (draft.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(draft.intent));
4855
- if (draft.rectDifferences != null) setRectangleDifferences(fn, annotPtr, draft.rectDifferences);
4856
- }
4887
+ const KEY_SCHEMA_VERSION = "SchemaVersion";
4888
+ const KEY_USER_ID = "UserID";
4889
+ const KEY_GROUP_ID = "GroupID";
4890
+ const KEY_CREATED_BY = "CreatedBy";
4891
+ const KEY_UPDATED_BY = "UpdatedBy";
4857
4892
  /**
4858
- * Apply a caret patch to an existing annotation. Only present fields are
4859
- * touched.
4893
+ * Stamp /EMBD_Metadata on a freshly created annotation. No-op if the
4894
+ * actor has no identity fields — the worker still emits the standard
4895
+ * /M timestamp via the base writer; only the vendor dictionary is
4896
+ * skipped.
4897
+ *
4898
+ * actor.userId → /UserID, /CreatedBy, /UpdatedBy
4899
+ * actor.groupId → /GroupID
4900
+ *
4901
+ * /SchemaVersion is written whenever any field is written.
4860
4902
  */
4861
- function applyCaretPatch(fn, mem, annotPtr, patch) {
4862
- applyAnnotationBasePatch(fn, mem, annotPtr, patch);
4863
- if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
4864
- if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
4865
- if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
4866
- if (patch.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(patch.intent));
4867
- if (patch.rectDifferences === null) clearRectangleDifferences(fn, annotPtr);
4868
- else if (patch.rectDifferences !== void 0) setRectangleDifferences(fn, annotPtr, patch.rectDifferences);
4903
+ function applyEmbedMetadataOnCreate(fn, mem, annotPtr, actor) {
4904
+ if (!actor || !actor.userId && !actor.groupId) return;
4905
+ fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
4906
+ if (actor.userId) {
4907
+ setMetadataString(fn, mem, annotPtr, KEY_USER_ID, actor.userId);
4908
+ setMetadataString(fn, mem, annotPtr, KEY_CREATED_BY, actor.userId);
4909
+ setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
4910
+ }
4911
+ if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
4869
4912
  }
4870
4913
  /**
4871
- * Type-narrowing predicate used by the writer registry to pick the caret
4872
- * writer for a draft/patch's `subtype`.
4914
+ * Apply /EMBD_Metadata refreshes on update. Two fields can change on
4915
+ * update; everything else (UserID, CreatedBy) is immutable.
4916
+ *
4917
+ * actor.userId → /UpdatedBy (refreshed every update where the
4918
+ * caller has an identity)
4919
+ * actor.groupId → /GroupID (reassigned only when the patch
4920
+ * explicitly contains a new groupId; route runs
4921
+ * `checkSetGroup` first)
4922
+ *
4923
+ * If the target annotation has no EMBD_Metadata yet (created before
4924
+ * the cloud started stamping, or by a non-cloud writer), we seed
4925
+ * /SchemaVersion so subsequent readers can find the marker.
4926
+ *
4927
+ * No-op when the actor has neither userId nor groupId.
4873
4928
  */
4874
- function isCaretSubtype(subtype) {
4875
- return subtype === "caret";
4929
+ function applyEmbedMetadataOnUpdate(fn, mem, annotPtr, actor) {
4930
+ if (!actor || !actor.userId && !actor.groupId) return;
4931
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
4932
+ if (actor.userId) setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
4933
+ if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
4876
4934
  }
4877
- //#endregion
4878
- //#region src/features/annotations/internal/write/writeFileAttachmentAnnotation.ts
4879
- /** Default `/C` — the generator's default icon fill, set explicitly so reads round-trip. */
4880
- const DEFAULT_FILE_ATTACHMENT_COLOR = {
4881
- r: 255,
4882
- g: 255,
4883
- b: 0
4884
- };
4885
- const DEFAULT_OPACITY$3 = 1;
4886
- /**
4887
- * Validate a file-attachment draft before any native write: the wire
4888
- * `file.resource` ref must have arrived with the mutation. No format
4889
- * sniffing — unlike stamps, ANY bytes are a valid attachment.
4890
- */
4891
- function preflightFileAttachmentDraft(draft, ctx) {
4892
- requireFileResource(draft.file, ctx);
4893
- if (ctx?.docPtr === void 0) throw new EngineError(EngineErrorCode.Unknown, "file-attachment writer requires docPtr on the write context");
4935
+ function setMetadataString(fn, mem, annotPtr, key, value) {
4936
+ const ptr = mem.writeU16String(value);
4937
+ try {
4938
+ fn.EPDFAnnot_SetEmbedMetadataString(annotPtr, key, ptr);
4939
+ } finally {
4940
+ mem.free(ptr);
4941
+ }
4894
4942
  }
4943
+ //#endregion
4944
+ //#region src/features/annotations/internal/write/writeAnnotationTransformMetadata.ts
4895
4945
  /**
4896
- * Apply a file-attachment draft. Order:
4897
- * 1. base author-metadata (contents/nm/flags)
4898
- * 2. `/Rect` + `/C` + `/CA` + `/Name` icon
4899
- * 3. the embedded file: `/FS` filespec via `FPDFAnnot_AddFileAttachment`,
4900
- * bytes via `FPDFAttachment_SetFile` (which also writes `/Params`
4901
- * Size/CheckSum/CreationDate), declared mime via
4902
- * `EPDFAttachment_SetSubtype`, optional `/Desc`.
4946
+ * Write the EmbedPDF transform keys under /EMBD_Metadata. This is the seam
4947
+ * PDFium's native AP generator reads to bake a ROTATED appearance:
4903
4948
  *
4904
- * The icon appearance itself is generator-owned: the mutator's closing
4905
- * `regenerateAppearance` bakes it from `/C` + `/Name`
4906
- * (GenerateFileAttachmentAP), exactly like the text note.
4907
- */
4908
- function applyFileAttachmentDraft(fn, mem, annotPtr, draft, ctx) {
4909
- applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
4910
- setAnnotRect(fn, mem, annotPtr, draft.rect);
4911
- setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_FILE_ATTACHMENT_COLOR);
4912
- setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$3);
4913
- setFileAttachmentIcon(fn, annotPtr, draft.icon ?? "paperclip");
4914
- attachFile(fn, mem, annotPtr, draft.file, requireFileResource(draft.file, ctx), ctx.docPtr);
4915
- }
4916
- /** Patch: presentation only — the attached file is create-only by design. */
4917
- function applyFileAttachmentPatch(fn, mem, annotPtr, patch) {
4918
- applyAnnotationBasePatch(fn, mem, annotPtr, patch);
4919
- if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
4920
- if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
4921
- if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
4922
- if (patch.icon !== void 0) setFileAttachmentIcon(fn, annotPtr, patch.icon);
4923
- }
4924
- function isFileAttachmentSubtype(subtype) {
4925
- return subtype === "file-attachment";
4926
- }
4927
- function setFileAttachmentIcon(fn, annotPtr, icon) {
4949
+ * /EMBD_Metadata <<
4950
+ * /Rotation 45 % degrees, PDF convention (CCW)
4951
+ * /UnrotatedRect [x0 y0 x1 y1] % BOX kinds only — the logical box
4952
+ * >>
4953
+ *
4954
+ * The family split (see the v3 plan):
4955
+ * - BOX kinds (square/circle/free-text): `/Rotation` + `/UnrotatedRect`.
4956
+ * With BOTH present the AP generator emits an `/AP /Matrix` that rotates a
4957
+ * box-sized appearance about the box centre, and `/Rect` is the enclosing
4958
+ * AABB. This is the portable, externally-correct rotation.
4959
+ * - VERTEX kinds (line/polyline/polygon/ink): `/Rotation` ONLY (advisory).
4960
+ * The points are already rotated (they are the visual), so a lone
4961
+ * `/Rotation` is INERT for the AP generator (it ignores `/Rotation` with no
4962
+ * `/UnrotatedRect`) — it just records the applied angle so EmbedPDF can
4963
+ * reconstruct an oriented selection box + offer reset.
4964
+ *
4965
+ * Both fields follow the engine-wide tri-state law ("a patch touches what it
4966
+ * states, preserves what it omits"):
4967
+ * - `undefined` → the key is UNTOUCHED (a rect-only move keeps its rotation).
4968
+ * - `null` or `0` → CLEAR just that key via `EPDFAnnot_ClearEmbedMetadataKey`
4969
+ * (0 ≡ no rotation is the canonical identity, not a sentinel; never clear
4970
+ * the whole dict — identity fields UserID/GroupID/CreatedBy/UpdatedBy must
4971
+ * survive).
4972
+ * - a value → SET the key. Setting a nonzero rotation with no
4973
+ * `/UnrotatedRect` (neither in the patch nor already on the annotation)
4974
+ * is an unsatisfiable state for the AP generator and throws `InvalidArg`.
4975
+ *
4976
+ * MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
4977
+ * `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
4978
+ */
4979
+ const KEY_ROTATION = "Rotation";
4980
+ const KEY_UNROTATED_RECT = "UnrotatedRect";
4981
+ /** Seed `/SchemaVersion` when we are about to create the dict by writing the
4982
+ * first transform key, so the marker readers look for is always present. */
4983
+ function ensureSchemaVersion(fn, annotPtr) {
4984
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
4985
+ }
4986
+ function setRotation(fn, annotPtr, rotation) {
4987
+ ensureSchemaVersion(fn, annotPtr);
4988
+ fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
4989
+ }
4990
+ function setUnrotatedRect(fn, mem, annotPtr, rect) {
4991
+ ensureSchemaVersion(fn, annotPtr);
4992
+ const buf = mem.alloc(16);
4993
+ try {
4994
+ mem.poke(buf, "f32", rect.left, 0);
4995
+ mem.poke(buf, "f32", rect.top, 4);
4996
+ mem.poke(buf, "f32", rect.right, 8);
4997
+ mem.poke(buf, "f32", rect.bottom, 12);
4998
+ fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
4999
+ } finally {
5000
+ mem.free(buf);
5001
+ }
5002
+ }
5003
+ /**
5004
+ * Write transform metadata for a BOX kind (square/circle/free-text/stamp),
5005
+ * tri-state per field: `undefined` never touches the document (safe to call on
5006
+ * every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
5007
+ * unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
5008
+ * generator needs the pair — and throws instead of writing a broken state.
5009
+ */
5010
+ function writeBoxTransformMetadata(fn, mem, annotPtr, t) {
5011
+ if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
5012
+ if (!(t.unrotatedRect != null || t.unrotatedRect === void 0 && readAnnotationUnrotatedRect(fn, mem, annotPtr) != null)) throw new EngineError(EngineErrorCode.InvalidArg, "rotation requires unrotatedRect: supply the pair together (or rely on an existing /EMBD_Metadata/UnrotatedRect)");
5013
+ setRotation(fn, annotPtr, t.rotation);
5014
+ } else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5015
+ if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
5016
+ else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
5017
+ }
5018
+ /**
5019
+ * Write the advisory `/Rotation` scalar for a VERTEX kind
5020
+ * (line/polyline/polygon/ink), tri-state: `undefined` never touches the
5021
+ * document, `null`/`0` clears, a value sets. Whenever it does write, any
5022
+ * `/UnrotatedRect` is defensively cleared — that key is an impossible state on
5023
+ * a vertex kind and must never accidentally drive the AP generator.
5024
+ */
5025
+ function writeVertexTransformMetadata(fn, annotPtr, t) {
5026
+ if (t.rotation === void 0) return;
5027
+ if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
5028
+ else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5029
+ fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
5030
+ }
5031
+ //#endregion
5032
+ //#region src/features/annotations/internal/write/writeCaretAnnotation.ts
5033
+ /** Default `/C` colour when a caret draft omits it (engine-wide default mark). */
5034
+ const DEFAULT_CARET_COLOR = {
5035
+ r: 255,
5036
+ g: 0,
5037
+ b: 0
5038
+ };
5039
+ /** Default opacity, set explicitly so reads always round-trip the same value. */
5040
+ const DEFAULT_OPACITY$4 = 1;
5041
+ /**
5042
+ * Apply a caret draft to a freshly-created annotation. Caret carries no
5043
+ * geometry of its own beyond `/Rect`. Order:
5044
+ * 1. base author-metadata (contents/nm/flags)
5045
+ * 2. `/Rect` (required — supplied by the caller; never derived)
5046
+ * 3. `/C` color + `/CA` opacity
5047
+ * 4. `/RD` rectangle differences (optional)
5048
+ */
5049
+ function applyCaretDraft(fn, mem, annotPtr, draft) {
5050
+ applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
5051
+ setAnnotRect(fn, mem, annotPtr, draft.rect);
5052
+ writeBoxTransformMetadata(fn, mem, annotPtr, {
5053
+ rotation: draft.rotation,
5054
+ unrotatedRect: draft.unrotatedRect
5055
+ });
5056
+ setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_CARET_COLOR);
5057
+ setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$4);
5058
+ if (draft.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(draft.intent));
5059
+ if (draft.rectDifferences != null) setRectangleDifferences(fn, annotPtr, draft.rectDifferences);
5060
+ }
5061
+ /**
5062
+ * Apply a caret patch to an existing annotation. Only present fields are
5063
+ * touched.
5064
+ */
5065
+ function applyCaretPatch(fn, mem, annotPtr, patch) {
5066
+ applyAnnotationBasePatch(fn, mem, annotPtr, patch);
5067
+ if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
5068
+ if (patch.rotation !== void 0 || patch.unrotatedRect !== void 0) writeBoxTransformMetadata(fn, mem, annotPtr, {
5069
+ rotation: patch.rotation,
5070
+ unrotatedRect: patch.unrotatedRect
5071
+ });
5072
+ if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
5073
+ if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
5074
+ if (patch.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(patch.intent));
5075
+ if (patch.rectDifferences === null) clearRectangleDifferences(fn, annotPtr);
5076
+ else if (patch.rectDifferences !== void 0) setRectangleDifferences(fn, annotPtr, patch.rectDifferences);
5077
+ }
5078
+ /**
5079
+ * Type-narrowing predicate used by the writer registry to pick the caret
5080
+ * writer for a draft/patch's `subtype`.
5081
+ */
5082
+ function isCaretSubtype(subtype) {
5083
+ return subtype === "caret";
5084
+ }
5085
+ //#endregion
5086
+ //#region src/features/annotations/internal/write/writeFileAttachmentAnnotation.ts
5087
+ /** Default `/C` — the generator's default icon fill, set explicitly so reads round-trip. */
5088
+ const DEFAULT_FILE_ATTACHMENT_COLOR = {
5089
+ r: 255,
5090
+ g: 255,
5091
+ b: 0
5092
+ };
5093
+ const DEFAULT_OPACITY$3 = 1;
5094
+ /**
5095
+ * Validate a file-attachment draft before any native write: the wire
5096
+ * `file.resource` ref must have arrived with the mutation. No format
5097
+ * sniffing — unlike stamps, ANY bytes are a valid attachment.
5098
+ */
5099
+ function preflightFileAttachmentDraft(draft, ctx) {
5100
+ requireFileResource(draft.file, ctx);
5101
+ if (ctx?.docPtr === void 0) throw new EngineError(EngineErrorCode.Unknown, "file-attachment writer requires docPtr on the write context");
5102
+ }
5103
+ /**
5104
+ * Apply a file-attachment draft. Order:
5105
+ * 1. base author-metadata (contents/nm/flags)
5106
+ * 2. `/Rect` + `/C` + `/CA` + `/Name` icon
5107
+ * 3. the embedded file: `/FS` filespec via `FPDFAnnot_AddFileAttachment`,
5108
+ * bytes via `FPDFAttachment_SetFile` (which also writes `/Params`
5109
+ * Size/CheckSum/CreationDate), declared mime via
5110
+ * `EPDFAttachment_SetSubtype`, optional `/Desc`.
5111
+ *
5112
+ * The icon appearance itself is generator-owned: the mutator's closing
5113
+ * `regenerateAppearance` bakes it from `/C` + `/Name`
5114
+ * (GenerateFileAttachmentAP), exactly like the text note.
5115
+ */
5116
+ function applyFileAttachmentDraft(fn, mem, annotPtr, draft, ctx) {
5117
+ applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
5118
+ setAnnotRect(fn, mem, annotPtr, draft.rect);
5119
+ setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_FILE_ATTACHMENT_COLOR);
5120
+ setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$3);
5121
+ setFileAttachmentIcon(fn, annotPtr, draft.icon ?? "paperclip");
5122
+ attachFile(fn, mem, annotPtr, draft.file, requireFileResource(draft.file, ctx), ctx.docPtr);
5123
+ }
5124
+ /** Patch: presentation only — the attached file is create-only by design. */
5125
+ function applyFileAttachmentPatch(fn, mem, annotPtr, patch) {
5126
+ applyAnnotationBasePatch(fn, mem, annotPtr, patch);
5127
+ if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
5128
+ if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
5129
+ if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
5130
+ if (patch.icon !== void 0) setFileAttachmentIcon(fn, annotPtr, patch.icon);
5131
+ }
5132
+ function isFileAttachmentSubtype(subtype) {
5133
+ return subtype === "file-attachment";
5134
+ }
5135
+ function setFileAttachmentIcon(fn, annotPtr, icon) {
4928
5136
  if (!fn.EPDFAnnot_SetName(annotPtr, FILE_ICON_TO_CODE[icon])) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
4929
5137
  }
4930
5138
  function requireFileResource(file, ctx) {
@@ -5039,177 +5247,6 @@ function applyFilledStylePatch(fn, mem, annotPtr, patch) {
5039
5247
  applyGeometryStylePatch(fn, mem, annotPtr, patch);
5040
5248
  }
5041
5249
  //#endregion
5042
- //#region src/features/annotations/internal/write/writeEmbedMetadata.ts
5043
- /**
5044
- * Write the EmbedPDF-namespaced /EMBD_Metadata annotation dictionary.
5045
- *
5046
- * This module owns ONLY the vendor-extension dictionary. Standard PDF
5047
- * base fields (/T, /M, /NM, /Contents) are handled by
5048
- * `writers/annotations/base.ts` — /EMBD_Metadata is a separate
5049
- * dictionary nested under the annotation that carries cloud-side
5050
- * identity bookkeeping which has no PDF-spec equivalent.
5051
- *
5052
- * Schema (v1):
5053
- * /EMBD_Metadata <<
5054
- * /SchemaVersion 1 % EmbedPDF metadata schema version
5055
- * /UserID (acme/alice) % identity that created the annotation
5056
- * /GroupID (engineering) % group the annotation belongs to
5057
- * /CreatedBy (acme/alice) % same as UserID on create
5058
- * /UpdatedBy (acme/alice) % last editor; refreshed on every update
5059
- * >>
5060
- *
5061
- * Read side: see `readers/annotations/embed-metadata.ts` (commit 12).
5062
- *
5063
- * PDFium bindings used: the `EPDFAnnot_*EmbedMetadata*` extension API
5064
- * shipped in the EmbedPDF PDFium fork. Values are written as UTF-16LE
5065
- * strings via the same `mem.writeU16String` pattern the base writer
5066
- * uses for /T.
5067
- */
5068
- const KEY_SCHEMA_VERSION = "SchemaVersion";
5069
- const KEY_USER_ID = "UserID";
5070
- const KEY_GROUP_ID = "GroupID";
5071
- const KEY_CREATED_BY = "CreatedBy";
5072
- const KEY_UPDATED_BY = "UpdatedBy";
5073
- /**
5074
- * Stamp /EMBD_Metadata on a freshly created annotation. No-op if the
5075
- * actor has no identity fields — the worker still emits the standard
5076
- * /M timestamp via the base writer; only the vendor dictionary is
5077
- * skipped.
5078
- *
5079
- * actor.userId → /UserID, /CreatedBy, /UpdatedBy
5080
- * actor.groupId → /GroupID
5081
- *
5082
- * /SchemaVersion is written whenever any field is written.
5083
- */
5084
- function applyEmbedMetadataOnCreate(fn, mem, annotPtr, actor) {
5085
- if (!actor || !actor.userId && !actor.groupId) return;
5086
- fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
5087
- if (actor.userId) {
5088
- setMetadataString(fn, mem, annotPtr, KEY_USER_ID, actor.userId);
5089
- setMetadataString(fn, mem, annotPtr, KEY_CREATED_BY, actor.userId);
5090
- setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
5091
- }
5092
- if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
5093
- }
5094
- /**
5095
- * Apply /EMBD_Metadata refreshes on update. Two fields can change on
5096
- * update; everything else (UserID, CreatedBy) is immutable.
5097
- *
5098
- * actor.userId → /UpdatedBy (refreshed every update where the
5099
- * caller has an identity)
5100
- * actor.groupId → /GroupID (reassigned only when the patch
5101
- * explicitly contains a new groupId; route runs
5102
- * `checkSetGroup` first)
5103
- *
5104
- * If the target annotation has no EMBD_Metadata yet (created before
5105
- * the cloud started stamping, or by a non-cloud writer), we seed
5106
- * /SchemaVersion so subsequent readers can find the marker.
5107
- *
5108
- * No-op when the actor has neither userId nor groupId.
5109
- */
5110
- function applyEmbedMetadataOnUpdate(fn, mem, annotPtr, actor) {
5111
- if (!actor || !actor.userId && !actor.groupId) return;
5112
- if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
5113
- if (actor.userId) setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
5114
- if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
5115
- }
5116
- function setMetadataString(fn, mem, annotPtr, key, value) {
5117
- const ptr = mem.writeU16String(value);
5118
- try {
5119
- fn.EPDFAnnot_SetEmbedMetadataString(annotPtr, key, ptr);
5120
- } finally {
5121
- mem.free(ptr);
5122
- }
5123
- }
5124
- //#endregion
5125
- //#region src/features/annotations/internal/write/writeAnnotationTransformMetadata.ts
5126
- /**
5127
- * Write the EmbedPDF transform keys under /EMBD_Metadata. This is the seam
5128
- * PDFium's native AP generator reads to bake a ROTATED appearance:
5129
- *
5130
- * /EMBD_Metadata <<
5131
- * /Rotation 45 % degrees, PDF convention (CCW)
5132
- * /UnrotatedRect [x0 y0 x1 y1] % BOX kinds only — the logical box
5133
- * >>
5134
- *
5135
- * The family split (see the v3 plan):
5136
- * - BOX kinds (square/circle/free-text): `/Rotation` + `/UnrotatedRect`.
5137
- * With BOTH present the AP generator emits an `/AP /Matrix` that rotates a
5138
- * box-sized appearance about the box centre, and `/Rect` is the enclosing
5139
- * AABB. This is the portable, externally-correct rotation.
5140
- * - VERTEX kinds (line/polyline/polygon/ink): `/Rotation` ONLY (advisory).
5141
- * The points are already rotated (they are the visual), so a lone
5142
- * `/Rotation` is INERT for the AP generator (it ignores `/Rotation` with no
5143
- * `/UnrotatedRect`) — it just records the applied angle so EmbedPDF can
5144
- * reconstruct an oriented selection box + offer reset.
5145
- *
5146
- * Both fields follow the engine-wide tri-state law ("a patch touches what it
5147
- * states, preserves what it omits"):
5148
- * - `undefined` → the key is UNTOUCHED (a rect-only move keeps its rotation).
5149
- * - `null` or `0` → CLEAR just that key via `EPDFAnnot_ClearEmbedMetadataKey`
5150
- * (0 ≡ no rotation is the canonical identity, not a sentinel; never clear
5151
- * the whole dict — identity fields UserID/GroupID/CreatedBy/UpdatedBy must
5152
- * survive).
5153
- * - a value → SET the key. Setting a nonzero rotation with no
5154
- * `/UnrotatedRect` (neither in the patch nor already on the annotation)
5155
- * is an unsatisfiable state for the AP generator and throws `InvalidArg`.
5156
- *
5157
- * MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
5158
- * `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
5159
- */
5160
- const KEY_ROTATION = "Rotation";
5161
- const KEY_UNROTATED_RECT = "UnrotatedRect";
5162
- /** Seed `/SchemaVersion` when we are about to create the dict by writing the
5163
- * first transform key, so the marker readers look for is always present. */
5164
- function ensureSchemaVersion(fn, annotPtr) {
5165
- if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
5166
- }
5167
- function setRotation(fn, annotPtr, rotation) {
5168
- ensureSchemaVersion(fn, annotPtr);
5169
- fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
5170
- }
5171
- function setUnrotatedRect(fn, mem, annotPtr, rect) {
5172
- ensureSchemaVersion(fn, annotPtr);
5173
- const buf = mem.alloc(16);
5174
- try {
5175
- mem.poke(buf, "f32", rect.left, 0);
5176
- mem.poke(buf, "f32", rect.top, 4);
5177
- mem.poke(buf, "f32", rect.right, 8);
5178
- mem.poke(buf, "f32", rect.bottom, 12);
5179
- fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
5180
- } finally {
5181
- mem.free(buf);
5182
- }
5183
- }
5184
- /**
5185
- * Write transform metadata for a BOX kind (square/circle/free-text/stamp),
5186
- * tri-state per field: `undefined` never touches the document (safe to call on
5187
- * every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
5188
- * unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
5189
- * generator needs the pair — and throws instead of writing a broken state.
5190
- */
5191
- function writeBoxTransformMetadata(fn, mem, annotPtr, t) {
5192
- if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
5193
- if (!(t.unrotatedRect != null || t.unrotatedRect === void 0 && readAnnotationUnrotatedRect(fn, mem, annotPtr) != null)) throw new EngineError(EngineErrorCode.InvalidArg, "rotation requires unrotatedRect: supply the pair together (or rely on an existing /EMBD_Metadata/UnrotatedRect)");
5194
- setRotation(fn, annotPtr, t.rotation);
5195
- } else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5196
- if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
5197
- else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
5198
- }
5199
- /**
5200
- * Write the advisory `/Rotation` scalar for a VERTEX kind
5201
- * (line/polyline/polygon/ink), tri-state: `undefined` never touches the
5202
- * document, `null`/`0` clears, a value sets. Whenever it does write, any
5203
- * `/UnrotatedRect` is defensively cleared — that key is an impossible state on
5204
- * a vertex kind and must never accidentally drive the AP generator.
5205
- */
5206
- function writeVertexTransformMetadata(fn, annotPtr, t) {
5207
- if (t.rotation === void 0) return;
5208
- if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
5209
- else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5210
- fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
5211
- }
5212
- //#endregion
5213
5250
  //#region src/features/annotations/internal/write/writeFreeTextAnnotation.ts
5214
5251
  /**
5215
5252
  * Default `/DA` colour for free text: black (border + default text). Unlike
@@ -6970,17 +7007,35 @@ var RedactionApplier = class {
6970
7007
  }
6971
7008
  applyAllOnPage(pagePtr) {
6972
7009
  const { fn, mem } = this.runtime;
7010
+ const hasRedactions = this.pageHasRedactAnnotations(pagePtr);
6973
7011
  return withScratch(mem, 4, (countPtr) => {
6974
- if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr)) return {
6975
- status: "unchanged",
6976
- removed: 0
6977
- };
7012
+ if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr)) {
7013
+ if (hasRedactions) throw new EngineError(EngineErrorCode.Unknown, "native page redaction apply failed");
7014
+ return {
7015
+ status: "unchanged",
7016
+ removed: 0
7017
+ };
7018
+ }
6978
7019
  return {
6979
7020
  status: "applied",
6980
7021
  removed: readI32(mem, countPtr) >>> 0
6981
7022
  };
6982
7023
  });
6983
7024
  }
7025
+ pageHasRedactAnnotations(pagePtr) {
7026
+ const { fn } = this.runtime;
7027
+ const count = fn.FPDFPage_GetAnnotCount(pagePtr);
7028
+ for (let index = 0; index < count; index++) {
7029
+ const annotPtr = fn.FPDFPage_GetAnnot(pagePtr, index);
7030
+ if (!annotPtr) continue;
7031
+ try {
7032
+ if (subtypeFromCode(fn.FPDFAnnot_GetSubtype(annotPtr)) === "redact") return true;
7033
+ } finally {
7034
+ fn.FPDFPage_CloseAnnot(annotPtr);
7035
+ }
7036
+ }
7037
+ return false;
7038
+ }
6984
7039
  applyRefsOnPage(pagePtr, refs) {
6985
7040
  const { fn, mem } = this.runtime;
6986
7041
  let removed = 0;
@@ -7006,23 +7061,26 @@ var RedactionApplier = class {
7006
7061
  //#region src/features/text/PageTextReader.ts
7007
7062
  /**
7008
7063
  * Per-page slow-path text reader. Acquires a `pagePtr` from the
7009
- * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`),
7010
- * extracts the full UTF-16 buffer with `FPDFText_GetText`, decodes,
7011
- * and releases everything in reverse order.
7064
+ * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
7065
+ * the page text, and releases everything in reverse order.
7012
7066
  *
7013
- * Memory model:
7014
- * - `count` is in UTF-16 code units (FPDFText's "char").
7015
- * - The buffer needs `(count + 1) * 2` bytes (PDFium writes a
7016
- * trailing NUL terminator).
7017
- * - `mem.readU16String` walks the buffer up to the NUL and decodes
7018
- * to a JS string, faithfully preserving surrogate pairs.
7067
+ * Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
7068
+ * CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
7069
+ * `text` is the TEXT projection. The two diverge when a character
7070
+ * contributes zero text units (non-printing) or two (supplementary plane);
7071
+ * `charMap` anchors encode exactly those deviations.
7019
7072
  *
7020
- * Errors:
7021
- * - `EPDFDoc_LoadPageByObjectNumber` returning 0 is `NotFound`
7022
- * (raised by `PagePtrPool.acquire`).
7023
- * - `FPDFText_LoadPage` returning 0 is `RuntimeUnavailable` — should
7024
- * not happen on a healthy page; treat it as a runtime bug rather
7025
- * than user error.
7073
+ * Both come from the fork's batched extension calls, which share one
7074
+ * encoding authority (`FX_UTF16Encode`) so text and map can never
7075
+ * disagree:
7076
+ *
7077
+ * - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
7078
+ * characters arrive as surrogate pairs instead of being silently
7079
+ * dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
7080
+ * upper-bound buffer (2 units per character + NUL), so no measure
7081
+ * round-trip.
7082
+ * - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
7083
+ * anchors is the common (identity) page and skips the fill call.
7026
7084
  */
7027
7085
  var PageTextReader = class {
7028
7086
  constructor(runtime, session) {
@@ -7031,7 +7089,7 @@ var PageTextReader = class {
7031
7089
  }
7032
7090
  read(pageObjectNumber, signal) {
7033
7091
  throwIfAborted(signal);
7034
- const { fn, mem } = this.runtime;
7092
+ const { fn } = this.runtime;
7035
7093
  const pool = this.session.pagePool();
7036
7094
  const pagePtr = pool.acquire(pageObjectNumber);
7037
7095
  try {
@@ -7040,10 +7098,18 @@ var PageTextReader = class {
7040
7098
  if (!textPagePtr) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `FPDFText_LoadPage returned null for page object ${pageObjectNumber}`);
7041
7099
  try {
7042
7100
  throwIfAborted(signal);
7043
- const charCount = fn.FPDFText_CountChars(textPagePtr);
7044
- return {
7045
- text: readPageText(fn, mem, textPagePtr, charCount),
7046
- charCount: Math.max(charCount, 0)
7101
+ const charCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
7102
+ const text = readTextFull(this.runtime, textPagePtr, charCount);
7103
+ const charMap = readCharMap(this.runtime, textPagePtr, pageObjectNumber);
7104
+ const violation = charMapViolation(charCount, text.length, charMap);
7105
+ if (violation !== null) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `char map invariant violated for page object ${pageObjectNumber}: ${violation}`);
7106
+ return charMap.length > 0 ? {
7107
+ text,
7108
+ charCount,
7109
+ charMap
7110
+ } : {
7111
+ text,
7112
+ charCount
7047
7113
  };
7048
7114
  } finally {
7049
7115
  fn.FPDFText_ClosePage(textPagePtr);
@@ -7053,21 +7119,65 @@ var PageTextReader = class {
7053
7119
  }
7054
7120
  }
7055
7121
  };
7056
- function readPageText(fn, mem, textPagePtr, charCount) {
7122
+ /**
7123
+ * Full-fidelity page text in one call: the buffer is sized at the maximum
7124
+ * possible expansion (every character a surrogate pair, plus the NUL), so
7125
+ * the returned requirement always fits and the measure round-trip is
7126
+ * skipped. The return value counts UTF-16 units INCLUDING the terminator.
7127
+ */
7128
+ function readTextFull(runtime, textPagePtr, charCount) {
7057
7129
  if (charCount <= 0) return "";
7058
- return withScratch(mem, (charCount + 1) * 2, (buf) => {
7059
- if (fn.FPDFText_GetText(textPagePtr, 0, charCount, buf) <= 0) return "";
7060
- return mem.readU16String(buf);
7130
+ const { fn, mem } = runtime;
7131
+ const bufUnits = charCount * 2 + 1;
7132
+ return withScratch(mem, bufUnits * 2, (buf) => {
7133
+ const written = fn.EPDFText_GetTextFull(textPagePtr, buf, bufUnits);
7134
+ if (written <= 1) return "";
7135
+ return decodeUtf16Le(mem.readBytes(buf, (written - 1) * 2));
7136
+ });
7137
+ }
7138
+ function decodeUtf16Le(bytes) {
7139
+ const units = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 1);
7140
+ let out = "";
7141
+ const CHUNK = 8192;
7142
+ for (let i = 0; i < units.length; i += CHUNK) out += String.fromCharCode(...units.subarray(i, Math.min(i + CHUNK, units.length)));
7143
+ return out;
7144
+ }
7145
+ /** Anchor struct layout: two little-endian i32s (see EPDF_CHAR_MAP_ANCHOR). */
7146
+ const ANCHOR_BYTES = 8;
7147
+ function readCharMap(runtime, textPagePtr, pageObjectNumber) {
7148
+ const { fn, mem } = runtime;
7149
+ const count = fn.EPDFText_GetCharToTextMap(textPagePtr, NULL_PTR, 0);
7150
+ if (count < 0) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `EPDFText_GetCharToTextMap failed for page object ${pageObjectNumber}`);
7151
+ if (count === 0) return [];
7152
+ return withScratch(mem, count * ANCHOR_BYTES, (buf) => {
7153
+ fn.EPDFText_GetCharToTextMap(textPagePtr, buf, count);
7154
+ const bytes = mem.readBytes(buf, count * ANCHOR_BYTES);
7155
+ const words = new Int32Array(bytes.buffer, bytes.byteOffset, count * 2);
7156
+ const anchors = new Array(count);
7157
+ for (let i = 0; i < count; i++) anchors[i] = [words[i * 2], words[i * 2 + 1]];
7158
+ return anchors;
7061
7159
  });
7062
7160
  }
7063
7161
  //#endregion
7064
7162
  //#region src/features/geometry/PageGeometryReader.ts
7065
7163
  /**
7164
+ * Mid-object orientation drift that forces a run split (~0.5°). Real content
7165
+ * keeps one char matrix per text object; this guards the exotic cases so a
7166
+ * run's `rotation` is always representative of every glyph in it.
7167
+ */
7168
+ const ROTATION_SPLIT_TOLERANCE = .0087;
7169
+ /**
7066
7170
  * Geometry-only text layout reader.
7067
7171
  *
7068
7172
  * Emits geometry in PDF user space (y-up edges) — the canonical engine
7069
7173
  * geometry. The viewer converts to content/view space via the page geometry
7070
7174
  * matrix; this reader applies NO Y-flip or device transform.
7175
+ *
7176
+ * One `EPDFText_GetCharGeometry` call per glyph supplies boxes, oriented
7177
+ * cells, and flags together; `buildRunsFromRawGlyphs` then groups glyphs
7178
+ * into the upright/rotated run union. Upright runs are byte-identical to the
7179
+ * pre-orientation reader (quads the native layer offers are deliberately
7180
+ * dropped — the dominant case never pays for orientation).
7071
7181
  */
7072
7182
  var PageGeometryReader = class {
7073
7183
  constructor(runtime, session) {
@@ -7076,7 +7186,7 @@ var PageGeometryReader = class {
7076
7186
  }
7077
7187
  read(pageObjectNumber, signal) {
7078
7188
  throwIfAborted(signal);
7079
- const { fn } = this.runtime;
7189
+ const { fn, mem } = this.runtime;
7080
7190
  const pool = this.session.pagePool();
7081
7191
  const pagePtr = pool.acquire(pageObjectNumber);
7082
7192
  try {
@@ -7086,12 +7196,27 @@ var PageGeometryReader = class {
7086
7196
  try {
7087
7197
  throwIfAborted(signal);
7088
7198
  const glyphCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
7089
- const glyphs = [];
7090
- for (let i = 0; i < glyphCount; i++) {
7091
- throwIfAborted(signal);
7092
- glyphs.push(this.readGlyphInfo(textPagePtr, i));
7199
+ const records = [];
7200
+ const geometryPtr = mem.alloc(EPDF_CHAR_GEOMETRY_LAYOUT.bytes);
7201
+ try {
7202
+ let prevObjectKey = null;
7203
+ for (let i = 0; i < glyphCount; i++) {
7204
+ throwIfAborted(signal);
7205
+ const objectKey = fn.FPDFText_GetTextObject(textPagePtr, i);
7206
+ const record = {
7207
+ objectKey,
7208
+ ...this.readGlyphRaw(textPagePtr, i, geometryPtr)
7209
+ };
7210
+ if (objectKey !== prevObjectKey) {
7211
+ record.fontSize = fn.FPDFText_GetFontSize(textPagePtr, i);
7212
+ prevObjectKey = objectKey;
7213
+ }
7214
+ records.push(record);
7215
+ }
7216
+ } finally {
7217
+ mem.free(geometryPtr);
7093
7218
  }
7094
- return { runs: this.buildRunsFromGlyphs(glyphs, textPagePtr) };
7219
+ return { runs: buildRunsFromRawGlyphs(records) };
7095
7220
  } finally {
7096
7221
  fn.FPDFText_ClosePage(textPagePtr);
7097
7222
  }
@@ -7099,96 +7224,206 @@ var PageGeometryReader = class {
7099
7224
  pool.release(pageObjectNumber);
7100
7225
  }
7101
7226
  }
7102
- buildRunsFromGlyphs(glyphs, textPagePtr) {
7103
- const { fn } = this.runtime;
7104
- const runs = [];
7105
- let current = null;
7106
- let curObjPtr = null;
7107
- let bounds = null;
7108
- for (let i = 0; i < glyphs.length; i++) {
7109
- const glyph = glyphs[i];
7110
- const objPtr = fn.FPDFText_GetTextObject(textPagePtr, i);
7111
- if (objPtr !== curObjPtr) {
7112
- curObjPtr = objPtr;
7113
- current = {
7114
- rect: { ...glyph.looseBox },
7115
- charStart: i,
7116
- glyphs: [],
7117
- fontSize: fn.FPDFText_GetFontSize(textPagePtr, i)
7118
- };
7119
- bounds = { ...glyph.looseBox };
7120
- runs.push(current);
7227
+ readGlyphRaw(textPagePtr, charIndex, geometryPtr) {
7228
+ const { fn, mem } = this.runtime;
7229
+ if (!fn.EPDFText_GetCharGeometry(textPagePtr, charIndex, geometryPtr)) return emptyRawGlyph();
7230
+ const { offsets, flags: bits } = EPDF_CHAR_GEOMETRY_LAYOUT;
7231
+ const native = readI32(mem, geometryPtr, offsets.flags) >>> 0;
7232
+ if (native & bits.empty) return emptyRawGlyph();
7233
+ const upright = Boolean(native & bits.upright);
7234
+ const raw = {
7235
+ flags: native & bits.space ? 1 : 0,
7236
+ looseBox: normalizePdfRect(readRectF(mem, geometryPtr, offsets.looseBox)),
7237
+ upright,
7238
+ rotation: 0,
7239
+ ascentFlip: false
7240
+ };
7241
+ if (native & bits.hasTightBox) raw.tightBox = normalizePdfRect(readRectF(mem, geometryPtr, offsets.tightBox));
7242
+ if (native & bits.hasLooseQuad) {
7243
+ raw.looseQuad = readQuad(mem, geometryPtr, offsets.looseQuad);
7244
+ if (native & bits.hasTightQuad) raw.tightQuad = readQuad(mem, geometryPtr, offsets.tightQuad);
7245
+ if (!upright) {
7246
+ const a = readF32(mem, geometryPtr, offsets.matrix);
7247
+ const b = readF32(mem, geometryPtr, offsets.matrix + 4);
7248
+ const c = readF32(mem, geometryPtr, offsets.matrix + 8);
7249
+ const d = readF32(mem, geometryPtr, offsets.matrix + 12);
7250
+ raw.rotation = Math.atan2(b, a);
7251
+ raw.ascentFlip = a * d - b * c < 0;
7121
7252
  }
7122
- current.glyphs.push(toSlimGlyph(glyph));
7123
- if (glyph.isEmpty) continue;
7124
- bounds.left = Math.min(bounds.left, glyph.looseBox.left);
7125
- bounds.bottom = Math.min(bounds.bottom, glyph.looseBox.bottom);
7126
- bounds.right = Math.max(bounds.right, glyph.looseBox.right);
7127
- bounds.top = Math.max(bounds.top, glyph.looseBox.top);
7128
- current.rect = { ...bounds };
7129
7253
  }
7130
- return runs;
7254
+ return raw;
7131
7255
  }
7132
- readGlyphInfo(textPagePtr, charIndex) {
7133
- const { fn, mem } = this.runtime;
7134
- const rectPtr = mem.alloc(16);
7135
- const tLeftPtr = mem.alloc(8);
7136
- const tRightPtr = mem.alloc(8);
7137
- const tBottomPtr = mem.alloc(8);
7138
- const tTopPtr = mem.alloc(8);
7139
- const ptrs = [
7140
- rectPtr,
7141
- tLeftPtr,
7142
- tRightPtr,
7143
- tBottomPtr,
7144
- tTopPtr
7145
- ];
7146
- try {
7147
- if (!fn.FPDFText_GetLooseCharBox(textPagePtr, charIndex, rectPtr)) return emptyGlyph();
7148
- const left = Number(mem.peek(rectPtr, "f32", 0));
7149
- const top = Number(mem.peek(rectPtr, "f32", 4));
7150
- const right = Number(mem.peek(rectPtr, "f32", 8));
7151
- const bottom = Number(mem.peek(rectPtr, "f32", 12));
7152
- if (left === right || top === bottom) return emptyGlyph();
7153
- const glyph = { looseBox: normalizePdfRect({
7154
- left,
7155
- bottom,
7156
- right,
7157
- top
7158
- }) };
7159
- if (fn.FPDFText_GetCharBox(textPagePtr, charIndex, tLeftPtr, tRightPtr, tBottomPtr, tTopPtr)) {
7160
- const tLeft = Number(mem.peek(tLeftPtr, "f64"));
7161
- const tRight = Number(mem.peek(tRightPtr, "f64"));
7162
- glyph.tightBox = normalizePdfRect({
7163
- left: tLeft,
7164
- bottom: Number(mem.peek(tBottomPtr, "f64")),
7165
- right: tRight,
7166
- top: Number(mem.peek(tTopPtr, "f64"))
7167
- });
7168
- }
7169
- glyph.isSpace = fn.FPDFText_GetUnicode(textPagePtr, charIndex) === 32;
7170
- return glyph;
7171
- } finally {
7172
- for (const ptr of ptrs) mem.free(ptr);
7256
+ };
7257
+ /**
7258
+ * Group per-glyph records into the run union. Pure — exported for tests.
7259
+ *
7260
+ * Runs split on text-object change (the legacy rule) and on orientation
7261
+ * change between classifiable glyphs (θ drift / mixed orientation). A run's
7262
+ * variant is decided by its FIRST classifiable glyph:
7263
+ * - empty glyphs never classify (they adopt the run's variant);
7264
+ * - real glyphs WITHOUT an oriented cell (singular matrix, synthesized
7265
+ * /ActualText pieces) classify as upright — box-only data degrades to
7266
+ * exactly the legacy behavior;
7267
+ * - real glyphs with a non-upright matrix classify as rotated.
7268
+ * Runs that never see a classifiable glyph emit as upright (degenerate,
7269
+ * legacy behavior).
7270
+ */
7271
+ function buildRunsFromRawGlyphs(records) {
7272
+ const runs = [];
7273
+ let buffer = [];
7274
+ let charStart = 0;
7275
+ let fontSize;
7276
+ let objectKey = null;
7277
+ let cls;
7278
+ const flush = (nextStart) => {
7279
+ if (buffer.length > 0) {
7280
+ runs.push(materializeRun(buffer, charStart, fontSize, cls));
7281
+ buffer = [];
7282
+ cls = void 0;
7283
+ }
7284
+ charStart = nextStart;
7285
+ };
7286
+ for (let i = 0; i < records.length; i++) {
7287
+ const record = records[i];
7288
+ if (record.objectKey !== objectKey) {
7289
+ flush(i);
7290
+ objectKey = record.objectKey;
7291
+ fontSize = record.fontSize;
7173
7292
  }
7293
+ const recordClass = classOf(record);
7294
+ if (recordClass) {
7295
+ if (cls && !sameClass(cls, recordClass)) flush(i);
7296
+ if (!cls) cls = recordClass;
7297
+ }
7298
+ buffer.push(record);
7174
7299
  }
7300
+ flush(records.length);
7301
+ return runs;
7302
+ }
7303
+ function classOf(record) {
7304
+ if (record.flags & 2) return void 0;
7305
+ if (record.looseQuad && !record.upright) return {
7306
+ upright: false,
7307
+ rotation: record.rotation,
7308
+ ascentFlip: record.ascentFlip
7309
+ };
7310
+ return { upright: true };
7311
+ }
7312
+ function sameClass(a, b) {
7313
+ if (a.upright || b.upright) return a.upright === b.upright;
7314
+ if (a.ascentFlip !== b.ascentFlip) return false;
7315
+ const delta = a.rotation - b.rotation;
7316
+ return Math.abs(Math.atan2(Math.sin(delta), Math.cos(delta))) <= ROTATION_SPLIT_TOLERANCE;
7317
+ }
7318
+ function materializeRun(buffer, charStart, fontSize, cls) {
7319
+ if (cls && !cls.upright) {
7320
+ const glyphs = buffer.map((g) => g.looseQuad ? {
7321
+ looseQuad: g.looseQuad,
7322
+ flags: g.flags,
7323
+ ...g.tightQuad ? { tightQuad: g.tightQuad } : {}
7324
+ } : {
7325
+ looseQuad: zeroQuad(),
7326
+ flags: g.flags
7327
+ });
7328
+ return {
7329
+ rect: runBounds(buffer, (g) => g.looseQuad ? pdfQuadBounds(g.looseQuad) : ZERO_RECT),
7330
+ charStart,
7331
+ glyphs,
7332
+ rotation: cls.rotation,
7333
+ ascentFlip: cls.ascentFlip,
7334
+ ...fontSize !== void 0 ? { fontSize } : {}
7335
+ };
7336
+ }
7337
+ const glyphs = buffer.map((g) => ({
7338
+ looseBox: g.looseBox,
7339
+ flags: g.flags,
7340
+ ...g.tightBox ? { tightBox: g.tightBox } : {}
7341
+ }));
7342
+ return {
7343
+ rect: runBounds(buffer, (g) => g.looseBox),
7344
+ charStart,
7345
+ glyphs,
7346
+ ...fontSize !== void 0 ? { fontSize } : {}
7347
+ };
7348
+ }
7349
+ /** Legacy run-rect algorithm: seed from the first glyph, expand on non-empty. */
7350
+ function runBounds(buffer, boundsOf) {
7351
+ const seed = boundsOf(buffer[0]);
7352
+ const rect = {
7353
+ left: seed.left,
7354
+ bottom: seed.bottom,
7355
+ right: seed.right,
7356
+ top: seed.top
7357
+ };
7358
+ for (const g of buffer) {
7359
+ if (g.flags & 2) continue;
7360
+ const b = boundsOf(g);
7361
+ rect.left = Math.min(rect.left, b.left);
7362
+ rect.bottom = Math.min(rect.bottom, b.bottom);
7363
+ rect.right = Math.max(rect.right, b.right);
7364
+ rect.top = Math.max(rect.top, b.top);
7365
+ }
7366
+ return rect;
7367
+ }
7368
+ const ZERO_RECT = {
7369
+ left: 0,
7370
+ bottom: 0,
7371
+ right: 0,
7372
+ top: 0
7175
7373
  };
7176
- function emptyGlyph() {
7374
+ function zeroQuad() {
7177
7375
  return {
7376
+ p1: {
7377
+ x: 0,
7378
+ y: 0
7379
+ },
7380
+ p2: {
7381
+ x: 0,
7382
+ y: 0
7383
+ },
7384
+ p3: {
7385
+ x: 0,
7386
+ y: 0
7387
+ },
7388
+ p4: {
7389
+ x: 0,
7390
+ y: 0
7391
+ }
7392
+ };
7393
+ }
7394
+ function emptyRawGlyph() {
7395
+ return {
7396
+ flags: 2,
7178
7397
  looseBox: {
7179
7398
  left: 0,
7180
7399
  bottom: 0,
7181
7400
  right: 0,
7182
7401
  top: 0
7183
7402
  },
7184
- isEmpty: true
7403
+ upright: true,
7404
+ rotation: 0,
7405
+ ascentFlip: false
7185
7406
  };
7186
7407
  }
7187
- function toSlimGlyph(glyph) {
7408
+ function readQuad(mem, ptr, byteOffset) {
7409
+ const f = (offset) => readF32(mem, ptr, byteOffset + offset);
7188
7410
  return {
7189
- looseBox: glyph.looseBox,
7190
- flags: glyph.isEmpty ? 2 : glyph.isSpace ? 1 : 0,
7191
- ...glyph.tightBox ? { tightBox: glyph.tightBox } : {}
7411
+ p1: {
7412
+ x: f(0),
7413
+ y: f(4)
7414
+ },
7415
+ p2: {
7416
+ x: f(8),
7417
+ y: f(12)
7418
+ },
7419
+ p3: {
7420
+ x: f(16),
7421
+ y: f(20)
7422
+ },
7423
+ p4: {
7424
+ x: f(24),
7425
+ y: f(28)
7426
+ }
7192
7427
  };
7193
7428
  }
7194
7429
  //#endregion
@@ -8561,7 +8796,7 @@ function emptyResult(index, status) {
8561
8796
  //#endregion
8562
8797
  //#region src/features/search/internal/pageCorpusCache.ts
8563
8798
  /**
8564
- * Per-session, per-page search corpus: the page's extracted text plus its
8799
+ * Per-session, per-page search corpus: the page's text snapshot plus its
8565
8800
  * DEFAULT fold (the one literal queries with default options search).
8566
8801
  * This is the local engine's in-memory equivalent of the server's corpus
8567
8802
  * artifacts — same fold version, same shape, built lazily on first search
@@ -8586,19 +8821,20 @@ function acquirePageCorpus(runtime, session, pageObjectNumber, signal) {
8586
8821
  cache.set(session, pages);
8587
8822
  }
8588
8823
  const hit = pages.get(pageObjectNumber);
8589
- if (hit && hit.seq === seq) return hit.folded;
8590
- const text = new PageTextReader(runtime, session).read(pageObjectNumber, signal).text;
8591
- const folded = foldText(text);
8592
- pages.delete(pageObjectNumber);
8593
- pages.set(pageObjectNumber, {
8824
+ if (hit && hit.seq === seq) return hit;
8825
+ const snapshot = new PageTextReader(runtime, session).read(pageObjectNumber, signal);
8826
+ const entry = {
8594
8827
  seq,
8595
- folded
8596
- });
8828
+ folded: foldText(snapshot.text),
8829
+ snapshot
8830
+ };
8831
+ pages.delete(pageObjectNumber);
8832
+ pages.set(pageObjectNumber, entry);
8597
8833
  if (pages.size > MAX_CACHED_PAGES) {
8598
8834
  const oldest = pages.keys().next().value;
8599
8835
  if (oldest !== void 0) pages.delete(oldest);
8600
8836
  }
8601
- return folded;
8837
+ return entry;
8602
8838
  }
8603
8839
  //#endregion
8604
8840
  //#region src/features/search/internal/searchCursor.ts
@@ -8707,19 +8943,23 @@ var SearchReader = class {
8707
8943
  throwIfAborted(signal);
8708
8944
  const pon = order[scanned].pageObjectNumber;
8709
8945
  const corpus = acquirePageCorpus(this.runtime, this.session, pon, signal);
8946
+ const text = corpus.snapshot.text;
8710
8947
  let ranges;
8711
- if (query.regex) ranges = matchRegex(corpus.original, query);
8712
- else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(corpus.original, foldOptionsFor(query)), query);
8713
- else ranges = matchLiteral(corpus, query);
8948
+ if (query.regex) ranges = matchRegex(text, query);
8949
+ else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
8950
+ else ranges = matchLiteral(corpus.folded, query);
8714
8951
  if (ranges.length > 0) {
8715
- const geometry = new PageGeometryReader(this.runtime, this.session).read(pon, signal);
8716
- for (const range of ranges) matches.push({
8717
- pageObjectNumber: pon,
8718
- charStart: range.start,
8719
- charCount: range.length,
8720
- rects: searchRectsForRange(geometry, range.start, range.length),
8721
- ...mode === "full" ? { snippet: buildSnippet(corpus.original, range) } : {}
8722
- });
8952
+ const layout = buildPageTextLayout(new PageGeometryReader(this.runtime, this.session).read(pon, signal));
8953
+ for (const range of ranges) {
8954
+ const chars = charRangeForTextOffsets(corpus.snapshot, range.start, range.start + range.length);
8955
+ matches.push({
8956
+ pageObjectNumber: pon,
8957
+ charStart: chars.start,
8958
+ charCount: chars.end - chars.start,
8959
+ segments: textSegmentsForRange(layout, chars.start, chars.end - chars.start),
8960
+ ...mode === "full" ? { snippet: buildSnippet(text, range) } : {}
8961
+ });
8962
+ }
8723
8963
  }
8724
8964
  scanned++;
8725
8965
  pagesThisSlice++;
@@ -9846,6 +10086,6 @@ function passwordRequiredProbe() {
9846
10086
  };
9847
10087
  }
9848
10088
  //#endregion
9849
- export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CloseStack, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EventHub, FontRegistrar, FormMutator, FormReader, FormsEffectsApplier, LocalRevisionAuthority, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SessionEventPublisher, WorkerHost, computeMutationImpact, destroyLibrary, disposeFormModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, throwIfAborted, writeAttachmentFilePayload };
10089
+ export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CloseStack, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EPDF_CHAR_GEOMETRY_LAYOUT, EventHub, FontRegistrar, FormMutator, FormReader, FormsEffectsApplier, LocalRevisionAuthority, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SessionEventPublisher, WorkerHost, buildRunsFromRawGlyphs, computeMutationImpact, destroyLibrary, disposeFormModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, throwIfAborted, writeAttachmentFilePayload };
9850
10090
 
9851
10091
  //# sourceMappingURL=index.js.map