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