@embedpdf/engine-services 3.0.0-next.0 → 3.0.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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, PAGE_INSERT_BLANK_MAX_COUNT, 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();
@@ -225,8 +225,10 @@ var EventHub = class {
225
225
  };
226
226
  }
227
227
  publish(event) {
228
- const { serverId } = event.origin;
229
- if (serverId !== null) this.serverId = this.serverId === null ? serverId : Math.max(this.serverId, serverId);
228
+ if ("origin" in event) {
229
+ const { serverId } = event.origin;
230
+ if (serverId !== null) this.serverId = this.serverId === null ? serverId : Math.max(this.serverId, serverId);
231
+ }
230
232
  for (const listener of [...this.listeners]) try {
231
233
  listener(event);
232
234
  } catch (err) {
@@ -1163,13 +1165,34 @@ var MetadataMutator = class {
1163
1165
  };
1164
1166
  //#endregion
1165
1167
  //#region src/runtime/memory/structs.ts
1168
+ /** Generated from `EPDF_CHAR_GEOMETRY`; pinned by native static_asserts. */
1169
+ const EPDF_CHAR_GEOMETRY_LAYOUT = {
1170
+ bytes: 124,
1171
+ offsets: {
1172
+ looseBox: 0,
1173
+ tightBox: 16,
1174
+ looseQuad: 32,
1175
+ tightQuad: 64,
1176
+ matrix: 96,
1177
+ flags: 120
1178
+ },
1179
+ flags: {
1180
+ hasTightBox: 1,
1181
+ hasLooseQuad: 2,
1182
+ hasTightQuad: 4,
1183
+ upright: 8,
1184
+ space: 16,
1185
+ empty: 32,
1186
+ synthesized: 64
1187
+ }
1188
+ };
1166
1189
  /** Decode an `FS_RECTF` struct that a native call wrote into `ptr`. */
1167
- function readRectF(mem, ptr) {
1190
+ function readRectF(mem, ptr, byteOffset = 0) {
1168
1191
  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)
1192
+ left: readF32(mem, ptr, byteOffset),
1193
+ top: readF32(mem, ptr, byteOffset + 4),
1194
+ right: readF32(mem, ptr, byteOffset + 8),
1195
+ bottom: readF32(mem, ptr, byteOffset + 12)
1173
1196
  };
1174
1197
  }
1175
1198
  /** Decode an `FS_SIZEF` struct that a native call wrote into `ptr`. */
@@ -1705,6 +1728,42 @@ var PagesInserter = class {
1705
1728
  cache: null
1706
1729
  };
1707
1730
  }
1731
+ /**
1732
+ * Create `count` blank pages of `size` at `destIndex`. Same mutation
1733
+ * contract as `insert`, but native creation (`FPDFPage_New`) instead of a
1734
+ * deep copy: no source document, so none of the retain-until-close
1735
+ * lifetime hazard above. Each page gets an empty content stream
1736
+ * (`FPDFPage_GenerateContent`) so the saved PDF renders identically in
1737
+ * third-party viewers.
1738
+ */
1739
+ insertBlank(spec, destIndex, signal) {
1740
+ throwIfAborted(signal);
1741
+ const { size } = spec;
1742
+ const count = spec.count ?? 1;
1743
+ if (!Number.isFinite(size?.width) || !Number.isFinite(size?.height) || size.width <= 0 || size.height <= 0) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank size must have positive finite dimensions, got ${size?.width}x${size?.height}`);
1744
+ if (!Number.isInteger(count) || count < 1 || count > PAGE_INSERT_BLANK_MAX_COUNT) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank count ${count} out of range [1, ${PAGE_INSERT_BLANK_MAX_COUNT}]`);
1745
+ const { fn } = this.runtime;
1746
+ const destPtr = this.session.requireDocPtr();
1747
+ const beforeCount = fn.FPDF_GetPageCount(destPtr);
1748
+ const at = destIndex ?? beforeCount;
1749
+ if (!Number.isInteger(at) || at < 0 || at > beforeCount) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank destIndex ${at} out of range [0, ${beforeCount}]`);
1750
+ for (let i = 0; i < count; i++) {
1751
+ const pagePtr = fn.FPDFPage_New(destPtr, at + i, size.width, size.height);
1752
+ if (!pagePtr) {
1753
+ for (let j = 0; j < i; j++) fn.FPDFPage_Delete(destPtr, at);
1754
+ throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_New rejected page ${i + 1}/${count} at index ${at + i}`);
1755
+ }
1756
+ fn.FPDFPage_GenerateContent(pagePtr);
1757
+ fn.FPDF_ClosePage(pagePtr);
1758
+ }
1759
+ this.session.refreshPageRegistry();
1760
+ const layout = new PagesReader(this.runtime, this.session).read(signal);
1761
+ return {
1762
+ insertedPageObjectNumbers: layout.pages.slice(at, at + count).map((page) => page.pageObjectNumber),
1763
+ layout,
1764
+ cache: null
1765
+ };
1766
+ }
1708
1767
  };
1709
1768
  //#endregion
1710
1769
  //#region src/features/annotations/internal/read/annotationReadContext.ts
@@ -2153,6 +2212,49 @@ function readQuadPoints(fn, mem, annotPtr) {
2153
2212
  });
2154
2213
  }
2155
2214
  //#endregion
2215
+ //#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
2216
+ /**
2217
+ * Read-side twin of `writers/.../writeAnnotationTransformMetadata.ts`. Decodes
2218
+ * the EmbedPDF rotation keys from /EMBD_Metadata back onto the DTO:
2219
+ *
2220
+ * - `Rotation` — degrees, PDF convention. Returned for EVERY rotatable
2221
+ * kind (box AND vertex); the plugin converts it back to its CW-content
2222
+ * `rot` once at the repository seam.
2223
+ * - `UnrotatedRect` — the logical box. BOX kinds only (square/circle/
2224
+ * free-text); a vertex annotation never carries one (its points are the
2225
+ * visual), so vertex readers don't ask for it.
2226
+ *
2227
+ * Both are absent on un-rotated annotations (and on everything authored before
2228
+ * rotation shipped), so both reads return `undefined` and the DTO field stays
2229
+ * unset.
2230
+ */
2231
+ const KEY_ROTATION$1 = "Rotation";
2232
+ const KEY_UNROTATED_RECT$1 = "UnrotatedRect";
2233
+ /** The advisory/portable rotation scalar (degrees, PDF convention), or
2234
+ * `undefined` when absent or zero. */
2235
+ function readAnnotationRotation(fn, mem, annotPtr) {
2236
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
2237
+ return withScratch(mem, 4, (buf) => {
2238
+ if (!fn.EPDFAnnot_GetEmbedMetadataNumber(annotPtr, KEY_ROTATION$1, buf)) return void 0;
2239
+ const v = readF32(mem, buf);
2240
+ return v ? v : void 0;
2241
+ });
2242
+ }
2243
+ /** The logical (unrotated) box of a rotated BOX annotation, or `undefined`. */
2244
+ function readAnnotationUnrotatedRect(fn, mem, annotPtr) {
2245
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) return void 0;
2246
+ return withScratch(mem, 16, (buf) => {
2247
+ if (!fn.EPDFAnnot_GetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT$1, buf)) return void 0;
2248
+ const r = readRectF(mem, buf);
2249
+ return {
2250
+ left: r.left,
2251
+ bottom: r.bottom,
2252
+ right: r.right,
2253
+ top: r.top
2254
+ };
2255
+ });
2256
+ }
2257
+ //#endregion
2156
2258
  //#region src/features/annotations/internal/textEditIntent.ts
2157
2259
  /** Adobe-compatible `/IT` names, normalized at the engine-core boundary. */
2158
2260
  const IT_REPLACE = "Replace";
@@ -2187,13 +2289,17 @@ function readCaret(fn, mem, annotPtr, base) {
2187
2289
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
2188
2290
  const rd = readRectangleDifferences(fn, mem, annotPtr);
2189
2291
  const intent = caretIntentFromName(readIntent(fn, mem, annotPtr));
2292
+ const rotation = readAnnotationRotation(fn, mem, annotPtr);
2293
+ const unrotatedRect = readAnnotationUnrotatedRect(fn, mem, annotPtr);
2190
2294
  return {
2191
2295
  ...base,
2192
2296
  subtype: "caret",
2193
2297
  intent,
2194
2298
  color,
2195
2299
  opacity,
2196
- rectDifferences: rd
2300
+ rectDifferences: rd,
2301
+ ...rotation != null ? { rotation } : {},
2302
+ ...unrotatedRect ? { unrotatedRect } : {}
2197
2303
  };
2198
2304
  }
2199
2305
  //#endregion
@@ -2611,49 +2717,6 @@ function readFilledStyleExtras(fn, mem, annotPtr) {
2611
2717
  };
2612
2718
  }
2613
2719
  //#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
2720
  //#region src/features/annotations/internal/read/readFreeTextAnnotation.ts
2658
2721
  /** Default `/DA` colour (black) when an annotation has no default appearance. */
2659
2722
  const DEFAULT_FREETEXT_COLOR$1 = {
@@ -3106,6 +3169,47 @@ function readStampName(fn, mem, annotPtr) {
3106
3169
  return readAnnotString(fn, mem, annotPtr, "Name");
3107
3170
  }
3108
3171
  //#endregion
3172
+ //#region src/features/annotations/internal/annotationState.ts
3173
+ /**
3174
+ * `/State` + `/StateModel` spelling maps — ISO 32000 §12.5.6.3, Table 174.
3175
+ *
3176
+ * Known values are wire-stable lowercase (the house vocabulary — same
3177
+ * rationale as `AnnotationReplyType` / `AnnotationBorderStyle`); the PDF
3178
+ * stores the capitalized Table 174 spellings. Unknown values — custom
3179
+ * Acrobat state models and their states — pass through VERBATIM in both
3180
+ * directions, so foreign review workflows survive a round trip.
3181
+ */
3182
+ const STATE_MODEL_TO_PDF = {
3183
+ review: "Review",
3184
+ marked: "Marked"
3185
+ };
3186
+ const STATE_TO_PDF = {
3187
+ accepted: "Accepted",
3188
+ rejected: "Rejected",
3189
+ cancelled: "Cancelled",
3190
+ completed: "Completed",
3191
+ none: "None",
3192
+ marked: "Marked",
3193
+ unmarked: "Unmarked"
3194
+ };
3195
+ const PDF_TO_STATE_MODEL = invert(STATE_MODEL_TO_PDF);
3196
+ const PDF_TO_STATE = invert(STATE_TO_PDF);
3197
+ function stateModelToPdf(value) {
3198
+ return STATE_MODEL_TO_PDF[value] ?? value;
3199
+ }
3200
+ function stateModelFromPdf(value) {
3201
+ return PDF_TO_STATE_MODEL[value] ?? value;
3202
+ }
3203
+ function stateToPdf(value) {
3204
+ return STATE_TO_PDF[value] ?? value;
3205
+ }
3206
+ function stateFromPdf(value) {
3207
+ return PDF_TO_STATE[value] ?? value;
3208
+ }
3209
+ function invert(map) {
3210
+ return Object.fromEntries(Object.entries(map).map(([wire, pdf]) => [pdf, wire]));
3211
+ }
3212
+ //#endregion
3109
3213
  //#region src/features/annotations/internal/read/readTextAnnotation.ts
3110
3214
  /** Default `/C` — matches the generator's yellow note fill and the writer default. */
3111
3215
  const DEFAULT_NOTE_COLOR$1 = {
@@ -3120,12 +3224,16 @@ function readText(fn, mem, annotPtr, base) {
3120
3224
  const ca = readAnnotOpacity(fn, mem, annotPtr);
3121
3225
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
3122
3226
  const icon = NOTE_CODE_TO_ICON[fn.EPDFAnnot_GetName(annotPtr)] ?? DEFAULT_NOTE_ICON;
3227
+ const stateRaw = readAnnotString(fn, mem, annotPtr, "State");
3228
+ const stateModelRaw = readAnnotString(fn, mem, annotPtr, "StateModel");
3123
3229
  return {
3124
3230
  ...base,
3125
3231
  subtype: "text",
3126
3232
  icon,
3127
3233
  color,
3128
- opacity
3234
+ opacity,
3235
+ state: stateRaw === null ? null : stateFromPdf(stateRaw),
3236
+ stateModel: stateModelRaw === null ? null : stateModelFromPdf(stateModelRaw)
3129
3237
  };
3130
3238
  }
3131
3239
  //#endregion
@@ -3840,6 +3948,7 @@ function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision
3840
3948
  const rect = readAnnotRect(fn, mem, annotPtr);
3841
3949
  const flags = readAnnotFlags(fn, annotPtr);
3842
3950
  const contents = readAnnotString(fn, mem, annotPtr, "Contents");
3951
+ const subject = readAnnotString(fn, mem, annotPtr, "Subj");
3843
3952
  const author = readAnnotString(fn, mem, annotPtr, "T");
3844
3953
  const createdRaw = readAnnotString(fn, mem, annotPtr, "CreationDate");
3845
3954
  const modifiedRaw = readAnnotString(fn, mem, annotPtr, "M");
@@ -3856,6 +3965,7 @@ function readAnnotationBase(fn, mem, annotPtr, pageObjectNumber, index, revision
3856
3965
  flags,
3857
3966
  rect,
3858
3967
  contents,
3968
+ subject,
3859
3969
  author,
3860
3970
  created: createdRaw ? pdfDateToIso(createdRaw) : null,
3861
3971
  modified: modifiedRaw ? pdfDateToIso(modifiedRaw) : null,
@@ -4130,19 +4240,31 @@ const ANNOT_SUBTYPE_WIDGET = 20;
4130
4240
  /** `FPDF_ANNOT_FREETEXT` — free-text annotation subtype code. */
4131
4241
  const ANNOT_SUBTYPE_FREETEXT = 3;
4132
4242
  /**
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.
4243
+ * BOX-family subtypes (free-text 3, square 5, circle 6, stamp 13, caret 14):
4244
+ * the kinds whose v3 writers put rotation in the AP `/Matrix` +
4245
+ * `/EMBD_Metadata` `/UnrotatedRect`. Only these are eligible for
4246
+ * rotation-stripped appearance rendering — vertex kinds
4247
+ * (line/polyline/polygon/ink) pre-rotate their geometry, so their rasters must
4248
+ * stay on the classic path. This set MUST cover every kind whose reader
4249
+ * surfaces the rotation pair: `fromDTO` (plugin-annotation repository) mirrors
4250
+ * this exact condition to re-apply the stripped rotation as `apRot`.
4138
4251
  */
4139
4252
  const BOX_FAMILY_SUBTYPES = /* @__PURE__ */ new Set([
4140
4253
  3,
4141
4254
  5,
4142
4255
  6,
4143
- 13
4256
+ 13,
4257
+ 14
4144
4258
  ]);
4145
4259
  /**
4260
+ * Per-appearance output-pixel ceiling (see the clamp in `renderOne`).
4261
+ * 16 M px ≈ a 4000² raster ≈ 64 MB of transient RGBA — safely inside the
4262
+ * wasm heap for the one-at-a-time batch loop, while a page-sized annotation
4263
+ * stays crisp to roughly 5–6× before its appearance starts stretching.
4264
+ * Small annotations (the overwhelming majority) never come near it.
4265
+ */
4266
+ const APPEARANCE_PIXEL_CLAMP = 16e6;
4267
+ /**
4146
4268
  * Maps an `AnnotationAppearanceMode` onto the PDFium appearance-mode int and
4147
4269
  * the `EPDFAnnot_GetAvailableAppearanceModes` bit it occupies.
4148
4270
  * N -> mode 0 / bit 1, R -> mode 1 / bit 2, D -> mode 2 / bit 4.
@@ -4241,13 +4363,15 @@ var AnnotationAppearanceReader = class {
4241
4363
  fn.EPDFAnnot_GenerateFormFieldAP(annotPtr);
4242
4364
  if (!fn.EPDFAnnot_HasAppearanceStream(annotPtr, modeInt)) return null;
4243
4365
  } else return null;
4366
+ const rectArea = Math.max(1, (rect.right - rect.left) * (rect.top - rect.bottom));
4367
+ const effScale = Math.min(scale, Math.sqrt(APPEARANCE_PIXEL_CLAMP / rectArea));
4244
4368
  return rasterize(this.runtime, {
4245
4369
  rect,
4246
4370
  page,
4247
4371
  rotation,
4248
4372
  viewport: {
4249
4373
  kind: "scale",
4250
- scale
4374
+ scale: effScale
4251
4375
  },
4252
4376
  ...maxOutputPixels !== void 0 ? { maxOutputPixels } : {},
4253
4377
  background: "transparent",
@@ -4362,7 +4486,9 @@ function setAnnotFlags(fn, annotPtr, partial) {
4362
4486
  if (!fn.FPDFAnnot_SetFlags(annotPtr, next)) throw new EngineError(EngineErrorCode.Unknown, "FPDFAnnot_SetFlags returned false");
4363
4487
  }
4364
4488
  /**
4365
- * Write `/Rect` via `FPDFAnnot_SetRect`. FS_RECTF layout is
4489
+ * Write `/Rect` without touching `/AP`. Appearance preservation/regeneration
4490
+ * belongs to AnnotationMutator; a geometry primitive must not silently rewrite
4491
+ * an existing appearance `/BBox`. FS_RECTF layout is
4366
4492
  * `{ left, top, right, bottom }` (top > bottom in PDF coords).
4367
4493
  */
4368
4494
  function setAnnotRect(fn, mem, annotPtr, rect) {
@@ -4372,12 +4498,40 @@ function setAnnotRect(fn, mem, annotPtr, rect) {
4372
4498
  mem.poke(buf, "f32", rect.top, 4);
4373
4499
  mem.poke(buf, "f32", rect.right, 8);
4374
4500
  mem.poke(buf, "f32", rect.bottom, 12);
4375
- if (!fn.FPDFAnnot_SetRect(annotPtr, buf)) throw new EngineError(EngineErrorCode.Unknown, "FPDFAnnot_SetRect returned false");
4501
+ if (!fn.EPDFAnnot_SetRect(annotPtr, buf)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetRect returned false");
4376
4502
  } finally {
4377
4503
  mem.free(buf);
4378
4504
  }
4379
4505
  }
4380
4506
  /**
4507
+ * Write a UTF-16 string entry into an annotation dictionary. The write
4508
+ * side of `readAnnotString`.
4509
+ */
4510
+ function writeAnnotString(fn, mem, annotPtr, key, value) {
4511
+ const ptr = mem.writeU16String(value);
4512
+ try {
4513
+ fn.FPDFAnnot_SetStringValue(annotPtr, key, ptr);
4514
+ } finally {
4515
+ mem.free(ptr);
4516
+ }
4517
+ }
4518
+ /**
4519
+ * Three-state string write: `null` REMOVES the dictionary entry.
4520
+ *
4521
+ * Clearing goes through `EPDFAnnot_RemoveKey` because
4522
+ * `FPDFAnnot_SetStringValue` with a NULL value writes an EMPTY string —
4523
+ * it never removes. True removal is what keeps the read side honest:
4524
+ * `readAnnotString` returns `null` iff the key is absent, so a cleared
4525
+ * field reads back as `null`, not `''`.
4526
+ */
4527
+ function writeAnnotStringOrClear(fn, mem, annotPtr, key, value) {
4528
+ if (value === null) {
4529
+ fn.EPDFAnnot_RemoveKey(annotPtr, key);
4530
+ return;
4531
+ }
4532
+ writeAnnotString(fn, mem, annotPtr, key, value);
4533
+ }
4534
+ /**
4381
4535
  * Set `/BS /S` (border style code) and `/BS /W` (width) in one call via
4382
4536
  * `EPDFAnnot_SetBorderStyle`. `styleCode` is a raw
4383
4537
  * `FPDF_ANNOT_BORDER_STYLE` enum value — the string<->code mapping lives
@@ -4576,8 +4730,8 @@ function setCalloutLine(fn, mem, annotPtr, points) {
4576
4730
  //#region src/features/annotations/internal/write/writeAnnotationBase.ts
4577
4731
  /**
4578
4732
  * Write the annotation-wide base fields shared by every Draft
4579
- * (contents/nm). The kind-specific writer calls this BEFORE its own
4580
- * field writes; order doesn't actually matter at the PDF level, but
4733
+ * (contents/subject/nm). The kind-specific writer calls this BEFORE its
4734
+ * own field writes; order doesn't actually matter at the PDF level, but
4581
4735
  * keeping the base first makes the writers symmetric with the readers.
4582
4736
  *
4583
4737
  * `/T` (author display) is stamped by the mutator from
@@ -4588,13 +4742,14 @@ function setCalloutLine(fn, mem, annotPtr, points) {
4588
4742
  * annotation; the writer does not.
4589
4743
  */
4590
4744
  function applyAnnotationBaseDraft(fn, mem, annotPtr, draft) {
4591
- if (draft.contents !== void 0) writeStringOrClear(fn, mem, annotPtr, "Contents", draft.contents);
4592
- if (draft.nm !== void 0 && draft.nm.length > 0) writeString(fn, mem, annotPtr, "NM", draft.nm);
4745
+ if (draft.contents !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Contents", draft.contents);
4746
+ if (draft.subject !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Subj", draft.subject);
4747
+ if (draft.nm !== void 0 && draft.nm.length > 0) writeAnnotString(fn, mem, annotPtr, "NM", draft.nm);
4593
4748
  if (draft.flags !== void 0) setAnnotFlags(fn, annotPtr, draft.flags);
4594
4749
  }
4595
4750
  /**
4596
4751
  * Write the annotation-wide base fields shared by every Patch
4597
- * (contents). /T is bound at creation and not patchable — see
4752
+ * (contents/subject). /T is bound at creation and not patchable — see
4598
4753
  * `AnnotationPatchBase`. /NM is monotonic per annotation; the mutator
4599
4754
  * may stamp /NM opportunistically on a weak annotation via
4600
4755
  * `writeAnnotationNm`.
@@ -4605,7 +4760,8 @@ function applyAnnotationBaseDraft(fn, mem, annotPtr, draft) {
4605
4760
  * "..." -> set the entry to that value
4606
4761
  */
4607
4762
  function applyAnnotationBasePatch(fn, mem, annotPtr, patch) {
4608
- if (patch.contents !== void 0) writeStringOrClear(fn, mem, annotPtr, "Contents", patch.contents);
4763
+ if (patch.contents !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Contents", patch.contents);
4764
+ if (patch.subject !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "Subj", patch.subject);
4609
4765
  if (patch.flags !== void 0) setAnnotFlags(fn, annotPtr, patch.flags);
4610
4766
  }
4611
4767
  /**
@@ -4616,7 +4772,7 @@ function applyAnnotationBasePatch(fn, mem, annotPtr, patch) {
4616
4772
  */
4617
4773
  function writeAnnotationAuthor(fn, mem, annotPtr, displayName) {
4618
4774
  if (displayName.length === 0) return;
4619
- writeString(fn, mem, annotPtr, "T", displayName);
4775
+ writeAnnotString(fn, mem, annotPtr, "T", displayName);
4620
4776
  }
4621
4777
  /**
4622
4778
  * Stamp /NM on an annotation that didn't have one. Used by the mutator
@@ -4625,7 +4781,7 @@ function writeAnnotationAuthor(fn, mem, annotPtr, displayName) {
4625
4781
  */
4626
4782
  function writeAnnotationNm(fn, mem, annotPtr, nm) {
4627
4783
  if (nm.length === 0) return;
4628
- writeString(fn, mem, annotPtr, "NM", nm);
4784
+ writeAnnotString(fn, mem, annotPtr, "NM", nm);
4629
4785
  }
4630
4786
  /**
4631
4787
  * Stamp /M (modification date) on an annotation. /M is a standard
@@ -4642,22 +4798,7 @@ function writeAnnotationNm(fn, mem, annotPtr, nm) {
4642
4798
  * Format: `D:YYYYMMDDHHmmSSOHH'mm'` (see formatPdfDate).
4643
4799
  */
4644
4800
  function writeAnnotationModified(fn, mem, annotPtr, now = /* @__PURE__ */ new Date()) {
4645
- writeString(fn, mem, annotPtr, "M", formatPdfDate(now));
4646
- }
4647
- function writeString(fn, mem, annotPtr, key, value) {
4648
- const ptr = mem.writeU16String(value);
4649
- try {
4650
- fn.FPDFAnnot_SetStringValue(annotPtr, key, ptr);
4651
- } finally {
4652
- mem.free(ptr);
4653
- }
4654
- }
4655
- function writeStringOrClear(fn, mem, annotPtr, key, value) {
4656
- if (value === null) {
4657
- fn.FPDFAnnot_SetStringValue(annotPtr, key, NULL_PTR);
4658
- return;
4659
- }
4660
- writeString(fn, mem, annotPtr, key, value);
4801
+ writeAnnotString(fn, mem, annotPtr, "M", formatPdfDate(now));
4661
4802
  }
4662
4803
  //#endregion
4663
4804
  //#region src/features/annotations/internal/identity/captureOrStampStableId.ts
@@ -4829,50 +4970,229 @@ function readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, index, revis
4829
4970
  return reader(fn, mem, annotPtr, base, subtypeCode, ctx);
4830
4971
  }
4831
4972
  //#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;
4973
+ //#region src/features/annotations/internal/write/writeEmbedMetadata.ts
4841
4974
  /**
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)
4975
+ * Write the EmbedPDF-namespaced /EMBD_Metadata annotation dictionary.
4976
+ *
4977
+ * This module owns ONLY the vendor-extension dictionary. Standard PDF
4978
+ * base fields (/T, /M, /NM, /Contents) are handled by
4979
+ * `writers/annotations/base.ts` — /EMBD_Metadata is a separate
4980
+ * dictionary nested under the annotation that carries cloud-side
4981
+ * identity bookkeeping which has no PDF-spec equivalent.
4982
+ *
4983
+ * Schema (v1):
4984
+ * /EMBD_Metadata <<
4985
+ * /SchemaVersion 1 % EmbedPDF metadata schema version
4986
+ * /UserID (acme/alice) % identity that created the annotation
4987
+ * /GroupID (engineering) % group the annotation belongs to
4988
+ * /CreatedBy (acme/alice) % same as UserID on create
4989
+ * /UpdatedBy (acme/alice) % last editor; refreshed on every update
4990
+ * >>
4991
+ *
4992
+ * Read side: see `readers/annotations/embed-metadata.ts` (commit 12).
4993
+ *
4994
+ * PDFium bindings used: the `EPDFAnnot_*EmbedMetadata*` extension API
4995
+ * shipped in the EmbedPDF PDFium fork. Values are written as UTF-16LE
4996
+ * strings via the same `mem.writeU16String` pattern the base writer
4997
+ * uses for /T.
4848
4998
  */
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
- }
4999
+ const KEY_SCHEMA_VERSION = "SchemaVersion";
5000
+ const KEY_USER_ID = "UserID";
5001
+ const KEY_GROUP_ID = "GroupID";
5002
+ const KEY_CREATED_BY = "CreatedBy";
5003
+ const KEY_UPDATED_BY = "UpdatedBy";
4857
5004
  /**
4858
- * Apply a caret patch to an existing annotation. Only present fields are
4859
- * touched.
5005
+ * Stamp /EMBD_Metadata on a freshly created annotation. No-op if the
5006
+ * actor has no identity fields — the worker still emits the standard
5007
+ * /M timestamp via the base writer; only the vendor dictionary is
5008
+ * skipped.
5009
+ *
5010
+ * actor.userId → /UserID, /CreatedBy, /UpdatedBy
5011
+ * actor.groupId → /GroupID
5012
+ *
5013
+ * /SchemaVersion is written whenever any field is written.
4860
5014
  */
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);
5015
+ function applyEmbedMetadataOnCreate(fn, mem, annotPtr, actor) {
5016
+ if (!actor || !actor.userId && !actor.groupId) return;
5017
+ fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
5018
+ if (actor.userId) {
5019
+ setMetadataString(fn, mem, annotPtr, KEY_USER_ID, actor.userId);
5020
+ setMetadataString(fn, mem, annotPtr, KEY_CREATED_BY, actor.userId);
5021
+ setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
5022
+ }
5023
+ if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
4869
5024
  }
4870
5025
  /**
4871
- * Type-narrowing predicate used by the writer registry to pick the caret
4872
- * writer for a draft/patch's `subtype`.
5026
+ * Apply /EMBD_Metadata refreshes on update. Two fields can change on
5027
+ * update; everything else (UserID, CreatedBy) is immutable.
5028
+ *
5029
+ * actor.userId → /UpdatedBy (refreshed every update where the
5030
+ * caller has an identity)
5031
+ * actor.groupId → /GroupID (reassigned only when the patch
5032
+ * explicitly contains a new groupId; route runs
5033
+ * `checkSetGroup` first)
5034
+ *
5035
+ * If the target annotation has no EMBD_Metadata yet (created before
5036
+ * the cloud started stamping, or by a non-cloud writer), we seed
5037
+ * /SchemaVersion so subsequent readers can find the marker.
5038
+ *
5039
+ * No-op when the actor has neither userId nor groupId.
4873
5040
  */
4874
- function isCaretSubtype(subtype) {
4875
- return subtype === "caret";
5041
+ function applyEmbedMetadataOnUpdate(fn, mem, annotPtr, actor) {
5042
+ if (!actor || !actor.userId && !actor.groupId) return;
5043
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_SCHEMA_VERSION, 1);
5044
+ if (actor.userId) setMetadataString(fn, mem, annotPtr, KEY_UPDATED_BY, actor.userId);
5045
+ if (actor.groupId) setMetadataString(fn, mem, annotPtr, KEY_GROUP_ID, actor.groupId);
5046
+ }
5047
+ function setMetadataString(fn, mem, annotPtr, key, value) {
5048
+ const ptr = mem.writeU16String(value);
5049
+ try {
5050
+ fn.EPDFAnnot_SetEmbedMetadataString(annotPtr, key, ptr);
5051
+ } finally {
5052
+ mem.free(ptr);
5053
+ }
5054
+ }
5055
+ //#endregion
5056
+ //#region src/features/annotations/internal/write/writeAnnotationTransformMetadata.ts
5057
+ /**
5058
+ * Write the EmbedPDF transform keys under /EMBD_Metadata. This is the seam
5059
+ * PDFium's native AP generator reads to bake a ROTATED appearance:
5060
+ *
5061
+ * /EMBD_Metadata <<
5062
+ * /Rotation 45 % degrees, PDF convention (CCW)
5063
+ * /UnrotatedRect [x0 y0 x1 y1] % BOX kinds only — the logical box
5064
+ * >>
5065
+ *
5066
+ * The family split (see the v3 plan):
5067
+ * - BOX kinds (square/circle/free-text): `/Rotation` + `/UnrotatedRect`.
5068
+ * With BOTH present the AP generator emits an `/AP /Matrix` that rotates a
5069
+ * box-sized appearance about the box centre, and `/Rect` is the enclosing
5070
+ * AABB. This is the portable, externally-correct rotation.
5071
+ * - VERTEX kinds (line/polyline/polygon/ink): `/Rotation` ONLY (advisory).
5072
+ * The points are already rotated (they are the visual), so a lone
5073
+ * `/Rotation` is INERT for the AP generator (it ignores `/Rotation` with no
5074
+ * `/UnrotatedRect`) — it just records the applied angle so EmbedPDF can
5075
+ * reconstruct an oriented selection box + offer reset.
5076
+ *
5077
+ * Both fields follow the engine-wide tri-state law ("a patch touches what it
5078
+ * states, preserves what it omits"):
5079
+ * - `undefined` → the key is UNTOUCHED (a rect-only move keeps its rotation).
5080
+ * - `null` or `0` → CLEAR just that key via `EPDFAnnot_ClearEmbedMetadataKey`
5081
+ * (0 ≡ no rotation is the canonical identity, not a sentinel; never clear
5082
+ * the whole dict — identity fields UserID/GroupID/CreatedBy/UpdatedBy must
5083
+ * survive).
5084
+ * - a value → SET the key. Setting a nonzero rotation with no
5085
+ * `/UnrotatedRect` (neither in the patch nor already on the annotation)
5086
+ * is an unsatisfiable state for the AP generator and throws `InvalidArg`.
5087
+ *
5088
+ * MUST run BEFORE `EPDFAnnot_GenerateAppearance` so the bake sees the rotation.
5089
+ * `/SchemaVersion` is seeded (stays 1) if this is the first key in the dict.
5090
+ */
5091
+ const KEY_ROTATION = "Rotation";
5092
+ const KEY_UNROTATED_RECT = "UnrotatedRect";
5093
+ /** Seed `/SchemaVersion` when we are about to create the dict by writing the
5094
+ * first transform key, so the marker readers look for is always present. */
5095
+ function ensureSchemaVersion(fn, annotPtr) {
5096
+ if (!fn.EPDFAnnot_HasEmbedMetadata(annotPtr)) fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, "SchemaVersion", 1);
5097
+ }
5098
+ function setRotation(fn, annotPtr, rotation) {
5099
+ ensureSchemaVersion(fn, annotPtr);
5100
+ fn.EPDFAnnot_SetEmbedMetadataNumber(annotPtr, KEY_ROTATION, rotation);
5101
+ }
5102
+ function setUnrotatedRect(fn, mem, annotPtr, rect) {
5103
+ ensureSchemaVersion(fn, annotPtr);
5104
+ const buf = mem.alloc(16);
5105
+ try {
5106
+ mem.poke(buf, "f32", rect.left, 0);
5107
+ mem.poke(buf, "f32", rect.top, 4);
5108
+ mem.poke(buf, "f32", rect.right, 8);
5109
+ mem.poke(buf, "f32", rect.bottom, 12);
5110
+ fn.EPDFAnnot_SetEmbedMetadataRect(annotPtr, KEY_UNROTATED_RECT, buf);
5111
+ } finally {
5112
+ mem.free(buf);
5113
+ }
5114
+ }
5115
+ /**
5116
+ * Write transform metadata for a BOX kind (square/circle/free-text/stamp),
5117
+ * tri-state per field: `undefined` never touches the document (safe to call on
5118
+ * every patch), `null`/`0` clears, a value sets. A nonzero rotation with no
5119
+ * unrotated box anywhere (patch or annotation) is unsatisfiable — the AP
5120
+ * generator needs the pair — and throws instead of writing a broken state.
5121
+ */
5122
+ function writeBoxTransformMetadata(fn, mem, annotPtr, t) {
5123
+ if (t.rotation !== void 0) if (t.rotation !== null && t.rotation !== 0) {
5124
+ 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)");
5125
+ setRotation(fn, annotPtr, t.rotation);
5126
+ } else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5127
+ if (t.unrotatedRect !== void 0) if (t.unrotatedRect !== null) setUnrotatedRect(fn, mem, annotPtr, t.unrotatedRect);
5128
+ else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
5129
+ }
5130
+ /**
5131
+ * Write the advisory `/Rotation` scalar for a VERTEX kind
5132
+ * (line/polyline/polygon/ink), tri-state: `undefined` never touches the
5133
+ * document, `null`/`0` clears, a value sets. Whenever it does write, any
5134
+ * `/UnrotatedRect` is defensively cleared — that key is an impossible state on
5135
+ * a vertex kind and must never accidentally drive the AP generator.
5136
+ */
5137
+ function writeVertexTransformMetadata(fn, annotPtr, t) {
5138
+ if (t.rotation === void 0) return;
5139
+ if (t.rotation !== null && t.rotation !== 0) setRotation(fn, annotPtr, t.rotation);
5140
+ else fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_ROTATION);
5141
+ fn.EPDFAnnot_ClearEmbedMetadataKey(annotPtr, KEY_UNROTATED_RECT);
5142
+ }
5143
+ //#endregion
5144
+ //#region src/features/annotations/internal/write/writeCaretAnnotation.ts
5145
+ /** Default `/C` colour when a caret draft omits it (engine-wide default mark). */
5146
+ const DEFAULT_CARET_COLOR = {
5147
+ r: 255,
5148
+ g: 0,
5149
+ b: 0
5150
+ };
5151
+ /** Default opacity, set explicitly so reads always round-trip the same value. */
5152
+ const DEFAULT_OPACITY$4 = 1;
5153
+ /**
5154
+ * Apply a caret draft to a freshly-created annotation. Caret carries no
5155
+ * geometry of its own beyond `/Rect`. Order:
5156
+ * 1. base author-metadata (contents/nm/flags)
5157
+ * 2. `/Rect` (required — supplied by the caller; never derived)
5158
+ * 3. `/C` color + `/CA` opacity
5159
+ * 4. `/RD` rectangle differences (optional)
5160
+ */
5161
+ function applyCaretDraft(fn, mem, annotPtr, draft) {
5162
+ applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
5163
+ setAnnotRect(fn, mem, annotPtr, draft.rect);
5164
+ writeBoxTransformMetadata(fn, mem, annotPtr, {
5165
+ rotation: draft.rotation,
5166
+ unrotatedRect: draft.unrotatedRect
5167
+ });
5168
+ setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_CARET_COLOR);
5169
+ setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY$4);
5170
+ if (draft.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(draft.intent));
5171
+ if (draft.rectDifferences != null) setRectangleDifferences(fn, annotPtr, draft.rectDifferences);
5172
+ }
5173
+ /**
5174
+ * Apply a caret patch to an existing annotation. Only present fields are
5175
+ * touched.
5176
+ */
5177
+ function applyCaretPatch(fn, mem, annotPtr, patch) {
5178
+ applyAnnotationBasePatch(fn, mem, annotPtr, patch);
5179
+ if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
5180
+ if (patch.rotation !== void 0 || patch.unrotatedRect !== void 0) writeBoxTransformMetadata(fn, mem, annotPtr, {
5181
+ rotation: patch.rotation,
5182
+ unrotatedRect: patch.unrotatedRect
5183
+ });
5184
+ if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
5185
+ if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
5186
+ if (patch.intent !== void 0) setIntent(fn, annotPtr, caretIntentToName(patch.intent));
5187
+ if (patch.rectDifferences === null) clearRectangleDifferences(fn, annotPtr);
5188
+ else if (patch.rectDifferences !== void 0) setRectangleDifferences(fn, annotPtr, patch.rectDifferences);
5189
+ }
5190
+ /**
5191
+ * Type-narrowing predicate used by the writer registry to pick the caret
5192
+ * writer for a draft/patch's `subtype`.
5193
+ */
5194
+ function isCaretSubtype(subtype) {
5195
+ return subtype === "caret";
4876
5196
  }
4877
5197
  //#endregion
4878
5198
  //#region src/features/annotations/internal/write/writeFileAttachmentAnnotation.ts
@@ -5039,177 +5359,6 @@ function applyFilledStylePatch(fn, mem, annotPtr, patch) {
5039
5359
  applyGeometryStylePatch(fn, mem, annotPtr, patch);
5040
5360
  }
5041
5361
  //#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
5362
  //#region src/features/annotations/internal/write/writeFreeTextAnnotation.ts
5214
5363
  /**
5215
5364
  * Default `/DA` colour for free text: black (border + default text). Unlike
@@ -5979,13 +6128,18 @@ const DEFAULT_OPACITY = 1;
5979
6128
  * Apply a text (sticky-note) draft. The visual is entirely generator-owned:
5980
6129
  * the mutator's closing `regenerateAppearance` bakes the 20×20 note icon
5981
6130
  * from `/C` + `/Name` (GenerateTextAP), so this writer only records state.
6131
+ * `/State` + `/StateModel` are dictionary-only (ISO 32000 §12.5.6.3) and
6132
+ * never reach the generator.
5982
6133
  */
5983
6134
  function applyTextDraft(fn, mem, annotPtr, draft) {
6135
+ if (draft.state !== void 0 && draft.stateModel === void 0) throw new EngineError(EngineErrorCode.InvalidArg, "text draft: state requires stateModel (ISO 32000 §12.5.6.3)");
5984
6136
  applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
5985
6137
  setAnnotRect(fn, mem, annotPtr, draft.rect);
5986
6138
  setAnnotColor(fn, annotPtr, draft.color ?? DEFAULT_NOTE_COLOR);
5987
6139
  setAnnotOpacity(fn, annotPtr, draft.opacity ?? DEFAULT_OPACITY);
5988
6140
  setNoteIcon(fn, annotPtr, draft.icon ?? "note");
6141
+ if (draft.stateModel !== void 0) writeAnnotString(fn, mem, annotPtr, "StateModel", stateModelToPdf(draft.stateModel));
6142
+ if (draft.state !== void 0) writeAnnotString(fn, mem, annotPtr, "State", stateToPdf(draft.state));
5989
6143
  }
5990
6144
  function applyTextPatch(fn, mem, annotPtr, patch) {
5991
6145
  applyAnnotationBasePatch(fn, mem, annotPtr, patch);
@@ -5993,6 +6147,8 @@ function applyTextPatch(fn, mem, annotPtr, patch) {
5993
6147
  if (patch.color !== void 0) setAnnotColor(fn, annotPtr, patch.color);
5994
6148
  if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
5995
6149
  if (patch.icon !== void 0) setNoteIcon(fn, annotPtr, patch.icon);
6150
+ if (patch.stateModel !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "StateModel", patch.stateModel === null ? null : stateModelToPdf(patch.stateModel));
6151
+ if (patch.state !== void 0) writeAnnotStringOrClear(fn, mem, annotPtr, "State", patch.state === null ? null : stateToPdf(patch.state));
5996
6152
  }
5997
6153
  function isTextSubtype(subtype) {
5998
6154
  return subtype === "text";
@@ -6970,17 +7126,35 @@ var RedactionApplier = class {
6970
7126
  }
6971
7127
  applyAllOnPage(pagePtr) {
6972
7128
  const { fn, mem } = this.runtime;
7129
+ const hasRedactions = this.pageHasRedactAnnotations(pagePtr);
6973
7130
  return withScratch(mem, 4, (countPtr) => {
6974
- if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr)) return {
6975
- status: "unchanged",
6976
- removed: 0
6977
- };
7131
+ if (!fn.EPDFPage_ApplyRedactions(pagePtr, countPtr)) {
7132
+ if (hasRedactions) throw new EngineError(EngineErrorCode.Unknown, "native page redaction apply failed");
7133
+ return {
7134
+ status: "unchanged",
7135
+ removed: 0
7136
+ };
7137
+ }
6978
7138
  return {
6979
7139
  status: "applied",
6980
7140
  removed: readI32(mem, countPtr) >>> 0
6981
7141
  };
6982
7142
  });
6983
7143
  }
7144
+ pageHasRedactAnnotations(pagePtr) {
7145
+ const { fn } = this.runtime;
7146
+ const count = fn.FPDFPage_GetAnnotCount(pagePtr);
7147
+ for (let index = 0; index < count; index++) {
7148
+ const annotPtr = fn.FPDFPage_GetAnnot(pagePtr, index);
7149
+ if (!annotPtr) continue;
7150
+ try {
7151
+ if (subtypeFromCode(fn.FPDFAnnot_GetSubtype(annotPtr)) === "redact") return true;
7152
+ } finally {
7153
+ fn.FPDFPage_CloseAnnot(annotPtr);
7154
+ }
7155
+ }
7156
+ return false;
7157
+ }
6984
7158
  applyRefsOnPage(pagePtr, refs) {
6985
7159
  const { fn, mem } = this.runtime;
6986
7160
  let removed = 0;
@@ -7006,23 +7180,26 @@ var RedactionApplier = class {
7006
7180
  //#region src/features/text/PageTextReader.ts
7007
7181
  /**
7008
7182
  * 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.
7183
+ * `PagePtrPool`, opens a PDFium text page (`FPDFText_LoadPage`), extracts
7184
+ * the page text, and releases everything in reverse order.
7185
+ *
7186
+ * Index spaces (see engine-core `text/charmap.ts`): `charCount` is the
7187
+ * CHARACTER space (`FPDFText_CountChars` — the space geometry runs tile);
7188
+ * `text` is the TEXT projection. The two diverge when a character
7189
+ * contributes zero text units (non-printing) or two (supplementary plane);
7190
+ * `charMap` anchors encode exactly those deviations.
7012
7191
  *
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.
7192
+ * Both come from the fork's batched extension calls, which share one
7193
+ * encoding authority (`FX_UTF16Encode`) so text and map can never
7194
+ * disagree:
7019
7195
  *
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.
7196
+ * - `EPDFText_GetTextFull` — true UTF-16LE extraction; supplementary
7197
+ * characters arrive as surrogate pairs instead of being silently
7198
+ * dropped by `FPDFText_GetText`'s UCS-2 encoder. Called once with an
7199
+ * upper-bound buffer (2 units per character + NUL), so no measure
7200
+ * round-trip.
7201
+ * - `EPDFText_GetCharToTextMap` — the anchors, measure-then-fill. Zero
7202
+ * anchors is the common (identity) page and skips the fill call.
7026
7203
  */
7027
7204
  var PageTextReader = class {
7028
7205
  constructor(runtime, session) {
@@ -7031,7 +7208,7 @@ var PageTextReader = class {
7031
7208
  }
7032
7209
  read(pageObjectNumber, signal) {
7033
7210
  throwIfAborted(signal);
7034
- const { fn, mem } = this.runtime;
7211
+ const { fn } = this.runtime;
7035
7212
  const pool = this.session.pagePool();
7036
7213
  const pagePtr = pool.acquire(pageObjectNumber);
7037
7214
  try {
@@ -7040,10 +7217,18 @@ var PageTextReader = class {
7040
7217
  if (!textPagePtr) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `FPDFText_LoadPage returned null for page object ${pageObjectNumber}`);
7041
7218
  try {
7042
7219
  throwIfAborted(signal);
7043
- const charCount = fn.FPDFText_CountChars(textPagePtr);
7044
- return {
7045
- text: readPageText(fn, mem, textPagePtr, charCount),
7046
- charCount: Math.max(charCount, 0)
7220
+ const charCount = Math.max(fn.FPDFText_CountChars(textPagePtr), 0);
7221
+ const text = readTextFull(this.runtime, textPagePtr, charCount);
7222
+ const charMap = readCharMap(this.runtime, textPagePtr, pageObjectNumber);
7223
+ const violation = charMapViolation(charCount, text.length, charMap);
7224
+ if (violation !== null) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `char map invariant violated for page object ${pageObjectNumber}: ${violation}`);
7225
+ return charMap.length > 0 ? {
7226
+ text,
7227
+ charCount,
7228
+ charMap
7229
+ } : {
7230
+ text,
7231
+ charCount
7047
7232
  };
7048
7233
  } finally {
7049
7234
  fn.FPDFText_ClosePage(textPagePtr);
@@ -7053,21 +7238,65 @@ var PageTextReader = class {
7053
7238
  }
7054
7239
  }
7055
7240
  };
7056
- function readPageText(fn, mem, textPagePtr, charCount) {
7241
+ /**
7242
+ * Full-fidelity page text in one call: the buffer is sized at the maximum
7243
+ * possible expansion (every character a surrogate pair, plus the NUL), so
7244
+ * the returned requirement always fits and the measure round-trip is
7245
+ * skipped. The return value counts UTF-16 units INCLUDING the terminator.
7246
+ */
7247
+ function readTextFull(runtime, textPagePtr, charCount) {
7057
7248
  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);
7249
+ const { fn, mem } = runtime;
7250
+ const bufUnits = charCount * 2 + 1;
7251
+ return withScratch(mem, bufUnits * 2, (buf) => {
7252
+ const written = fn.EPDFText_GetTextFull(textPagePtr, buf, bufUnits);
7253
+ if (written <= 1) return "";
7254
+ return decodeUtf16Le(mem.readBytes(buf, (written - 1) * 2));
7255
+ });
7256
+ }
7257
+ function decodeUtf16Le(bytes) {
7258
+ const units = new Uint16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength >> 1);
7259
+ let out = "";
7260
+ const CHUNK = 8192;
7261
+ for (let i = 0; i < units.length; i += CHUNK) out += String.fromCharCode(...units.subarray(i, Math.min(i + CHUNK, units.length)));
7262
+ return out;
7263
+ }
7264
+ /** Anchor struct layout: two little-endian i32s (see EPDF_CHAR_MAP_ANCHOR). */
7265
+ const ANCHOR_BYTES = 8;
7266
+ function readCharMap(runtime, textPagePtr, pageObjectNumber) {
7267
+ const { fn, mem } = runtime;
7268
+ const count = fn.EPDFText_GetCharToTextMap(textPagePtr, NULL_PTR, 0);
7269
+ if (count < 0) throw new EngineError(EngineErrorCode.RuntimeUnavailable, `EPDFText_GetCharToTextMap failed for page object ${pageObjectNumber}`);
7270
+ if (count === 0) return [];
7271
+ return withScratch(mem, count * ANCHOR_BYTES, (buf) => {
7272
+ fn.EPDFText_GetCharToTextMap(textPagePtr, buf, count);
7273
+ const bytes = mem.readBytes(buf, count * ANCHOR_BYTES);
7274
+ const words = new Int32Array(bytes.buffer, bytes.byteOffset, count * 2);
7275
+ const anchors = new Array(count);
7276
+ for (let i = 0; i < count; i++) anchors[i] = [words[i * 2], words[i * 2 + 1]];
7277
+ return anchors;
7061
7278
  });
7062
7279
  }
7063
7280
  //#endregion
7064
7281
  //#region src/features/geometry/PageGeometryReader.ts
7065
7282
  /**
7283
+ * Mid-object orientation drift that forces a run split (~0.5°). Real content
7284
+ * keeps one char matrix per text object; this guards the exotic cases so a
7285
+ * run's `rotation` is always representative of every glyph in it.
7286
+ */
7287
+ const ROTATION_SPLIT_TOLERANCE = .0087;
7288
+ /**
7066
7289
  * Geometry-only text layout reader.
7067
7290
  *
7068
7291
  * Emits geometry in PDF user space (y-up edges) — the canonical engine
7069
7292
  * geometry. The viewer converts to content/view space via the page geometry
7070
7293
  * matrix; this reader applies NO Y-flip or device transform.
7294
+ *
7295
+ * One `EPDFText_GetCharGeometry` call per glyph supplies boxes, oriented
7296
+ * cells, and flags together; `buildRunsFromRawGlyphs` then groups glyphs
7297
+ * into the upright/rotated run union. Upright runs are byte-identical to the
7298
+ * pre-orientation reader (quads the native layer offers are deliberately
7299
+ * dropped — the dominant case never pays for orientation).
7071
7300
  */
7072
7301
  var PageGeometryReader = class {
7073
7302
  constructor(runtime, session) {
@@ -7076,7 +7305,7 @@ var PageGeometryReader = class {
7076
7305
  }
7077
7306
  read(pageObjectNumber, signal) {
7078
7307
  throwIfAborted(signal);
7079
- const { fn } = this.runtime;
7308
+ const { fn, mem } = this.runtime;
7080
7309
  const pool = this.session.pagePool();
7081
7310
  const pagePtr = pool.acquire(pageObjectNumber);
7082
7311
  try {
@@ -7086,12 +7315,27 @@ var PageGeometryReader = class {
7086
7315
  try {
7087
7316
  throwIfAborted(signal);
7088
7317
  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));
7318
+ const records = [];
7319
+ const geometryPtr = mem.alloc(EPDF_CHAR_GEOMETRY_LAYOUT.bytes);
7320
+ try {
7321
+ let prevObjectKey = null;
7322
+ for (let i = 0; i < glyphCount; i++) {
7323
+ throwIfAborted(signal);
7324
+ const objectKey = fn.FPDFText_GetTextObject(textPagePtr, i);
7325
+ const record = {
7326
+ objectKey,
7327
+ ...this.readGlyphRaw(textPagePtr, i, geometryPtr)
7328
+ };
7329
+ if (objectKey !== prevObjectKey) {
7330
+ record.fontSize = fn.FPDFText_GetFontSize(textPagePtr, i);
7331
+ prevObjectKey = objectKey;
7332
+ }
7333
+ records.push(record);
7334
+ }
7335
+ } finally {
7336
+ mem.free(geometryPtr);
7093
7337
  }
7094
- return { runs: this.buildRunsFromGlyphs(glyphs, textPagePtr) };
7338
+ return { runs: buildRunsFromRawGlyphs(records) };
7095
7339
  } finally {
7096
7340
  fn.FPDFText_ClosePage(textPagePtr);
7097
7341
  }
@@ -7099,96 +7343,206 @@ var PageGeometryReader = class {
7099
7343
  pool.release(pageObjectNumber);
7100
7344
  }
7101
7345
  }
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);
7346
+ readGlyphRaw(textPagePtr, charIndex, geometryPtr) {
7347
+ const { fn, mem } = this.runtime;
7348
+ if (!fn.EPDFText_GetCharGeometry(textPagePtr, charIndex, geometryPtr)) return emptyRawGlyph();
7349
+ const { offsets, flags: bits } = EPDF_CHAR_GEOMETRY_LAYOUT;
7350
+ const native = readI32(mem, geometryPtr, offsets.flags) >>> 0;
7351
+ if (native & bits.empty) return emptyRawGlyph();
7352
+ const upright = Boolean(native & bits.upright);
7353
+ const raw = {
7354
+ flags: native & bits.space ? 1 : 0,
7355
+ looseBox: normalizePdfRect(readRectF(mem, geometryPtr, offsets.looseBox)),
7356
+ upright,
7357
+ rotation: 0,
7358
+ ascentFlip: false
7359
+ };
7360
+ if (native & bits.hasTightBox) raw.tightBox = normalizePdfRect(readRectF(mem, geometryPtr, offsets.tightBox));
7361
+ if (native & bits.hasLooseQuad) {
7362
+ raw.looseQuad = readQuad(mem, geometryPtr, offsets.looseQuad);
7363
+ if (native & bits.hasTightQuad) raw.tightQuad = readQuad(mem, geometryPtr, offsets.tightQuad);
7364
+ if (!upright) {
7365
+ const a = readF32(mem, geometryPtr, offsets.matrix);
7366
+ const b = readF32(mem, geometryPtr, offsets.matrix + 4);
7367
+ const c = readF32(mem, geometryPtr, offsets.matrix + 8);
7368
+ const d = readF32(mem, geometryPtr, offsets.matrix + 12);
7369
+ raw.rotation = Math.atan2(b, a);
7370
+ raw.ascentFlip = a * d - b * c < 0;
7121
7371
  }
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
7372
  }
7130
- return runs;
7373
+ return raw;
7131
7374
  }
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);
7375
+ };
7376
+ /**
7377
+ * Group per-glyph records into the run union. Pure — exported for tests.
7378
+ *
7379
+ * Runs split on text-object change (the legacy rule) and on orientation
7380
+ * change between classifiable glyphs (θ drift / mixed orientation). A run's
7381
+ * variant is decided by its FIRST classifiable glyph:
7382
+ * - empty glyphs never classify (they adopt the run's variant);
7383
+ * - real glyphs WITHOUT an oriented cell (singular matrix, synthesized
7384
+ * /ActualText pieces) classify as upright — box-only data degrades to
7385
+ * exactly the legacy behavior;
7386
+ * - real glyphs with a non-upright matrix classify as rotated.
7387
+ * Runs that never see a classifiable glyph emit as upright (degenerate,
7388
+ * legacy behavior).
7389
+ */
7390
+ function buildRunsFromRawGlyphs(records) {
7391
+ const runs = [];
7392
+ let buffer = [];
7393
+ let charStart = 0;
7394
+ let fontSize;
7395
+ let objectKey = null;
7396
+ let cls;
7397
+ const flush = (nextStart) => {
7398
+ if (buffer.length > 0) {
7399
+ runs.push(materializeRun(buffer, charStart, fontSize, cls));
7400
+ buffer = [];
7401
+ cls = void 0;
7402
+ }
7403
+ charStart = nextStart;
7404
+ };
7405
+ for (let i = 0; i < records.length; i++) {
7406
+ const record = records[i];
7407
+ if (record.objectKey !== objectKey) {
7408
+ flush(i);
7409
+ objectKey = record.objectKey;
7410
+ fontSize = record.fontSize;
7411
+ }
7412
+ const recordClass = classOf(record);
7413
+ if (recordClass) {
7414
+ if (cls && !sameClass(cls, recordClass)) flush(i);
7415
+ if (!cls) cls = recordClass;
7173
7416
  }
7417
+ buffer.push(record);
7418
+ }
7419
+ flush(records.length);
7420
+ return runs;
7421
+ }
7422
+ function classOf(record) {
7423
+ if (record.flags & 2) return void 0;
7424
+ if (record.looseQuad && !record.upright) return {
7425
+ upright: false,
7426
+ rotation: record.rotation,
7427
+ ascentFlip: record.ascentFlip
7428
+ };
7429
+ return { upright: true };
7430
+ }
7431
+ function sameClass(a, b) {
7432
+ if (a.upright || b.upright) return a.upright === b.upright;
7433
+ if (a.ascentFlip !== b.ascentFlip) return false;
7434
+ const delta = a.rotation - b.rotation;
7435
+ return Math.abs(Math.atan2(Math.sin(delta), Math.cos(delta))) <= ROTATION_SPLIT_TOLERANCE;
7436
+ }
7437
+ function materializeRun(buffer, charStart, fontSize, cls) {
7438
+ if (cls && !cls.upright) {
7439
+ const glyphs = buffer.map((g) => g.looseQuad ? {
7440
+ looseQuad: g.looseQuad,
7441
+ flags: g.flags,
7442
+ ...g.tightQuad ? { tightQuad: g.tightQuad } : {}
7443
+ } : {
7444
+ looseQuad: zeroQuad(),
7445
+ flags: g.flags
7446
+ });
7447
+ return {
7448
+ rect: runBounds(buffer, (g) => g.looseQuad ? pdfQuadBounds(g.looseQuad) : ZERO_RECT),
7449
+ charStart,
7450
+ glyphs,
7451
+ rotation: cls.rotation,
7452
+ ascentFlip: cls.ascentFlip,
7453
+ ...fontSize !== void 0 ? { fontSize } : {}
7454
+ };
7455
+ }
7456
+ const glyphs = buffer.map((g) => ({
7457
+ looseBox: g.looseBox,
7458
+ flags: g.flags,
7459
+ ...g.tightBox ? { tightBox: g.tightBox } : {}
7460
+ }));
7461
+ return {
7462
+ rect: runBounds(buffer, (g) => g.looseBox),
7463
+ charStart,
7464
+ glyphs,
7465
+ ...fontSize !== void 0 ? { fontSize } : {}
7466
+ };
7467
+ }
7468
+ /** Legacy run-rect algorithm: seed from the first glyph, expand on non-empty. */
7469
+ function runBounds(buffer, boundsOf) {
7470
+ const seed = boundsOf(buffer[0]);
7471
+ const rect = {
7472
+ left: seed.left,
7473
+ bottom: seed.bottom,
7474
+ right: seed.right,
7475
+ top: seed.top
7476
+ };
7477
+ for (const g of buffer) {
7478
+ if (g.flags & 2) continue;
7479
+ const b = boundsOf(g);
7480
+ rect.left = Math.min(rect.left, b.left);
7481
+ rect.bottom = Math.min(rect.bottom, b.bottom);
7482
+ rect.right = Math.max(rect.right, b.right);
7483
+ rect.top = Math.max(rect.top, b.top);
7174
7484
  }
7485
+ return rect;
7486
+ }
7487
+ const ZERO_RECT = {
7488
+ left: 0,
7489
+ bottom: 0,
7490
+ right: 0,
7491
+ top: 0
7175
7492
  };
7176
- function emptyGlyph() {
7493
+ function zeroQuad() {
7494
+ return {
7495
+ p1: {
7496
+ x: 0,
7497
+ y: 0
7498
+ },
7499
+ p2: {
7500
+ x: 0,
7501
+ y: 0
7502
+ },
7503
+ p3: {
7504
+ x: 0,
7505
+ y: 0
7506
+ },
7507
+ p4: {
7508
+ x: 0,
7509
+ y: 0
7510
+ }
7511
+ };
7512
+ }
7513
+ function emptyRawGlyph() {
7177
7514
  return {
7515
+ flags: 2,
7178
7516
  looseBox: {
7179
7517
  left: 0,
7180
7518
  bottom: 0,
7181
7519
  right: 0,
7182
7520
  top: 0
7183
7521
  },
7184
- isEmpty: true
7522
+ upright: true,
7523
+ rotation: 0,
7524
+ ascentFlip: false
7185
7525
  };
7186
7526
  }
7187
- function toSlimGlyph(glyph) {
7527
+ function readQuad(mem, ptr, byteOffset) {
7528
+ const f = (offset) => readF32(mem, ptr, byteOffset + offset);
7188
7529
  return {
7189
- looseBox: glyph.looseBox,
7190
- flags: glyph.isEmpty ? 2 : glyph.isSpace ? 1 : 0,
7191
- ...glyph.tightBox ? { tightBox: glyph.tightBox } : {}
7530
+ p1: {
7531
+ x: f(0),
7532
+ y: f(4)
7533
+ },
7534
+ p2: {
7535
+ x: f(8),
7536
+ y: f(12)
7537
+ },
7538
+ p3: {
7539
+ x: f(16),
7540
+ y: f(20)
7541
+ },
7542
+ p4: {
7543
+ x: f(24),
7544
+ y: f(28)
7545
+ }
7192
7546
  };
7193
7547
  }
7194
7548
  //#endregion
@@ -8561,7 +8915,7 @@ function emptyResult(index, status) {
8561
8915
  //#endregion
8562
8916
  //#region src/features/search/internal/pageCorpusCache.ts
8563
8917
  /**
8564
- * Per-session, per-page search corpus: the page's extracted text plus its
8918
+ * Per-session, per-page search corpus: the page's text snapshot plus its
8565
8919
  * DEFAULT fold (the one literal queries with default options search).
8566
8920
  * This is the local engine's in-memory equivalent of the server's corpus
8567
8921
  * artifacts — same fold version, same shape, built lazily on first search
@@ -8586,19 +8940,20 @@ function acquirePageCorpus(runtime, session, pageObjectNumber, signal) {
8586
8940
  cache.set(session, pages);
8587
8941
  }
8588
8942
  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, {
8943
+ if (hit && hit.seq === seq) return hit;
8944
+ const snapshot = new PageTextReader(runtime, session).read(pageObjectNumber, signal);
8945
+ const entry = {
8594
8946
  seq,
8595
- folded
8596
- });
8947
+ folded: foldText(snapshot.text),
8948
+ snapshot
8949
+ };
8950
+ pages.delete(pageObjectNumber);
8951
+ pages.set(pageObjectNumber, entry);
8597
8952
  if (pages.size > MAX_CACHED_PAGES) {
8598
8953
  const oldest = pages.keys().next().value;
8599
8954
  if (oldest !== void 0) pages.delete(oldest);
8600
8955
  }
8601
- return folded;
8956
+ return entry;
8602
8957
  }
8603
8958
  //#endregion
8604
8959
  //#region src/features/search/internal/searchCursor.ts
@@ -8707,19 +9062,23 @@ var SearchReader = class {
8707
9062
  throwIfAborted(signal);
8708
9063
  const pon = order[scanned].pageObjectNumber;
8709
9064
  const corpus = acquirePageCorpus(this.runtime, this.session, pon, signal);
9065
+ const text = corpus.snapshot.text;
8710
9066
  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);
9067
+ if (query.regex) ranges = matchRegex(text, query);
9068
+ else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
9069
+ else ranges = matchLiteral(corpus.folded, query);
8714
9070
  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
- });
9071
+ const layout = buildPageTextLayout(new PageGeometryReader(this.runtime, this.session).read(pon, signal));
9072
+ for (const range of ranges) {
9073
+ const chars = charRangeForTextOffsets(corpus.snapshot, range.start, range.start + range.length);
9074
+ matches.push({
9075
+ pageObjectNumber: pon,
9076
+ charStart: chars.start,
9077
+ charCount: chars.end - chars.start,
9078
+ segments: textSegmentsForRange(layout, chars.start, chars.end - chars.start),
9079
+ ...mode === "full" ? { snippet: buildSnippet(text, range) } : {}
9080
+ });
9081
+ }
8723
9082
  }
8724
9083
  scanned++;
8725
9084
  pagesThisSlice++;
@@ -9000,9 +9359,10 @@ function validatePatchValue(key, value) {
9000
9359
  * worker wire shape from @embedpdf/engine-core.
9001
9360
  */
9002
9361
  var WorkerHost = class {
9003
- constructor(runtime, post) {
9362
+ constructor(runtime, post, options = {}) {
9004
9363
  this.runtime = runtime;
9005
9364
  this.post = post;
9365
+ this.options = options;
9006
9366
  _defineProperty(this, "sessions", /* @__PURE__ */ new Map());
9007
9367
  _defineProperty(this, "baseDocuments", void 0);
9008
9368
  _defineProperty(this, "aborts", /* @__PURE__ */ new Map());
@@ -9028,6 +9388,10 @@ var WorkerHost = class {
9028
9388
  this.aborts.get(msg.jobId)?.abort();
9029
9389
  return;
9030
9390
  }
9391
+ if (msg.kind === "pages.renderEncoded" || msg.kind === "document.renderPageFileEncoded" || msg.kind === "annotations.renderAppearancesEncoded") {
9392
+ this.receiveEncoded(msg);
9393
+ return;
9394
+ }
9031
9395
  const ctrl = new AbortController();
9032
9396
  this.aborts.set(msg.jobId, ctrl);
9033
9397
  let resultPack;
@@ -9131,6 +9495,9 @@ var WorkerHost = class {
9131
9495
  case "pages.insert":
9132
9496
  resultPack = this.handlePagesInsert(msg, ctrl.signal);
9133
9497
  break;
9498
+ case "pages.insertBlank":
9499
+ resultPack = this.handlePagesInsertBlank(msg, ctrl.signal);
9500
+ break;
9134
9501
  case "attachments.list":
9135
9502
  resultPack = this.handleAttachmentsList(msg, ctrl.signal);
9136
9503
  break;
@@ -9423,6 +9790,17 @@ var WorkerHost = class {
9423
9790
  result
9424
9791
  }, req.artifactPath);
9425
9792
  }
9793
+ handlePagesInsertBlank(req, signal) {
9794
+ const session = this.requireSession(req);
9795
+ const result = new PagesInserter(this.runtime, session).insertBlank({
9796
+ size: req.size,
9797
+ count: req.count
9798
+ }, req.destIndex, signal);
9799
+ return this.finishMutation(session, {
9800
+ tag: "pages.insertBlank",
9801
+ result
9802
+ }, req.artifactPath);
9803
+ }
9426
9804
  handleAttachmentsList(req, signal) {
9427
9805
  const session = this.requireSession(req);
9428
9806
  return wirePack({
@@ -9515,6 +9893,105 @@ var WorkerHost = class {
9515
9893
  raster
9516
9894
  }, [raster.data]);
9517
9895
  }
9896
+ async receiveEncoded(msg) {
9897
+ if (!this.options.imageEncoder) {
9898
+ this.post(wirePack({
9899
+ kind: "reject",
9900
+ jobId: msg.jobId,
9901
+ error: serializeError(new EngineError(EngineErrorCode.NotImplemented, "this engine has no image encoder (the *.renderEncoded kinds are cloud-server surface)"))
9902
+ }, EMPTY_TRANSFER));
9903
+ return;
9904
+ }
9905
+ const ctrl = new AbortController();
9906
+ this.aborts.set(msg.jobId, ctrl);
9907
+ try {
9908
+ let resultPack;
9909
+ switch (msg.kind) {
9910
+ case "pages.renderEncoded":
9911
+ resultPack = await this.handlePagesRenderEncoded(msg, ctrl.signal);
9912
+ break;
9913
+ case "document.renderPageFileEncoded":
9914
+ resultPack = await this.handleDocumentRenderPageFileEncoded(msg, ctrl.signal);
9915
+ break;
9916
+ case "annotations.renderAppearancesEncoded":
9917
+ resultPack = await this.handleAnnotationsRenderAppearancesEncoded(msg, ctrl.signal);
9918
+ break;
9919
+ }
9920
+ this.post(wirePack({
9921
+ kind: "resolve",
9922
+ jobId: msg.jobId,
9923
+ result: resultPack.payload
9924
+ }, resultPack.transfer));
9925
+ } catch (err) {
9926
+ this.post(wirePack({
9927
+ kind: "reject",
9928
+ jobId: msg.jobId,
9929
+ error: serializeError(err)
9930
+ }, EMPTY_TRANSFER));
9931
+ } finally {
9932
+ this.aborts.delete(msg.jobId);
9933
+ }
9934
+ }
9935
+ async encodeRaster(raster, encode, signal) {
9936
+ const encoder = this.options.imageEncoder;
9937
+ if (!encoder) throw new EngineError(EngineErrorCode.NotImplemented, "this engine has no image encoder (the *.renderEncoded kinds are cloud-server surface)");
9938
+ const { bytes, contentType } = await encoder.encode(raster, encode);
9939
+ if (signal.aborted) throw new EngineError(EngineErrorCode.Aborted, "aborted during image encode");
9940
+ return {
9941
+ contentType,
9942
+ width: raster.width,
9943
+ height: raster.height,
9944
+ bytes
9945
+ };
9946
+ }
9947
+ async handlePagesRenderEncoded(req, signal) {
9948
+ const inner = this.handlePagesRender({
9949
+ ...req,
9950
+ kind: "pages.render"
9951
+ }, signal);
9952
+ if (inner.payload.tag !== "pages.render") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
9953
+ const image = await this.encodeRaster(inner.payload.raster, req.encode, signal);
9954
+ return wirePack({
9955
+ tag: "pages.renderEncoded",
9956
+ image
9957
+ }, [image.bytes.buffer]);
9958
+ }
9959
+ async handleDocumentRenderPageFileEncoded(req, signal) {
9960
+ const inner = this.handleDocumentRenderPageFile({
9961
+ ...req,
9962
+ kind: "document.renderPageFile"
9963
+ }, signal);
9964
+ if (inner.payload.tag !== "document.renderPageFile") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
9965
+ const image = await this.encodeRaster(inner.payload.raster, req.encode, signal);
9966
+ return wirePack({
9967
+ tag: "document.renderPageFileEncoded",
9968
+ pageObjectNumber: inner.payload.pageObjectNumber,
9969
+ pageCount: inner.payload.pageCount,
9970
+ image
9971
+ }, [image.bytes.buffer]);
9972
+ }
9973
+ async handleAnnotationsRenderAppearancesEncoded(req, signal) {
9974
+ const inner = this.handleAnnotationsRenderAppearances({
9975
+ ...req,
9976
+ kind: "annotations.renderAppearances"
9977
+ }, signal);
9978
+ if (inner.payload.tag !== "annotations.renderAppearances") throw new EngineError(EngineErrorCode.WireFormat, `unexpected ${inner.payload.tag}`);
9979
+ const { pageState, appearances } = inner.payload.result;
9980
+ const encoded = [];
9981
+ for (const a of appearances) encoded.push({
9982
+ ref: a.ref,
9983
+ mode: a.mode,
9984
+ rect: a.rect,
9985
+ image: await this.encodeRaster(a.raster, req.encode, signal)
9986
+ });
9987
+ return wirePack({
9988
+ tag: "annotations.renderAppearancesEncoded",
9989
+ result: {
9990
+ pageState,
9991
+ appearances: encoded
9992
+ }
9993
+ }, encoded.map((e) => e.image.bytes.buffer));
9994
+ }
9518
9995
  handleDocumentSaveBuffer(req) {
9519
9996
  const session = this.requireSession(req);
9520
9997
  const saved = new DocumentSaver(this.runtime, session).saveStandaloneToBuffer(req.mode);
@@ -9846,6 +10323,6 @@ function passwordRequiredProbe() {
9846
10323
  };
9847
10324
  }
9848
10325
  //#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 };
10326
+ 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
10327
 
9851
10328
  //# sourceMappingURL=index.js.map