@embedpdf/engine-services 3.0.0-next.11 → 3.0.0-next.13

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, PAGE_INSERT_BLANK_MAX_COUNT, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, UNKNOWN_WEAK_ANNOTATION_STATE, appearanceImpactOf, buildPageTextLayout, buildSnippet, charMapViolation, charRangeForTextOffsets, decodeSubmitFormFlags, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, pdfQuadBounds, revisionTokensEqual, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, textSegmentsForRange, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
1
+ import { AbortError, EMPTY_TRANSFER, EdgeResolver, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PAGE_INSERT_BLANK_MAX_COUNT, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, SIGNATURE_POLICY_VERSION, UNKNOWN_WEAK_ANNOTATION_STATE, USAGE_INCOMPLETE, appearanceImpactOf, assessmentOf, buildPageTextLayout, buildSnippet, charMapViolation, charRangeForTextOffsets, combine, decodeSubmitFormFlags, deriveProtection, evaluateStep, fieldLockFor, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, parsePdfValue, pdfQuadBounds, restrictionsFor, revisionTokensEqual, richTextParagraphsFromPlainText, richTextPlainText, sameEffectiveValue, 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();
@@ -270,8 +270,108 @@ var SessionEventPublisher = class {
270
270
  }
271
271
  };
272
272
  //#endregion
273
+ //#region src/runtime/memory/scratch.ts
274
+ /**
275
+ * Allocate a scratch buffer, run `body` with it, and always free it.
276
+ *
277
+ * Every PDFium out-parameter call follows the same shape: malloc a
278
+ * buffer, hand its pointer to the native function, read the result back,
279
+ * free the buffer. Hand-rolling that `try/finally` at each call site is
280
+ * how leaks and copy-paste bugs creep in, so it lives here once.
281
+ */
282
+ function withScratch(mem, bytes, body) {
283
+ const ptr = mem.alloc(bytes);
284
+ try {
285
+ return body(ptr);
286
+ } finally {
287
+ mem.free(ptr);
288
+ }
289
+ }
290
+ /**
291
+ * Like {@link withScratch} but for native calls that take several
292
+ * out-parameters at once (e.g. `FPDFAnnot_GetColor` with one pointer per
293
+ * channel). Buffers are freed in reverse allocation order.
294
+ */
295
+ function withScratchN(mem, sizes, body) {
296
+ const ptrs = sizes.map((size) => mem.alloc(size));
297
+ try {
298
+ return body(ptrs);
299
+ } finally {
300
+ for (let i = ptrs.length - 1; i >= 0; i--) mem.free(ptrs[i]);
301
+ }
302
+ }
303
+ //#endregion
304
+ //#region src/runtime/memory/strings.ts
305
+ /**
306
+ * Read a PDFium UTF-16LE string getter that uses the standard
307
+ * (probe length, allocate, read) ABI. `call(buf, capacity)` must invoke
308
+ * the native function and return the length in BYTES including the
309
+ * trailing NUL (the value PDFium reports when `capacity` is 0).
310
+ *
311
+ * `emptyAs` decides what a present-but-empty value (`len === 2`, a lone
312
+ * UTF-16 NUL) decodes to. This is the one knob different callers actually
313
+ * disagree on: document Info strings treat `''` as a real value, whereas
314
+ * a page label of `''` is indistinguishable from "absent" and reads as
315
+ * `null`. Making it a parameter keeps that decision explicit instead of
316
+ * forking the buffer dance.
317
+ *
318
+ * Returns `null` when the value is absent (`len <= 0`).
319
+ */
320
+ function readUtf16String(mem, call, emptyAs = "") {
321
+ const len = call(NULL_PTR, 0);
322
+ if (len <= 0) return null;
323
+ if (len === 2) return emptyAs;
324
+ return withScratch(mem, len, (buf) => {
325
+ if (call(buf, len) <= 0) return null;
326
+ return mem.readU16String(buf);
327
+ });
328
+ }
329
+ /**
330
+ * UTF-8 sibling of {@link readUtf16String} for the handful of native
331
+ * getters that return UTF-8 (e.g. `EPDF_GetMetaKeyName`). UTF-8 has no
332
+ * fixed empty-NUL width to special-case, so an empty value decodes to
333
+ * `''`; absent (`len <= 0`) is `null`.
334
+ */
335
+ function readUtf8String(mem, call) {
336
+ const len = call(NULL_PTR, 0);
337
+ if (len <= 0) return null;
338
+ return withScratch(mem, len, (buf) => {
339
+ if (call(buf, len) <= 0) return null;
340
+ return mem.readU8String(buf);
341
+ });
342
+ }
343
+ /**
344
+ * Write side of the UTF-16 convention: encode `value` into a scratch
345
+ * buffer, hand the pointer to `set`, and always free it. `set` returns
346
+ * the native call's success flag, which is propagated.
347
+ */
348
+ function writeUtf16String(mem, value, set) {
349
+ const ptr = mem.writeU16String(value);
350
+ try {
351
+ return set(ptr);
352
+ } finally {
353
+ mem.free(ptr);
354
+ }
355
+ }
356
+ /**
357
+ * A NUL-terminated UTF-8 copy of `value` in runtime memory for the duration
358
+ * of `fn` (a `FPDF_STRING` parameter: a file system path, say).
359
+ */
360
+ function withUtf8CString(mem, value, fn) {
361
+ const encoded = new TextEncoder().encode(value);
362
+ const bytes = new Uint8Array(encoded.byteLength + 1);
363
+ bytes.set(encoded);
364
+ const ptr = mem.alloc(bytes.byteLength);
365
+ try {
366
+ mem.writeBytes(ptr, bytes);
367
+ return fn(ptr);
368
+ } finally {
369
+ mem.free(ptr);
370
+ }
371
+ }
372
+ //#endregion
273
373
  //#region src/document-session/lifecycle/PdfDocumentOpener.ts
274
- const FPDF_ERR_PASSWORD$1 = 4;
374
+ const FPDF_ERR_PASSWORD$2 = 4;
275
375
  var CloseStack = class {
276
376
  constructor() {
277
377
  _defineProperty(this, "actions", []);
@@ -306,7 +406,7 @@ function openFatMemoryDocument(runtime, bytes, password = null) {
306
406
  mem.writeBytes(dataPtr, bytes);
307
407
  const docPtr = fn.FPDF_LoadMemDocument64(dataPtr, bytes.byteLength, password ?? "");
308
408
  if (!docPtr) {
309
- if (fn.FPDF_GetLastError() === FPDF_ERR_PASSWORD$1) throw password ? new EngineError(EngineErrorCode.DocPasswordIncorrect, "incorrect document password") : new EngineError(EngineErrorCode.DocPasswordRequired, "document requires a password");
409
+ if (fn.FPDF_GetLastError() === FPDF_ERR_PASSWORD$2) throw password ? new EngineError(EngineErrorCode.DocPasswordIncorrect, "incorrect document password") : new EngineError(EngineErrorCode.DocPasswordRequired, "document requires a password");
310
410
  throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to open document");
311
411
  }
312
412
  setRuntimeOwnerPermissionsIfEncrypted(runtime, docPtr);
@@ -314,6 +414,11 @@ function openFatMemoryDocument(runtime, bytes, password = null) {
314
414
  return {
315
415
  kind: "fat-memory",
316
416
  docPtr,
417
+ source: {
418
+ base: null,
419
+ layer: null,
420
+ password
421
+ },
317
422
  close: () => stack.close()
318
423
  };
319
424
  } catch (error) {
@@ -331,6 +436,7 @@ function openLayerDocument(runtime, base, layer = { kind: "fresh" }, password =
331
436
  mem.poke(statusPtr, "i32", -1);
332
437
  let docPtr;
333
438
  if (layer.kind === "fresh") docPtr = fn.EPDFLayer_OpenLayer(base.basePtr, NULL_PTR, password ?? "", statusPtr);
439
+ else if (layer.kind === "artifact-file" && runtime.kind === "native") docPtr = withUtf8CString(mem, layer.path, (pathPtr) => fn.EPDFLayer_OpenLayerArtifactFromPath(base.basePtr, pathPtr, password ?? "", statusPtr));
334
440
  else {
335
441
  layerAccess = layer.kind === "artifact-file" ? runtime.fileAccess.fromNodeFile(layer.path) : runtime.fileAccess.fromMemory(layer.bytes);
336
442
  stack.push(() => layerAccess?.close());
@@ -340,9 +446,22 @@ function openLayerDocument(runtime, base, layer = { kind: "fresh" }, password =
340
446
  if (!docPtr || status !== 0) throw layerOpenError(status);
341
447
  setRuntimeOwnerPermissionsIfEncrypted(runtime, docPtr);
342
448
  stack.push(() => fn.FPDF_CloseDocument(docPtr));
449
+ const baseSource = base.kind === "file" && base.path !== void 0 ? {
450
+ kind: "file",
451
+ key: base.key,
452
+ path: base.path
453
+ } : {
454
+ kind: "memory",
455
+ key: base.key
456
+ };
343
457
  return {
344
458
  kind: "layer",
345
459
  docPtr,
460
+ source: {
461
+ base: baseSource,
462
+ layer,
463
+ password
464
+ },
346
465
  close: () => stack.close()
347
466
  };
348
467
  } catch (error) {
@@ -495,22 +614,13 @@ var LocalRevisionAuthority = class {
495
614
  };
496
615
  //#endregion
497
616
  //#region src/document-session/DocumentSession.ts
498
- /**
499
- * Owns the lifecycle of a single open PDFium document and the v3
500
- * identity machinery: page registry (pageObjectNumber <-> pageIndex),
501
- * `RevisionAuthority` (per-page generation counters), and `PagePtrPool`
502
- * (refcounted pagePtr access).
503
- *
504
- * Both the local browser Worker and the server worker_thread instantiate
505
- * this exactly the same way; the only thing that differs is the
506
- * underlying PdfRuntimeModule (WASM vs native).
507
- */
508
617
  var DocumentSession = class {
509
618
  constructor(runtime, sessionId) {
510
619
  this.runtime = runtime;
511
620
  _defineProperty(this, "docPtr", null);
512
621
  _defineProperty(this, "closeDocument", null);
513
622
  _defineProperty(this, "_kind", null);
623
+ _defineProperty(this, "_source", null);
514
624
  _defineProperty(this, "_sessionId", void 0);
515
625
  _defineProperty(this, "recordsByObjectNumber", /* @__PURE__ */ new Map());
516
626
  _defineProperty(this, "recordsByIndex", /* @__PURE__ */ new Map());
@@ -518,6 +628,59 @@ var DocumentSession = class {
518
628
  _defineProperty(this, "revisions", null);
519
629
  _defineProperty(this, "mutationSeqCounter", 0);
520
630
  _defineProperty(this, "pages", null);
631
+ _defineProperty(
632
+ this,
633
+ /**
634
+ * Whether writers honour what the document's signatures forbid (locked
635
+ * fields, structural edits). Set at open from the engine option; the
636
+ * main-thread guard subtracts the matching capabilities.
637
+ */
638
+ "signedDocumentPolicy",
639
+ "protect"
640
+ );
641
+ _defineProperty(
642
+ this,
643
+ /**
644
+ * The shape the caller asked for when opening plain bytes. `layer` (the
645
+ * default) opens them as an immutable base with a fresh layer; `plain`
646
+ * keeps one in-memory document unless the bytes already carry a
647
+ * signature, which always opens as a layer.
648
+ */
649
+ "sessionKind",
650
+ "layer"
651
+ );
652
+ _defineProperty(
653
+ this,
654
+ /** The password the document was opened with; signing candidates open with the same one. */
655
+ "password",
656
+ null
657
+ );
658
+ _defineProperty(
659
+ this,
660
+ /**
661
+ * Whether mutations on a layer session also serialize the layer artifact
662
+ * into their response (what a server persists). False for a session that
663
+ * became a layer only because its document is signed (see
664
+ * `WorkerHost.openSignedAware`): the caller opened plain bytes and never
665
+ * asked for artifacts.
666
+ */
667
+ "persistLayerArtifact",
668
+ true
669
+ );
670
+ _defineProperty(this, "loadedSeq", 0);
671
+ _defineProperty(this, "plainSha256", null);
672
+ _defineProperty(
673
+ this,
674
+ /** The signing candidate parked by `signatures.prepare`, if any. */
675
+ "pendingSigning",
676
+ null
677
+ );
678
+ _defineProperty(
679
+ this,
680
+ /** The last completed signing, so a replayed `complete` answers `already-completed`. */
681
+ "lastCompletion",
682
+ null
683
+ );
521
684
  _defineProperty(this, "parkedBytes", null);
522
685
  _defineProperty(this, "retained", []);
523
686
  this._sessionId = sessionId ?? generateSessionId();
@@ -528,6 +691,15 @@ var DocumentSession = class {
528
691
  get kind() {
529
692
  return this._kind;
530
693
  }
694
+ /**
695
+ * Where the current bytes come from: the immutable base (registry key,
696
+ * file path for file bases) and the layer the session was opened with.
697
+ * One interpretation for signing candidates, overlays and verbatim reads.
698
+ */
699
+ get source() {
700
+ if (!this._source) throw new EngineError(EngineErrorCode.DocNotOpen, "document is not open");
701
+ return this._source;
702
+ }
531
703
  isOpen() {
532
704
  return this.docPtr !== null;
533
705
  }
@@ -542,9 +714,76 @@ var DocumentSession = class {
542
714
  this.docPtr = handle.docPtr;
543
715
  this.closeDocument = () => handle.close();
544
716
  this._kind = handle.kind;
717
+ this._source = handle.source;
545
718
  this.revisions = new LocalRevisionAuthority(this._sessionId);
546
719
  this.pages = new PagePtrPool(this.runtime, handle.docPtr);
547
720
  this.parkedBytes = null;
721
+ this.loadedSeq = this.mutationSeqCounter;
722
+ }
723
+ /**
724
+ * Whether anything was mutated since the current bytes were loaded. When
725
+ * false, the loaded bytes ARE the document: a save returns them verbatim
726
+ * and a signing candidate is built straight on them.
727
+ */
728
+ hasUnsavedEdits() {
729
+ return this.mutationSeqCounter !== this.loadedSeq;
730
+ }
731
+ /** Cached hash of the current loaded bytes, or `null` when not computed since the last load. */
732
+ cachedPlainSha256() {
733
+ return this.plainSha256 && this.plainSha256.loadedSeq === this.loadedSeq ? this.plainSha256.sha256 : null;
734
+ }
735
+ rememberPlainSha256(sha256) {
736
+ this.plainSha256 = {
737
+ loadedSeq: this.loadedSeq,
738
+ sha256
739
+ };
740
+ }
741
+ /**
742
+ * Re-point this session at new bytes — the one operation that changes
743
+ * what an open session is backed by, used when a completed signature
744
+ * installs its sealed file. The session id, its page object numbers
745
+ * (an incremental save never renumbers) and its retained resources
746
+ * survive; the old document is closed, every page is re-pinned (its
747
+ * revision bumped, its cached pointer dropped), and the mutation
748
+ * sequence advances so every version-keyed cache rebuilds.
749
+ */
750
+ install(handle) {
751
+ if (!this.docPtr) {
752
+ handle.close();
753
+ throw new EngineError(EngineErrorCode.DocNotOpen, "no document to replace");
754
+ }
755
+ const previousPages = Array.from(this.recordsByObjectNumber.keys());
756
+ let firstError = null;
757
+ try {
758
+ this.pages?.closeAll();
759
+ } catch (error) {
760
+ firstError = error;
761
+ }
762
+ try {
763
+ this.closeDocument?.();
764
+ } catch (error) {
765
+ firstError ?? (firstError = error);
766
+ }
767
+ this.docPtr = handle.docPtr;
768
+ this.closeDocument = () => handle.close();
769
+ this._kind = handle.kind;
770
+ this._source = handle.source;
771
+ this.pages = new PagePtrPool(this.runtime, handle.docPtr);
772
+ this.recordsByIndex.clear();
773
+ this.recordsByObjectNumber.clear();
774
+ this.fullyEnumerated = false;
775
+ for (const pon of previousPages) this.requireRevisions().bump(pon);
776
+ this.mutationSeqCounter++;
777
+ this.loadedSeq = this.mutationSeqCounter;
778
+ this.pendingSigning = null;
779
+ if (firstError) throw firstError;
780
+ }
781
+ /** The two publish fences a candidate is built on. */
782
+ versionRef(baseSha256) {
783
+ return {
784
+ baseSha256,
785
+ editsVersion: this.mutationSeqCounter
786
+ };
548
787
  }
549
788
  /**
550
789
  * Park this session in the password-locked state: the document could not
@@ -704,6 +943,11 @@ var DocumentSession = class {
704
943
  } finally {
705
944
  this.pages = null;
706
945
  }
946
+ if (this.pendingSigning?.saved.kind === "file") try {
947
+ this.runtime.fileWrite.removeFile(this.pendingSigning.saved.path);
948
+ } catch (error) {
949
+ firstError ?? (firstError = error);
950
+ }
707
951
  try {
708
952
  this.closeDocument?.();
709
953
  } catch (error) {
@@ -712,7 +956,10 @@ var DocumentSession = class {
712
956
  this.closeDocument = null;
713
957
  this.docPtr = null;
714
958
  this._kind = null;
959
+ this._source = null;
715
960
  this.parkedBytes = null;
961
+ this.pendingSigning = null;
962
+ this.lastCompletion = null;
716
963
  this.revisions?.clear();
717
964
  this.revisions = null;
718
965
  this.recordsByIndex.clear();
@@ -738,6 +985,17 @@ function generateSessionId() {
738
985
  }
739
986
  //#endregion
740
987
  //#region src/document-session/lifecycle/BaseDocumentRegistry.ts
988
+ const FPDF_ERR_PASSWORD$1 = 4;
989
+ /**
990
+ * A password failure is a recoverable STATE the caller can act on (prompt,
991
+ * unlock), never a generic open failure — the same distinction
992
+ * `openFatMemoryDocument` draws, so a plain-bytes open that becomes a base
993
+ * parks and unlocks exactly like it always did.
994
+ */
995
+ function baseOpenError(runtime, password) {
996
+ if (runtime.fn.FPDF_GetLastError() === FPDF_ERR_PASSWORD$1) return password ? new EngineError(EngineErrorCode.DocPasswordIncorrect, "incorrect document password") : new EngineError(EngineErrorCode.DocPasswordRequired, "document requires a password");
997
+ return new EngineError(EngineErrorCode.DocOpenFailed, "failed to open base document");
998
+ }
741
999
  var BaseDocumentRegistry = class {
742
1000
  constructor(runtime) {
743
1001
  this.runtime = runtime;
@@ -753,10 +1011,17 @@ var BaseDocumentRegistry = class {
753
1011
  try {
754
1012
  mem.writeBytes(dataPtr, opts.bytes);
755
1013
  const basePtr = fn.EPDF_LoadMemBaseDocument64(dataPtr, opts.bytes.byteLength, opts.password ?? "");
756
- if (!basePtr) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to open base document");
1014
+ if (!basePtr) throw baseOpenError(this.runtime, opts.password);
757
1015
  setRuntimeOwnerPermissionsIfEncrypted(this.runtime, basePtr);
758
1016
  stack.push(() => fn.EPDF_ReleaseBaseDocument(basePtr));
759
- return this.insert(opts.key, basePtr, () => stack.close());
1017
+ this.supplyKnownSha(basePtr, opts.knownSha256);
1018
+ return this.insert({
1019
+ key: opts.key,
1020
+ kind: "memory",
1021
+ basePtr,
1022
+ refs: 1,
1023
+ close: () => stack.close()
1024
+ });
760
1025
  } catch (error) {
761
1026
  stack.close();
762
1027
  throw error;
@@ -772,15 +1037,50 @@ var BaseDocumentRegistry = class {
772
1037
  access = this.runtime.fileAccess.fromNodeFile(opts.path);
773
1038
  stack.push(() => access?.close());
774
1039
  const basePtr = fn.EPDF_LoadBaseDocument(access.ptr, opts.password ?? "");
775
- if (!basePtr) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to open base document");
1040
+ if (!basePtr) throw baseOpenError(this.runtime, opts.password);
776
1041
  setRuntimeOwnerPermissionsIfEncrypted(this.runtime, basePtr);
777
1042
  stack.push(() => fn.EPDF_ReleaseBaseDocument(basePtr));
778
- return this.insert(opts.key, basePtr, () => stack.close());
1043
+ this.supplyKnownSha(basePtr, opts.knownSha256);
1044
+ return this.insert({
1045
+ key: opts.key,
1046
+ kind: "file",
1047
+ path: opts.path,
1048
+ basePtr,
1049
+ refs: 1,
1050
+ close: () => stack.close()
1051
+ });
779
1052
  } catch (error) {
780
1053
  stack.close();
781
1054
  throw error;
782
1055
  }
783
1056
  }
1057
+ /**
1058
+ * One more retain on a base already in the registry (a session holds one
1059
+ * for its lifetime, so its key is live while it is open), or null. The
1060
+ * way a signing candidate reopens over its session's own base without
1061
+ * copying a byte.
1062
+ */
1063
+ retainByKey(key) {
1064
+ return this.retain(key);
1065
+ }
1066
+ /**
1067
+ * Hand the runtime a hash the host already verified for these exact
1068
+ * bytes, so it never hashes them itself. Malformed values are ignored:
1069
+ * the runtime then hashes lazily on first use, which is always correct.
1070
+ */
1071
+ supplyKnownSha(basePtr, sha256Hex) {
1072
+ if (!sha256Hex || !/^[0-9a-fA-F]{64}$/.test(sha256Hex)) return;
1073
+ const { mem, fn } = this.runtime;
1074
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
1075
+ for (let i = 0; i < 32; i++) bytes[i] = parseInt(sha256Hex.slice(i * 2, i * 2 + 2), 16);
1076
+ const ptr = mem.alloc(32);
1077
+ try {
1078
+ mem.writeBytes(ptr, bytes);
1079
+ fn.EPDF_SetBaseDocumentSha256(basePtr, ptr);
1080
+ } finally {
1081
+ mem.free(ptr);
1082
+ }
1083
+ }
784
1084
  releaseAll() {
785
1085
  const entries = Array.from(this.entries.values());
786
1086
  this.entries.clear();
@@ -802,20 +1102,16 @@ var BaseDocumentRegistry = class {
802
1102
  entry.refs++;
803
1103
  return this.handleFor(entry);
804
1104
  }
805
- insert(key, basePtr, close) {
806
- const entry = {
807
- key,
808
- basePtr,
809
- refs: 1,
810
- close
811
- };
812
- this.entries.set(key, entry);
1105
+ insert(entry) {
1106
+ this.entries.set(entry.key, entry);
813
1107
  return this.handleFor(entry);
814
1108
  }
815
1109
  handleFor(entry) {
816
1110
  let released = false;
817
1111
  return {
818
1112
  key: entry.key,
1113
+ kind: entry.kind,
1114
+ ...entry.path !== void 0 ? { path: entry.path } : {},
819
1115
  basePtr: entry.basePtr,
820
1116
  release: () => {
821
1117
  if (released) return;
@@ -831,90 +1127,6 @@ var BaseDocumentRegistry = class {
831
1127
  }
832
1128
  };
833
1129
  //#endregion
834
- //#region src/runtime/memory/scratch.ts
835
- /**
836
- * Allocate a scratch buffer, run `body` with it, and always free it.
837
- *
838
- * Every PDFium out-parameter call follows the same shape: malloc a
839
- * buffer, hand its pointer to the native function, read the result back,
840
- * free the buffer. Hand-rolling that `try/finally` at each call site is
841
- * how leaks and copy-paste bugs creep in, so it lives here once.
842
- */
843
- function withScratch(mem, bytes, body) {
844
- const ptr = mem.alloc(bytes);
845
- try {
846
- return body(ptr);
847
- } finally {
848
- mem.free(ptr);
849
- }
850
- }
851
- /**
852
- * Like {@link withScratch} but for native calls that take several
853
- * out-parameters at once (e.g. `FPDFAnnot_GetColor` with one pointer per
854
- * channel). Buffers are freed in reverse allocation order.
855
- */
856
- function withScratchN(mem, sizes, body) {
857
- const ptrs = sizes.map((size) => mem.alloc(size));
858
- try {
859
- return body(ptrs);
860
- } finally {
861
- for (let i = ptrs.length - 1; i >= 0; i--) mem.free(ptrs[i]);
862
- }
863
- }
864
- //#endregion
865
- //#region src/runtime/memory/strings.ts
866
- /**
867
- * Read a PDFium UTF-16LE string getter that uses the standard
868
- * (probe length, allocate, read) ABI. `call(buf, capacity)` must invoke
869
- * the native function and return the length in BYTES including the
870
- * trailing NUL (the value PDFium reports when `capacity` is 0).
871
- *
872
- * `emptyAs` decides what a present-but-empty value (`len === 2`, a lone
873
- * UTF-16 NUL) decodes to. This is the one knob different callers actually
874
- * disagree on: document Info strings treat `''` as a real value, whereas
875
- * a page label of `''` is indistinguishable from "absent" and reads as
876
- * `null`. Making it a parameter keeps that decision explicit instead of
877
- * forking the buffer dance.
878
- *
879
- * Returns `null` when the value is absent (`len <= 0`).
880
- */
881
- function readUtf16String(mem, call, emptyAs = "") {
882
- const len = call(NULL_PTR, 0);
883
- if (len <= 0) return null;
884
- if (len === 2) return emptyAs;
885
- return withScratch(mem, len, (buf) => {
886
- if (call(buf, len) <= 0) return null;
887
- return mem.readU16String(buf);
888
- });
889
- }
890
- /**
891
- * UTF-8 sibling of {@link readUtf16String} for the handful of native
892
- * getters that return UTF-8 (e.g. `EPDF_GetMetaKeyName`). UTF-8 has no
893
- * fixed empty-NUL width to special-case, so an empty value decodes to
894
- * `''`; absent (`len <= 0`) is `null`.
895
- */
896
- function readUtf8String(mem, call) {
897
- const len = call(NULL_PTR, 0);
898
- if (len <= 0) return null;
899
- return withScratch(mem, len, (buf) => {
900
- if (call(buf, len) <= 0) return null;
901
- return mem.readU8String(buf);
902
- });
903
- }
904
- /**
905
- * Write side of the UTF-16 convention: encode `value` into a scratch
906
- * buffer, hand the pointer to `set`, and always free it. `set` returns
907
- * the native call's success flag, which is propagated.
908
- */
909
- function writeUtf16String(mem, value, set) {
910
- const ptr = mem.writeU16String(value);
911
- try {
912
- return set(ptr);
913
- } finally {
914
- mem.free(ptr);
915
- }
916
- }
917
- //#endregion
918
1130
  //#region src/features/metadata/internal/readMetadataText.ts
919
1131
  /**
920
1132
  * Read a key from the document's Info dictionary. Returns null if the key
@@ -1731,7 +1943,8 @@ var PagesReader = class {
1731
1943
  });
1732
1944
  return {
1733
1945
  pageCount: pages.length,
1734
- pages
1946
+ pages,
1947
+ namedPages: readNamedPages(fn, mem, docPtr)
1735
1948
  };
1736
1949
  });
1737
1950
  }
@@ -1796,6 +2009,41 @@ function readUserUnit(fn, mem, docPtr, index, userUnitPtr) {
1796
2009
  function readLabel(fn, mem, docPtr, index) {
1797
2010
  return readUtf16String(mem, (buf, capacity) => fn.FPDF_GetPageLabel(docPtr, index, buf, capacity), null) || null;
1798
2011
  }
2012
+ const NAMED_PAGE_TREES = [0, 1];
2013
+ const NAMED_PAGE_KIND_PAGE = 0;
2014
+ const NAMED_PAGE_KIND_TEMPLATE = 1;
2015
+ /**
2016
+ * The catalog's `/Names /Pages` and `/Names /Templates` registrations, in
2017
+ * tree order, each value classified by the fork (page / template /
2018
+ * dangling). Page-identity data like `label`, so it ships inside the same
2019
+ * snapshot — a registration is only meaningful against the page set that
2020
+ * contains its target.
2021
+ */
2022
+ function readNamedPages(fn, mem, docPtr) {
2023
+ const entries = [];
2024
+ withScratchN(mem, [4, 4], ([objNumPtr, kindPtr]) => {
2025
+ for (const tree of NAMED_PAGE_TREES) {
2026
+ const count = fn.EPDFDoc_GetNamedPageCount(docPtr, tree);
2027
+ for (let index = 0; index < count; index++) {
2028
+ const name = readUtf16String(mem, (buf, capacity) => fn.EPDFDoc_GetNamedPageAt(docPtr, tree, index, buf, capacity, objNumPtr, kindPtr), "");
2029
+ if (name === null) continue;
2030
+ const objectNumber = Number(mem.peek(objNumPtr, "i32")) >>> 0;
2031
+ const kind = Number(mem.peek(kindPtr, "i32"));
2032
+ entries.push({
2033
+ name,
2034
+ target: kind === NAMED_PAGE_KIND_PAGE ? {
2035
+ kind: "page",
2036
+ pageObjectNumber: objectNumber
2037
+ } : kind === NAMED_PAGE_KIND_TEMPLATE ? {
2038
+ kind: "template",
2039
+ objectNumber
2040
+ } : { kind: "dangling" }
2041
+ });
2042
+ }
2043
+ }
2044
+ });
2045
+ return entries;
2046
+ }
1799
2047
  //#endregion
1800
2048
  //#region src/features/pages/PagesMutator.ts
1801
2049
  /**
@@ -1943,19 +2191,51 @@ var PagesMutator = class {
1943
2191
  cache: null
1944
2192
  };
1945
2193
  }
1946
- /** Shared input check: non-empty, no duplicate PONs. */
1947
- requireUniquePons(op, pageObjectNumbers) {
1948
- if (pageObjectNumbers.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, `${op} requires at least one page`);
1949
- const seen = /* @__PURE__ */ new Set();
1950
- for (const pon of pageObjectNumbers) {
1951
- if (seen.has(pon)) throw new EngineError(EngineErrorCode.InvalidArg, `${op} was given duplicate page object number ${pon}`);
1952
- seen.add(pon);
1953
- }
2194
+ /**
2195
+ * Register `name` → page in `/Names /Pages` (create, or replace what the
2196
+ * key points at); with `replace`, drop that other key first — a rename as
2197
+ * one job. Named pages are LAYOUT: page identity and order are untouched
2198
+ * (no registry refresh, no revision bumps) and the fresh snapshot is
2199
+ * returned like `move()`.
2200
+ */
2201
+ setName(input, signal) {
2202
+ throwIfAborted(signal);
2203
+ if (input.name.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "pages.setName requires a non-empty name");
2204
+ const { fn, mem } = this.runtime;
2205
+ const docPtr = this.session.requireDocPtr();
2206
+ this.session.recordByObjectNumber(input.pageObjectNumber);
2207
+ if (input.replace !== void 0 && input.replace !== input.name && input.replace.length > 0) writeUtf16String(mem, input.replace, (ptr) => fn.EPDFDoc_RemoveNamedPage(docPtr, ptr));
2208
+ if (!writeUtf16String(mem, input.name, (ptr) => fn.EPDFDoc_SetNamedPage(docPtr, ptr, input.pageObjectNumber))) throw new EngineError(EngineErrorCode.Unknown, `EPDFDoc_SetNamedPage rejected '${input.name}' for page ${input.pageObjectNumber}`);
2209
+ return {
2210
+ layout: new PagesReader(this.runtime, this.session).read(signal),
2211
+ cache: null
2212
+ };
2213
+ }
2214
+ /** Remove one `/Names /Pages` registration; the page stays. */
2215
+ removeName(input, signal) {
2216
+ throwIfAborted(signal);
2217
+ if (input.name.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "pages.removeName requires a non-empty name");
2218
+ const { fn, mem } = this.runtime;
2219
+ const docPtr = this.session.requireDocPtr();
2220
+ if (!writeUtf16String(mem, input.name, (ptr) => fn.EPDFDoc_RemoveNamedPage(docPtr, ptr))) throw new EngineError(EngineErrorCode.NotFound, `pages.removeName: no /Names /Pages registration '${input.name}'`);
2221
+ return {
2222
+ layout: new PagesReader(this.runtime, this.session).read(signal),
2223
+ cache: null
2224
+ };
2225
+ }
2226
+ /** Shared input check: non-empty, no duplicate PONs. */
2227
+ requireUniquePons(op, pageObjectNumbers) {
2228
+ if (pageObjectNumbers.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, `${op} requires at least one page`);
2229
+ const seen = /* @__PURE__ */ new Set();
2230
+ for (const pon of pageObjectNumbers) {
2231
+ if (seen.has(pon)) throw new EngineError(EngineErrorCode.InvalidArg, `${op} was given duplicate page object number ${pon}`);
2232
+ seen.add(pon);
2233
+ }
1954
2234
  }
1955
2235
  };
1956
2236
  //#endregion
1957
2237
  //#region src/features/pages/PagesExtractor.ts
1958
- const FPDF_NO_INCREMENTAL$1 = 2;
2238
+ const FPDF_NO_INCREMENTAL$2 = 2;
1959
2239
  /**
1960
2240
  * Export a subset of pages as a standalone PDF. A READ over the session:
1961
2241
  * `FPDF_ImportPagesByIndex` copies pages into a scratch document, so the
@@ -1994,7 +2274,7 @@ var PagesExtractor = class {
1994
2274
  for (let i = 0; i < indices.length; i++) mem.poke(idxPtr, "i32", indices[i], 4 * i);
1995
2275
  if (!fn.FPDF_ImportPagesByIndex(destPtr, srcPtr, idxPtr, indices.length, 0)) throw new EngineError(EngineErrorCode.Unknown, `FPDF_ImportPagesByIndex rejected the request (indices=[${indices.join(",")}])`);
1996
2276
  mem.poke(sizePtr, "i32", 0);
1997
- pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(destPtr, FPDF_NO_INCREMENTAL$1, sizePtr);
2277
+ pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(destPtr, FPDF_NO_INCREMENTAL$2, sizePtr);
1998
2278
  const size = Number(mem.peek(sizePtr, "i32"));
1999
2279
  if (!pdfPtr || size <= 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save extracted pages");
2000
2280
  const bytes = mem.readBytes(pdfPtr, size);
@@ -2103,8 +2383,11 @@ var PagesInserter = class {
2103
2383
  //#endregion
2104
2384
  //#region src/features/annotations/internal/read/annotationReadContext.ts
2105
2385
  /** Build the read context both read paths (list loop, mutator read-back) use. */
2106
- function readContextFor(session) {
2107
- return { docPtr: session.requireDocPtr() };
2386
+ function readContextFor(session, fonts) {
2387
+ return {
2388
+ docPtr: session.requireDocPtr(),
2389
+ ...fonts ? { fonts } : {}
2390
+ };
2108
2391
  }
2109
2392
  //#endregion
2110
2393
  //#region src/features/annotations/internal/annotationFlagBits.ts
@@ -2546,6 +2829,13 @@ function readQuadPoints(fn, mem, annotPtr) {
2546
2829
  return out;
2547
2830
  });
2548
2831
  }
2832
+ /**
2833
+ * `/Name` as text, whatever its value (`EPDFAnnot_GetName`): a standard
2834
+ * icon/stamp name or a custom identifier. `null` when absent.
2835
+ */
2836
+ function readAnnotName(fn, mem, annotPtr) {
2837
+ return readUtf8String(mem, (buf, capacity) => fn.EPDFAnnot_GetName(annotPtr, buf, capacity));
2838
+ }
2549
2839
  //#endregion
2550
2840
  //#region src/features/annotations/internal/read/readAnnotationTransformMetadata.ts
2551
2841
  /**
@@ -2795,29 +3085,29 @@ function normalizeChecksum(raw) {
2795
3085
  //#endregion
2796
3086
  //#region src/features/annotations/internal/annotationIcon.ts
2797
3087
  /**
2798
- * `/Name` icon ↔ `FPDF_ANNOT_NAME` enum code mapping for text and
2799
- * file-attachment annotations (`EPDFAnnot_SetName` / `EPDFAnnot_GetName`).
2800
- * Codes mirror the `FPDF_ANNOT_NAME` enum in `public/fpdf_annot.h` —
2801
- * same pattern as `stampName.ts` (whose stamp block starts at 13, right
2802
- * after these) keeping engine-core PDFium-free.
2803
- */
2804
- const NOTE_ICON_TO_CODE = Object.freeze({
2805
- comment: 0,
2806
- key: 1,
2807
- note: 2,
2808
- help: 3,
2809
- "new-paragraph": 4,
2810
- paragraph: 5,
2811
- insert: 6
3088
+ * `/Name` icon vocabulary ↔ PDF name mapping for text and file-attachment
3089
+ * annotations (ISO 32000-2 tables 175 and 184). The wire speaks the
3090
+ * kebab-case ids; `EPDFAnnot_SetName` / `EPDFAnnot_GetName` speak the PDF
3091
+ * names. An unknown PDF name reads as the subtype's default (the reader
3092
+ * draws the default glyph for names it does not know — spec behavior).
3093
+ */
3094
+ const NOTE_ICON_TO_NAME = Object.freeze({
3095
+ comment: "Comment",
3096
+ key: "Key",
3097
+ note: "Note",
3098
+ help: "Help",
3099
+ "new-paragraph": "NewParagraph",
3100
+ paragraph: "Paragraph",
3101
+ insert: "Insert"
2812
3102
  });
2813
- const NOTE_CODE_TO_ICON = Object.freeze(Object.fromEntries(Object.entries(NOTE_ICON_TO_CODE).map(([icon, code]) => [code, icon])));
2814
- const FILE_ICON_TO_CODE = Object.freeze({
2815
- graph: 7,
2816
- "push-pin": 8,
2817
- paperclip: 9,
2818
- tag: 10
3103
+ const NOTE_NAME_TO_ICON = Object.freeze(Object.fromEntries(Object.entries(NOTE_ICON_TO_NAME).map(([icon, name]) => [name, icon])));
3104
+ const FILE_ICON_TO_NAME = Object.freeze({
3105
+ graph: "Graph",
3106
+ "push-pin": "PushPin",
3107
+ paperclip: "Paperclip",
3108
+ tag: "Tag"
2819
3109
  });
2820
- const FILE_CODE_TO_ICON = Object.freeze(Object.fromEntries(Object.entries(FILE_ICON_TO_CODE).map(([icon, code]) => [code, icon])));
3110
+ const FILE_NAME_TO_ICON = Object.freeze(Object.fromEntries(Object.entries(FILE_ICON_TO_NAME).map(([icon, name]) => [name, icon])));
2821
3111
  //#endregion
2822
3112
  //#region src/features/annotations/internal/read/readFileAttachmentAnnotation.ts
2823
3113
  /** Default `/C` — matches the generator's default icon fill and the writer default. */
@@ -2839,7 +3129,7 @@ function readFileAttachment(fn, mem, annotPtr, base) {
2839
3129
  const color = readAnnotColor(fn, mem, annotPtr) ?? { ...DEFAULT_FILE_ATTACHMENT_COLOR$1 };
2840
3130
  const ca = readAnnotOpacity(fn, mem, annotPtr);
2841
3131
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
2842
- const icon = FILE_CODE_TO_ICON[fn.EPDFAnnot_GetName(annotPtr)] ?? DEFAULT_FILE_ATTACHMENT_ICON;
3132
+ const icon = FILE_NAME_TO_ICON[readAnnotName(fn, mem, annotPtr) ?? ""] ?? DEFAULT_FILE_ATTACHMENT_ICON;
2843
3133
  const attachmentPtr = fn.FPDFAnnot_GetFileAttachment(annotPtr);
2844
3134
  const file = attachmentPtr ? readAttachmentFileInfo(fn, mem, attachmentPtr) : { name: "" };
2845
3135
  return {
@@ -2938,6 +3228,206 @@ function standardFontFromCode(code) {
2938
3228
  return CODE_TO_FONT[code] ?? "helvetica";
2939
3229
  }
2940
3230
  //#endregion
3231
+ //#region src/features/annotations/internal/richTextWire.ts
3232
+ const STANDARD_FACES = {
3233
+ courier: {
3234
+ family: "Courier",
3235
+ weight: 400,
3236
+ italic: false
3237
+ },
3238
+ "courier-bold": {
3239
+ family: "Courier",
3240
+ weight: 700,
3241
+ italic: false
3242
+ },
3243
+ "courier-bold-oblique": {
3244
+ family: "Courier",
3245
+ weight: 700,
3246
+ italic: true
3247
+ },
3248
+ "courier-oblique": {
3249
+ family: "Courier",
3250
+ weight: 400,
3251
+ italic: true
3252
+ },
3253
+ helvetica: {
3254
+ family: "Helvetica",
3255
+ weight: 400,
3256
+ italic: false
3257
+ },
3258
+ "helvetica-bold": {
3259
+ family: "Helvetica",
3260
+ weight: 700,
3261
+ italic: false
3262
+ },
3263
+ "helvetica-bold-oblique": {
3264
+ family: "Helvetica",
3265
+ weight: 700,
3266
+ italic: true
3267
+ },
3268
+ "helvetica-oblique": {
3269
+ family: "Helvetica",
3270
+ weight: 400,
3271
+ italic: true
3272
+ },
3273
+ "times-roman": {
3274
+ family: "Times",
3275
+ weight: 400,
3276
+ italic: false
3277
+ },
3278
+ "times-bold": {
3279
+ family: "Times",
3280
+ weight: 700,
3281
+ italic: false
3282
+ },
3283
+ "times-bold-italic": {
3284
+ family: "Times",
3285
+ weight: 700,
3286
+ italic: true
3287
+ },
3288
+ "times-italic": {
3289
+ family: "Times",
3290
+ weight: 400,
3291
+ italic: true
3292
+ },
3293
+ symbol: {
3294
+ family: "Symbol",
3295
+ weight: 400,
3296
+ italic: false
3297
+ },
3298
+ "zapf-dingbats": {
3299
+ family: "ZapfDingbats",
3300
+ weight: 400,
3301
+ italic: false
3302
+ }
3303
+ };
3304
+ /** Family names compare without case, spaces, hyphens and underscores
3305
+ * (the engine's own rule): "Noto Sans" == "NotoSans" == "noto-sans". */
3306
+ function familyKey$1(family) {
3307
+ return family.toLowerCase().replace(/[\s\-_'"]/g, "");
3308
+ }
3309
+ const STANDARD_FAMILY_KEYS = {
3310
+ helvetica: "Helvetica",
3311
+ arial: "Helvetica",
3312
+ arialmt: "Helvetica",
3313
+ sansserif: "Helvetica",
3314
+ times: "Times",
3315
+ timesroman: "Times",
3316
+ timesnewroman: "Times",
3317
+ timesnewromanpsmt: "Times",
3318
+ serif: "Times",
3319
+ courier: "Courier",
3320
+ couriernew: "Courier",
3321
+ couriernewpsmt: "Courier",
3322
+ monospace: "Courier",
3323
+ symbol: "Symbol",
3324
+ zapfdingbats: "ZapfDingbats",
3325
+ dingbats: "ZapfDingbats"
3326
+ };
3327
+ /**
3328
+ * The face a DTO `fontFamily` (or a rich run's `family`) asks for: a
3329
+ * standard font's family/weight/italic, a registered key's resolved
3330
+ * identity, or — for anything else — the string itself as a family name
3331
+ * (a face the document embeds, or one the engine will substitute).
3332
+ */
3333
+ function faceForFreeTextFont(font, describe) {
3334
+ if (isStandardFont(font)) {
3335
+ const face = STANDARD_FACES[font];
3336
+ return {
3337
+ family: face.family,
3338
+ weight: face.weight,
3339
+ italic: face.italic
3340
+ };
3341
+ }
3342
+ const registered = describe?.(font);
3343
+ if (registered) return {
3344
+ family: registered.familyName,
3345
+ weight: registered.weight,
3346
+ italic: registered.italic
3347
+ };
3348
+ return { family: font };
3349
+ }
3350
+ /**
3351
+ * The DTO `fontFamily` for a body face: the registered `key` whose identity
3352
+ * matches, else the standard font's kebab name, else the family itself.
3353
+ */
3354
+ function freeTextFontForFace(face, keyForFace) {
3355
+ const key = keyForFace?.(face.family, face.weight, face.italic);
3356
+ if (key !== void 0) return key;
3357
+ const standard = STANDARD_FAMILY_KEYS[familyKey$1(face.family)];
3358
+ if (standard) {
3359
+ const bold = face.weight >= 600;
3360
+ for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard && spec.weight >= 600 === bold && spec.italic === face.italic) return name;
3361
+ for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard) return name;
3362
+ }
3363
+ return face.family;
3364
+ }
3365
+ /** A run style or body with its `family` (and the weight/italic a key or
3366
+ * standard name implies) rewritten to what the engine resolves by. */
3367
+ function withEngineFace(style, describe) {
3368
+ if (style.family === void 0) return style;
3369
+ const face = faceForFreeTextFont(style.family, describe);
3370
+ const out = {
3371
+ ...style,
3372
+ family: face.family
3373
+ };
3374
+ if (face.weight !== void 0 && style.weight === void 0) out.weight = face.weight;
3375
+ if (face.italic !== void 0 && style.italic === void 0) out.italic = face.italic;
3376
+ return out;
3377
+ }
3378
+ /** The engine's JSON for a write: the input shape with faces resolved. */
3379
+ function engineRichTextJson(input, describe) {
3380
+ const body = input.body ? withEngineFace(input.body, describe) : void 0;
3381
+ const paragraphs = input.paragraphs.map((p) => ({
3382
+ ...p,
3383
+ runs: p.runs.map((r) => r.style ? {
3384
+ ...r,
3385
+ style: withEngineFace(r.style, describe)
3386
+ } : r)
3387
+ }));
3388
+ return JSON.stringify(body ? {
3389
+ body,
3390
+ paragraphs
3391
+ } : { paragraphs });
3392
+ }
3393
+ /** The engine's JSON, parsed into the DTO shape (its `source` and
3394
+ * `diagnostics` dropped). `null` when it is not what the engine writes. */
3395
+ function parseEngineRichText(json) {
3396
+ let raw;
3397
+ try {
3398
+ raw = JSON.parse(json);
3399
+ } catch {
3400
+ return null;
3401
+ }
3402
+ if (!raw || typeof raw !== "object" || !raw.body || !Array.isArray(raw.paragraphs)) return null;
3403
+ const { source: _source, ...bodyRest } = raw.body;
3404
+ return {
3405
+ body: bodyRest,
3406
+ paragraphs: raw.paragraphs.map((p) => {
3407
+ const { runs, ...props } = p;
3408
+ return {
3409
+ ...props,
3410
+ runs: (runs ?? []).map((r) => r)
3411
+ };
3412
+ })
3413
+ };
3414
+ }
3415
+ /** Read an annotation's rich text through `EPDFAnnot_GetRichTextJSON`. */
3416
+ function readEngineRichText(fn, mem, annotPtr) {
3417
+ const json = readUtf8String(mem, (buf, cap) => fn.EPDFAnnot_GetRichTextJSON(annotPtr, buf, cap));
3418
+ return json ? parseEngineRichText(json) : null;
3419
+ }
3420
+ /**
3421
+ * A draft or patch that carries both `contents` and `richText` must agree:
3422
+ * `contents` is the plain projection of the rich text (plan §4.7). A stale
3423
+ * DTO echo fails loud here instead of quietly restoring old text.
3424
+ */
3425
+ function assertRichTextAgreement(input) {
3426
+ if (input.subtype !== "free-text" || input.richText === void 0) return;
3427
+ if (input.contents === void 0 || input.contents === null) return;
3428
+ if (input.contents !== richTextPlainText(input.richText)) throw new EngineError(EngineErrorCode.InvalidArg, "free-text: `contents` must equal the plain projection of `richText` when both are given");
3429
+ }
3430
+ //#endregion
2941
3431
  //#region src/features/annotations/internal/textAlignment.ts
2942
3432
  /**
2943
3433
  * `/Q` quadding codes (ISO 32000 §12.7.3.3) mapped to the wire-stable
@@ -3064,17 +3554,36 @@ const DEFAULT_FONT_SIZE$2 = 12;
3064
3554
  function colorsEqual(a, b) {
3065
3555
  return a.r === b.r && a.g === b.g && a.b === b.b;
3066
3556
  }
3067
- function readFreeText(fn, mem, annotPtr, base) {
3557
+ function readFreeText(fn, mem, annotPtr, base, _subtypeCode, ctx) {
3068
3558
  const da = readDefaultAppearance(fn, mem, annotPtr);
3069
3559
  const color = da?.color ?? { ...DEFAULT_FREETEXT_COLOR$1 };
3070
- const fontFamily = da ? standardFontFromCode(da.fontCode) : DEFAULT_STANDARD_FONT;
3071
- const fontSize = da && da.fontSize > 0 ? da.fontSize : DEFAULT_FONT_SIZE$2;
3560
+ const rich = readEngineRichText(fn, mem, annotPtr);
3561
+ const fonts = ctx?.fonts;
3562
+ const fontFamily = rich ? freeTextFontForFace(rich.body, fonts ? (family, weight, italic) => fonts.keyForFace(family, weight, italic) : void 0) : da ? standardFontFromCode(da.fontCode) : DEFAULT_STANDARD_FONT;
3563
+ const fontSize = rich && rich.body.size > 0 ? rich.body.size : da && da.fontSize > 0 ? da.fontSize : DEFAULT_FONT_SIZE$2;
3564
+ const richText = rich ?? {
3565
+ body: {
3566
+ family: "Helvetica",
3567
+ weight: 400,
3568
+ italic: false,
3569
+ size: fontSize,
3570
+ color: "#000000",
3571
+ decoration: [],
3572
+ script: "normal",
3573
+ letterSpacing: 0,
3574
+ horizontalScale: 1,
3575
+ align: "left",
3576
+ dir: "ltr"
3577
+ },
3578
+ paragraphs: [{ runs: [{ text: base.contents ?? "" }] }]
3579
+ };
3072
3580
  const textColor = readAnnotColor(fn, mem, annotPtr, FPDFANNOT_COLORTYPE.TextColor);
3073
3581
  const fontColor = textColor && !colorsEqual(textColor, color) ? textColor : void 0;
3074
3582
  const background = readAnnotColor(fn, mem, annotPtr, FPDFANNOT_COLORTYPE.Color);
3075
3583
  const ca = readAnnotOpacity(fn, mem, annotPtr);
3076
3584
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
3077
- const textAlign = textAlignmentFromCode(readTextAlignment(fn, annotPtr));
3585
+ const quadding = textAlignmentFromCode(readTextAlignment(fn, annotPtr));
3586
+ const textAlign = rich && rich.body.align !== "justify" ? rich.body.align : quadding;
3078
3587
  const intent = freeTextIntentFromName(readIntent(fn, mem, annotPtr));
3079
3588
  const points = readCalloutLine(fn, mem, annotPtr);
3080
3589
  const calloutLine = points.length === 2 ? [points[0], points[1]] : points.length === 3 ? [
@@ -3093,6 +3602,7 @@ function readFreeText(fn, mem, annotPtr, base) {
3093
3602
  fontFamily,
3094
3603
  fontSize,
3095
3604
  textAlign,
3605
+ richText,
3096
3606
  color,
3097
3607
  ...fontColor !== void 0 ? { fontColor } : {},
3098
3608
  interiorColor: background ?? null,
@@ -3308,45 +3818,10 @@ function readSquare(fn, mem, annotPtr, base) {
3308
3818
  };
3309
3819
  }
3310
3820
  //#endregion
3311
- //#region src/features/annotations/internal/stampName.ts
3312
- /**
3313
- * Stamp `/Name` label ↔ `FPDF_ANNOT_NAME` enum code mapping for the
3314
- * `EPDFAnnot_SetName` / `EPDFAnnot_GetName` fork helpers. Labels are the
3315
- * PDF spec / Adobe literal name values (ISO 32000 §12.5.6.12 table 181
3316
- * plus the Adobe SB/SH extended set the fork supports). Codes mirror the
3317
- * `FPDF_ANNOT_NAME` enum in `public/fpdf_annot.h` — same pattern as
3318
- * `lineEnding.ts` keeping engine-core PDFium-free.
3319
- */
3320
- const STAMP_NAME_TO_CODE = Object.freeze({
3321
- Approved: 13,
3322
- Experimental: 14,
3323
- NotApproved: 15,
3324
- AsIs: 16,
3325
- Expired: 17,
3326
- NotForPublicRelease: 18,
3327
- Confidential: 19,
3328
- Final: 20,
3329
- Sold: 21,
3330
- Departmental: 22,
3331
- ForComment: 23,
3332
- TopSecret: 24,
3333
- Draft: 25,
3334
- ForPublicRelease: 26,
3335
- Completed: 27,
3336
- Void: 28,
3337
- PreliminaryResults: 29,
3338
- InformationOnly: 30,
3339
- Rejected: 31,
3340
- Witness: 32,
3341
- InitialHere: 33,
3342
- SignHere: 34,
3343
- Accepted: 35
3344
- });
3345
- const STAMP_CODE_TO_NAME = Object.freeze(Object.fromEntries(Object.entries(STAMP_NAME_TO_CODE).map(([name, code]) => [code, name])));
3346
- //#endregion
3347
3821
  //#region src/features/annotations/internal/read/readStampAnnotation.ts
3348
3822
  /**
3349
- * Stamp DTO: base + `/Name` label + transform metadata. The visual content
3823
+ * Stamp DTO: base + `/Name` (standard or custom identifier, verbatim) +
3824
+ * transform metadata. The visual content
3350
3825
  * stays in the `/AP` stream — rendered via `renderAppearanceImages()`,
3351
3826
  * never surfaced as DTO data.
3352
3827
  */
@@ -3356,16 +3831,11 @@ function readStamp(fn, mem, annotPtr, base) {
3356
3831
  return {
3357
3832
  ...base,
3358
3833
  subtype: "stamp",
3359
- name: readStampName(fn, mem, annotPtr),
3834
+ name: readAnnotName(fn, mem, annotPtr),
3360
3835
  ...rotation != null ? { rotation } : {},
3361
3836
  ...unrotatedRect != null ? { unrotatedRect } : {}
3362
3837
  };
3363
3838
  }
3364
- function readStampName(fn, mem, annotPtr) {
3365
- const known = STAMP_CODE_TO_NAME[fn.EPDFAnnot_GetName(annotPtr)];
3366
- if (known !== void 0) return known;
3367
- return readAnnotString(fn, mem, annotPtr, "Name");
3368
- }
3369
3839
  //#endregion
3370
3840
  //#region src/features/annotations/internal/annotationState.ts
3371
3841
  /**
@@ -3421,7 +3891,7 @@ function readText(fn, mem, annotPtr, base) {
3421
3891
  const color = readAnnotColor(fn, mem, annotPtr) ?? { ...DEFAULT_NOTE_COLOR$1 };
3422
3892
  const ca = readAnnotOpacity(fn, mem, annotPtr);
3423
3893
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
3424
- const icon = NOTE_CODE_TO_ICON[fn.EPDFAnnot_GetName(annotPtr)] ?? DEFAULT_NOTE_ICON;
3894
+ const icon = NOTE_NAME_TO_ICON[readAnnotName(fn, mem, annotPtr) ?? ""] ?? DEFAULT_NOTE_ICON;
3425
3895
  const stateRaw = readAnnotString(fn, mem, annotPtr, "State");
3426
3896
  const stateModelRaw = readAnnotString(fn, mem, annotPtr, "StateModel");
3427
3897
  return {
@@ -3645,18 +4115,18 @@ function pickReader(rawSubtypeCode) {
3645
4115
  * per session, closed before replacement and on session teardown via
3646
4116
  * {@link disposeFormModel}.
3647
4117
  */
3648
- const cache$1 = /* @__PURE__ */ new WeakMap();
4118
+ const cache$2 = /* @__PURE__ */ new WeakMap();
3649
4119
  function acquireFormModel(runtime, session) {
3650
4120
  const seq = session.mutationSeq();
3651
- const hit = cache$1.get(session);
4121
+ const hit = cache$2.get(session);
3652
4122
  if (hit && hit.seq === seq) return hit.ptr;
3653
4123
  if (hit) {
3654
4124
  runtime.fn.EPDFForm_CloseModel(hit.ptr);
3655
- cache$1.delete(session);
4125
+ cache$2.delete(session);
3656
4126
  }
3657
4127
  const ptr = runtime.fn.EPDFForm_LoadModel(session.requireDocPtr());
3658
4128
  if (ptr === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to build form model");
3659
- cache$1.set(session, {
4129
+ cache$2.set(session, {
3660
4130
  seq,
3661
4131
  ptr
3662
4132
  });
@@ -3664,10 +4134,10 @@ function acquireFormModel(runtime, session) {
3664
4134
  }
3665
4135
  /** Close and forget the session's cached model, if any. */
3666
4136
  function disposeFormModel(runtime, session) {
3667
- const hit = cache$1.get(session);
4137
+ const hit = cache$2.get(session);
3668
4138
  if (!hit) return;
3669
4139
  runtime.fn.EPDFForm_CloseModel(hit.ptr);
3670
- cache$1.delete(session);
4140
+ cache$2.delete(session);
3671
4141
  }
3672
4142
  //#endregion
3673
4143
  //#region src/features/forms/internal/readFormSnapshot.ts
@@ -3696,7 +4166,7 @@ const FF_EDIT = 1 << 18;
3696
4166
  const FF_MULTI_SELECT = 1 << 21;
3697
4167
  const FF_COMB = 1 << 24;
3698
4168
  const FF_RADIOS_IN_UNISON = 1 << 25;
3699
- function readWide(runtime, call) {
4169
+ function readWide$1(runtime, call) {
3700
4170
  return readUtf16String(runtime.mem, call) ?? "";
3701
4171
  }
3702
4172
  function readWideOrNull(runtime, call) {
@@ -3710,7 +4180,7 @@ function readToggleWidgets(runtime, model, fieldIndex) {
3710
4180
  annotObjectNumber: fn.EPDFForm_GetFieldWidgetObjNum(model, fieldIndex, w),
3711
4181
  pageObjectNumber: fn.EPDFForm_GetFieldWidgetPageObjNum(model, fieldIndex, w),
3712
4182
  onState: readUtf8String(runtime.mem, (buf, cap) => fn.EPDFForm_GetFieldWidgetOnState(model, fieldIndex, w, buf, cap)) ?? "",
3713
- exportValue: readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldWidgetExportValue(model, fieldIndex, w, buf, cap)),
4183
+ exportValue: readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldWidgetExportValue(model, fieldIndex, w, buf, cap)),
3714
4184
  checked: fn.EPDFForm_IsFieldWidgetChecked(model, fieldIndex, w)
3715
4185
  });
3716
4186
  return widgets;
@@ -3730,8 +4200,8 @@ function readOptions(runtime, model, fieldIndex) {
3730
4200
  const count = fn.EPDFForm_CountFieldOptions(model, fieldIndex);
3731
4201
  const options = [];
3732
4202
  for (let o = 0; o < count; o++) options.push({
3733
- label: readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionLabel(model, fieldIndex, o, buf, cap)),
3734
- value: readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionValue(model, fieldIndex, o, buf, cap)),
4203
+ label: readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionLabel(model, fieldIndex, o, buf, cap)),
4204
+ value: readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionValue(model, fieldIndex, o, buf, cap)),
3735
4205
  selected: fn.EPDFForm_IsFieldOptionSelected(model, fieldIndex, o)
3736
4206
  });
3737
4207
  return options;
@@ -3740,7 +4210,7 @@ function readOptions(runtime, model, fieldIndex) {
3740
4210
  function readFieldAt(runtime, model, fieldIndex, docPtr, actionBudget = new ActionReadBudgetTracker()) {
3741
4211
  const { fn } = runtime;
3742
4212
  const fieldObjectNumber = fn.EPDFForm_GetFieldObjNum(model, fieldIndex);
3743
- const name = readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldName(model, fieldIndex, buf, cap));
4213
+ const name = readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldName(model, fieldIndex, buf, cap));
3744
4214
  const rawFlags = fn.EPDFForm_GetFieldFlags(model, fieldIndex);
3745
4215
  const family = FAMILY_BY_CODE[fn.EPDFForm_GetFieldFamily(model, fieldIndex)] ?? "unknown";
3746
4216
  const valueEntry = readValueEntry(runtime, model, fieldIndex, false);
@@ -3871,7 +4341,7 @@ function readValueEntry(runtime, model, fieldIndex, isDefault) {
3871
4341
  if (kind !== 1 && kind !== 2) return { kind: "unsupported" };
3872
4342
  const count = isDefault ? fn.EPDFForm_CountFieldDefaultValues(model, fieldIndex) : fn.EPDFForm_CountFieldValues(model, fieldIndex);
3873
4343
  const values = [];
3874
- for (let index = 0; index < count; index++) values.push(readWide(runtime, (buf, capacity) => isDefault ? fn.EPDFForm_GetFieldDefaultValueAt(model, fieldIndex, index, buf, capacity) : fn.EPDFForm_GetFieldValueAt(model, fieldIndex, index, buf, capacity)));
4344
+ for (let index = 0; index < count; index++) values.push(readWide$1(runtime, (buf, capacity) => isDefault ? fn.EPDFForm_GetFieldDefaultValueAt(model, fieldIndex, index, buf, capacity) : fn.EPDFForm_GetFieldValueAt(model, fieldIndex, index, buf, capacity)));
3875
4345
  return kind === 1 ? {
3876
4346
  kind: "scalar",
3877
4347
  value: values[0] ?? ""
@@ -4212,13 +4682,13 @@ function readAnnotationActions(fn, mem, docPtr, annotPtr, budget) {
4212
4682
  * releasing any enclosing `pagePtr`.
4213
4683
  */
4214
4684
  function collectPageAnnotations(input) {
4215
- const { runtime, session, pageObjectNumber, count, getAnnotPtrAt, signal } = input;
4685
+ const { runtime, session, pageObjectNumber, count, getAnnotPtrAt, signal, fonts } = input;
4216
4686
  const { fn, mem } = runtime;
4217
4687
  const annotations = [];
4218
4688
  let hasWeak = false;
4219
4689
  const revision = session.pageState(pageObjectNumber).revision;
4220
4690
  const actionBudget = new ActionReadBudgetTracker();
4221
- const readCtx = readContextFor(session);
4691
+ const readCtx = readContextFor(session, fonts);
4222
4692
  for (let i = 0; i < count; i++) {
4223
4693
  throwIfAborted(signal);
4224
4694
  const annotPtr = getAnnotPtrAt(i);
@@ -4254,9 +4724,10 @@ function collectPageAnnotations(input) {
4254
4724
  * as they land - the public DTO surface stays the same.
4255
4725
  */
4256
4726
  var AnnotationReader = class {
4257
- constructor(runtime, session) {
4727
+ constructor(runtime, session, fonts) {
4258
4728
  this.runtime = runtime;
4259
4729
  this.session = session;
4730
+ this.fonts = fonts;
4260
4731
  }
4261
4732
  list(pageObjectNumber, signal) {
4262
4733
  throwIfAborted(signal);
@@ -4271,7 +4742,8 @@ var AnnotationReader = class {
4271
4742
  pageObjectNumber,
4272
4743
  count,
4273
4744
  getAnnotPtrAt: (i) => fn.FPDFPage_GetAnnot(pagePtr, i),
4274
- signal
4745
+ signal,
4746
+ ...this.fonts ? { fonts: this.fonts } : {}
4275
4747
  });
4276
4748
  } finally {
4277
4749
  pool.release(pageObjectNumber);
@@ -4600,9 +5072,10 @@ function normalizeScale(scale) {
4600
5072
  * actually need a pagePtr to materialise their fields.
4601
5073
  */
4602
5074
  var RawAnnotationReader = class {
4603
- constructor(runtime, session) {
5075
+ constructor(runtime, session, fonts) {
4604
5076
  this.runtime = runtime;
4605
5077
  this.session = session;
5078
+ this.fonts = fonts;
4606
5079
  }
4607
5080
  listAll(signal) {
4608
5081
  throwIfAborted(signal);
@@ -4628,7 +5101,8 @@ var RawAnnotationReader = class {
4628
5101
  pageObjectNumber,
4629
5102
  count,
4630
5103
  getAnnotPtrAt: (i) => fn.EPDFPage_GetAnnotRaw(docPtr, record.pageIndex, i),
4631
- signal
5104
+ signal,
5105
+ ...this.fonts ? { fonts: this.fonts } : {}
4632
5106
  });
4633
5107
  }
4634
5108
  };
@@ -5443,7 +5917,7 @@ function isFileAttachmentSubtype(subtype) {
5443
5917
  return subtype === "file-attachment";
5444
5918
  }
5445
5919
  function setFileAttachmentIcon(fn, annotPtr, icon) {
5446
- if (!fn.EPDFAnnot_SetName(annotPtr, FILE_ICON_TO_CODE[icon])) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
5920
+ if (!fn.EPDFAnnot_SetName(annotPtr, FILE_ICON_TO_NAME[icon])) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
5447
5921
  }
5448
5922
  function requireFileResource(file, ctx) {
5449
5923
  const resource = ctx?.resources?.[file.resource];
@@ -5569,6 +6043,20 @@ const DEFAULT_FREETEXT_COLOR = {
5569
6043
  b: 0
5570
6044
  };
5571
6045
  /**
6046
+ * Write rich text through the engine's rich writer: `/RC`, `/DS`, `/DA`,
6047
+ * `/Contents` and the appearance, all or nothing. Faces in the input are
6048
+ * resolved from keys / standard names to the identities the engine names.
6049
+ */
6050
+ function writeRichText(fn, annotPtr, input, ctx) {
6051
+ const json = engineRichTextJson(input, ctx?.describeRegisteredFont);
6052
+ if (!fn.EPDFAnnot_SetRichTextJSON(annotPtr, json)) throw new EngineError(EngineErrorCode.InvalidArg, "rich text could not be written (empty rect, or a font resource could not be built)");
6053
+ }
6054
+ /** `#RRGGBB` for a rich body colour. */
6055
+ function hexColor(color) {
6056
+ const hex = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
6057
+ return `#${hex(color.r)}${hex(color.g)}${hex(color.b)}`.toUpperCase();
6058
+ }
6059
+ /**
5572
6060
  * Apply a free-text draft to a freshly-created annotation. Colour model:
5573
6061
  * - `color` -> `/DA` colour = border + default text colour.
5574
6062
  * - `fontColor` (optional) -> `TextColor` channel, overriding text only;
@@ -5610,6 +6098,7 @@ function applyFreeTextDraft(fn, mem, annotPtr, draft, ctx) {
5610
6098
  rotation: draft.rotation,
5611
6099
  unrotatedRect: draft.unrotatedRect
5612
6100
  });
6101
+ writeRichText(fn, annotPtr, draft.richText ?? { paragraphs: richTextParagraphsFromPlainText(draft.contents ?? "") }, ctx);
5613
6102
  }
5614
6103
  /**
5615
6104
  * Apply a free-text patch to an existing annotation. Only present fields are
@@ -5636,11 +6125,30 @@ function applyFreeTextPatch(fn, mem, annotPtr, patch, ctx) {
5636
6125
  else setAnnotColor(fn, annotPtr, patch.interiorColor, FPDFANNOT_COLORTYPE.Color);
5637
6126
  if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
5638
6127
  applyBorderPatch(fn, mem, annotPtr, patch);
5639
- if (patch.fontFamily !== void 0 || patch.fontSize !== void 0 || patch.color !== void 0) {
6128
+ const current = readEngineRichText(fn, mem, annotPtr);
6129
+ if (patch.color !== void 0) {
5640
6130
  const cur = readDefaultAppearance(fn, mem, annotPtr);
5641
- applyDefaultAppearance(fn, annotPtr, patch.fontFamily ?? (cur ? standardFontFromCode(cur.fontCode) : "helvetica"), patch.fontSize ?? (cur && cur.fontSize > 0 ? cur.fontSize : 12), patch.color ?? cur?.color ?? DEFAULT_FREETEXT_COLOR, ctx);
6131
+ applyDefaultAppearance(fn, annotPtr, cur ? standardFontFromCode(cur.fontCode) : DEFAULT_STANDARD_FONT, cur && cur.fontSize > 0 ? cur.fontSize : 12, patch.color, ctx);
5642
6132
  }
5643
6133
  if (patch.fontColor !== void 0) setAnnotColor(fn, annotPtr, patch.fontColor, FPDFANNOT_COLORTYPE.TextColor);
6134
+ if (patch.richText !== void 0) writeRichText(fn, annotPtr, patch.richText, ctx);
6135
+ else if (patch.contents !== void 0) writeRichText(fn, annotPtr, { paragraphs: richTextParagraphsFromPlainText(patch.contents ?? "") }, ctx);
6136
+ else if (current && (patch.fontFamily !== void 0 || patch.fontSize !== void 0 || patch.fontColor !== void 0 || patch.textAlign !== void 0)) {
6137
+ const body = { ...current.body };
6138
+ if (patch.fontFamily !== void 0) {
6139
+ const face = faceForFreeTextFont(patch.fontFamily, ctx?.describeRegisteredFont);
6140
+ body.family = face.family;
6141
+ if (face.weight !== void 0) body.weight = face.weight;
6142
+ if (face.italic !== void 0) body.italic = face.italic;
6143
+ }
6144
+ if (patch.fontSize !== void 0) body.size = patch.fontSize;
6145
+ if (patch.fontColor !== void 0) body.color = hexColor(patch.fontColor);
6146
+ if (patch.textAlign !== void 0) body.align = patch.textAlign;
6147
+ writeRichText(fn, annotPtr, {
6148
+ body,
6149
+ paragraphs: current.paragraphs
6150
+ }, ctx);
6151
+ }
5644
6152
  if (patch.textAlign !== void 0) setTextAlignment(fn, annotPtr, textAlignmentToCode(patch.textAlign));
5645
6153
  if (patch.intent !== void 0) setIntent(fn, annotPtr, freeTextIntentToName(patch.intent));
5646
6154
  if (patch.rectDifferences === null) clearRectangleDifferences(fn, annotPtr);
@@ -6105,12 +6613,12 @@ const STAMP_FIT_TO_CODE = {
6105
6613
  * creating an annotation or strengthening a weak annotation id.
6106
6614
  */
6107
6615
  function preflightStampDraft(draft, ctx) {
6108
- if (draft.name !== void 0) requireStampNameCode(draft.name);
6616
+ if (draft.name !== void 0) requireStampName(draft.name);
6109
6617
  requireStampFit(draft.fit ?? "contain");
6110
6618
  requireStampContent(draft.source, ctx);
6111
6619
  }
6112
6620
  function preflightStampPatch(patch, ctx) {
6113
- if (patch.name !== void 0) requireStampNameCode(patch.name);
6621
+ if (patch.name !== void 0 && patch.name !== null) requireStampName(patch.name);
6114
6622
  requireStampFit(patch.fit ?? "contain");
6115
6623
  if (patch.source !== void 0) requireStampContent(patch.source, ctx);
6116
6624
  }
@@ -6136,7 +6644,8 @@ function applyStampDraft(fn, mem, annotPtr, draft, ctx) {
6136
6644
  }
6137
6645
  function applyStampPatch(fn, mem, annotPtr, patch, ctx) {
6138
6646
  applyAnnotationBasePatch(fn, mem, annotPtr, patch);
6139
- if (patch.name !== void 0) setStampName(fn, annotPtr, patch.name);
6647
+ if (patch.name === null) fn.EPDFAnnot_RemoveKey(annotPtr, "Name");
6648
+ else if (patch.name !== void 0) setStampName(fn, annotPtr, patch.name);
6140
6649
  if (patch.source !== void 0) {
6141
6650
  const rect = patch.rect ?? readAnnotRect(fn, mem, annotPtr);
6142
6651
  authorStampAppearance(fn, mem, annotPtr, patch.source, patch.fit ?? "contain", {
@@ -6156,15 +6665,14 @@ function applyStampPatch(fn, mem, annotPtr, patch, ctx) {
6156
6665
  function isStampSubtype(subtype) {
6157
6666
  return subtype === "stamp";
6158
6667
  }
6668
+ /** Any non-empty name: standard ('Approved') or custom ('#LBGiYhk8V…') —
6669
+ * the fork writes a name object and escapes it; predefined sets are a
6670
+ * reader concern (ISO 32000-2 table 187 allows additional names). */
6159
6671
  function setStampName(fn, annotPtr, name) {
6160
- const code = STAMP_NAME_TO_CODE[name];
6161
- if (code === void 0) throw new EngineError(EngineErrorCode.Unknown, "stamp name preflight invariant broken");
6162
- if (!fn.EPDFAnnot_SetName(annotPtr, code)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
6672
+ if (!fn.EPDFAnnot_SetName(annotPtr, name)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
6163
6673
  }
6164
- function requireStampNameCode(name) {
6165
- const code = STAMP_NAME_TO_CODE[name];
6166
- if (code === void 0) throw new EngineError(EngineErrorCode.InvalidArg, `unknown stamp name '${name}'; supported names: ${Object.keys(STAMP_NAME_TO_CODE).join(", ")}`);
6167
- return code;
6674
+ function requireStampName(name) {
6675
+ if (name.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "stamp name must be non-empty");
6168
6676
  }
6169
6677
  function requireStampFit(fit) {
6170
6678
  if (STAMP_FIT_TO_CODE[fit] === void 0) throw new EngineError(EngineErrorCode.InvalidArg, `unknown stamp fit '${String(fit)}'; expected contain, cover, or fill`);
@@ -6352,7 +6860,7 @@ function isTextSubtype(subtype) {
6352
6860
  return subtype === "text";
6353
6861
  }
6354
6862
  function setNoteIcon(fn, annotPtr, icon) {
6355
- if (!fn.EPDFAnnot_SetName(annotPtr, NOTE_ICON_TO_CODE[icon])) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
6863
+ if (!fn.EPDFAnnot_SetName(annotPtr, NOTE_ICON_TO_NAME[icon])) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetName returned false");
6356
6864
  }
6357
6865
  //#endregion
6358
6866
  //#region src/features/annotations/internal/write/writeVertexAnnotation.ts
@@ -6690,7 +7198,10 @@ var AnnotationMutator = class {
6690
7198
  writeContext(pagePtr, resources) {
6691
7199
  const fonts = this.fonts;
6692
7200
  return {
6693
- ...fonts ? { resolveRegisteredFontId: (key) => fonts.idFor(key) } : {},
7201
+ ...fonts ? {
7202
+ resolveRegisteredFontId: (key) => fonts.idFor(key),
7203
+ describeRegisteredFont: (key) => fonts.describeOrUndefined(key)
7204
+ } : {},
6694
7205
  docPtr: this.session.requireDocPtr(),
6695
7206
  pagePtr,
6696
7207
  ...resources ? { resources } : {}
@@ -6706,6 +7217,7 @@ var AnnotationMutator = class {
6706
7217
  this.ensureKnownWeakStateFromPage(pageObjectNumber, pagePtr);
6707
7218
  const writeCtx = this.writeContext(pagePtr, resources);
6708
7219
  preflightDraft(draft, writeCtx);
7220
+ assertRichTextAgreement(draft);
6709
7221
  const pageStateBefore = this.session.pageState(pageObjectNumber);
6710
7222
  throwIfAborted(signal);
6711
7223
  const annotPtr = fn.EPDFPage_CreateAnnot(pagePtr, subtypeCode);
@@ -6728,7 +7240,7 @@ var AnnotationMutator = class {
6728
7240
  if (newObjNum <= 0) throw new EngineError(EngineErrorCode.Unknown, `EPDFPage_CreateAnnot produced a direct object (no objectNumber); fork helper invariant broken`);
6729
7241
  newIndex = fn.FPDFPage_GetAnnotIndex(pagePtr, annotPtr);
6730
7242
  if (newIndex < 0) throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_GetAnnotIndex returned ${newIndex} for freshly created annotation`);
6731
- dto = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session));
7243
+ dto = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session, this.fonts));
6732
7244
  joinWidgetFieldNumbers(this.runtime, this.session, [dto]);
6733
7245
  } finally {
6734
7246
  fn.FPDFPage_CloseAnnot(annotPtr);
@@ -6763,6 +7275,7 @@ var AnnotationMutator = class {
6763
7275
  throwIfAborted(signal);
6764
7276
  const writeCtx = this.writeContext(pagePtr, resources);
6765
7277
  preflightPatch(patch, writeCtx);
7278
+ assertRichTextAgreement(patch);
6766
7279
  this.ensureKnownWeakStateFromPage(ref.pageObjectNumber, pagePtr);
6767
7280
  const pageStateBefore = this.session.pageState(ref.pageObjectNumber);
6768
7281
  throwIfAborted(signal);
@@ -6770,7 +7283,7 @@ var AnnotationMutator = class {
6770
7283
  const previousBlendMode = blendModeFromCode(fn.EPDFAnnot_GetBlendMode(annotPtr));
6771
7284
  const preIndex = fn.FPDFPage_GetAnnotIndex(pagePtr, annotPtr);
6772
7285
  if (preIndex < 0) throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_GetAnnotIndex returned ${preIndex} before update`);
6773
- const currentDto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, preIndex, pageStateBefore.revision, readContextFor(this.session));
7286
+ const currentDto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, preIndex, pageStateBefore.revision, readContextFor(this.session, this.fonts));
6774
7287
  applyPatch(fn, mem, annotPtr, patch, writeCtx);
6775
7288
  let linkedParentId = null;
6776
7289
  if (patch.inReplyTo !== void 0 || patch.replyType !== void 0) linkedParentId = writeAnnotationRelationship(this.runtime, this.session, pagePtr, annotPtr, ref.pageObjectNumber, {
@@ -6797,7 +7310,7 @@ var AnnotationMutator = class {
6797
7310
  annotPtr = resolveAnnotPtr(this.runtime, this.session, pagePtr, ref);
6798
7311
  const newIndex = fn.FPDFPage_GetAnnotIndex(pagePtr, annotPtr);
6799
7312
  if (newIndex < 0) throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_GetAnnotIndex returned ${newIndex} after update`);
6800
- const dto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session));
7313
+ const dto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session, this.fonts));
6801
7314
  joinWidgetFieldNumbers(this.runtime, this.session, [dto]);
6802
7315
  this.recordWeakStateFromPage(ref.pageObjectNumber, pagePtr);
6803
7316
  const meta = computeMutationImpact({
@@ -7010,7 +7523,7 @@ var AnnotationMutator = class {
7010
7523
  const annotPtr = fn.FPDFPage_GetAnnot(pagePtr, newIdx);
7011
7524
  if (!annotPtr) throw new EngineError(EngineErrorCode.Unknown, `failed to re-read moved annotation at index ${newIdx}`);
7012
7525
  try {
7013
- moved[i] = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIdx, bumpedRev, readContextFor(this.session));
7526
+ moved[i] = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIdx, bumpedRev, readContextFor(this.session, this.fonts));
7014
7527
  } finally {
7015
7528
  fn.FPDFPage_CloseAnnot(annotPtr);
7016
7529
  }
@@ -7079,6 +7592,141 @@ var AnnotationMutator = class {
7079
7592
  }
7080
7593
  };
7081
7594
  //#endregion
7595
+ //#region src/features/annotations/AnnotationFlattener.ts
7596
+ const FLATTEN_FAIL$1 = 0;
7597
+ const FLATTEN_SUCCESS$1 = 1;
7598
+ const FLATTEN_NOTHING_TO_DO$1 = 2;
7599
+ const STATUS_APPLIED = 0;
7600
+ const STATUS_NOT_ON_PAGE = 2;
7601
+ const FPDF_NO_INCREMENTAL$1 = 2;
7602
+ /**
7603
+ * The annotation-plane flatten verbs — `pages.flatten` for a chosen set:
7604
+ * in place (`flatten`) or into a new single-page document
7605
+ * (`exportAppearance`). Both resolve refs to native handles on the page,
7606
+ * hand the SET to the fork (one candidate plan, one placement writer — the
7607
+ * same code whole-page flatten runs), and release every handle afterwards.
7608
+ */
7609
+ var AnnotationFlattener = class {
7610
+ constructor(runtime, session) {
7611
+ this.runtime = runtime;
7612
+ this.session = session;
7613
+ }
7614
+ flatten(pageObjectNumber, refs, usage, signal) {
7615
+ throwIfAborted(signal);
7616
+ this.requireRefs("annotations.flatten", pageObjectNumber, refs);
7617
+ const { fn, mem } = this.runtime;
7618
+ const pool = this.session.pagePool();
7619
+ const pagePtr = pool.acquire(pageObjectNumber);
7620
+ try {
7621
+ const annotPtrs = this.resolveAll(pagePtr, refs);
7622
+ let code;
7623
+ let statuses;
7624
+ try {
7625
+ [code, statuses] = this.withHandleArray(annotPtrs, (arrayPtr) => withScratch(mem, 4 * refs.length, (statusPtr) => {
7626
+ const rc = fn.EPDFPage_FlattenAnnotations(pagePtr, arrayPtr, refs.length, usage === "print" ? 1 : 0, statusPtr);
7627
+ const out = [];
7628
+ for (let i = 0; i < refs.length; i++) out.push(Number(mem.peek(statusPtr, "i32", 4 * i)));
7629
+ return [rc, out];
7630
+ }));
7631
+ } finally {
7632
+ for (const annotPtr of annotPtrs) fn.FPDFPage_CloseAnnot(annotPtr);
7633
+ }
7634
+ if (code === FLATTEN_FAIL$1) {
7635
+ const foreign = statuses.findIndex((status) => status === STATUS_NOT_ON_PAGE);
7636
+ throw new EngineError(foreign >= 0 ? EngineErrorCode.InvalidArg : EngineErrorCode.Unknown, foreign >= 0 ? `annotations.flatten: ref ${foreign} is not an annotation of page ${pageObjectNumber}` : "native annotation flatten failed after preflight");
7637
+ }
7638
+ const results = refs.map((ref, i) => ({
7639
+ ref,
7640
+ status: statuses[i] === STATUS_APPLIED ? "applied" : "skipped"
7641
+ }));
7642
+ if (code === FLATTEN_NOTHING_TO_DO$1 || code !== FLATTEN_SUCCESS$1) return {
7643
+ pageObjectNumber,
7644
+ usage,
7645
+ results,
7646
+ meta: null
7647
+ };
7648
+ this.session.noteMutation();
7649
+ this.session.bumpRevision(pageObjectNumber);
7650
+ try {
7651
+ new AnnotationReader(this.runtime, this.session).list(pageObjectNumber, signal);
7652
+ } catch {}
7653
+ return {
7654
+ pageObjectNumber,
7655
+ usage,
7656
+ results,
7657
+ meta: {
7658
+ affectedPages: [this.session.pageState(pageObjectNumber)],
7659
+ cacheDelta: null
7660
+ }
7661
+ };
7662
+ } finally {
7663
+ pool.release(pageObjectNumber);
7664
+ }
7665
+ }
7666
+ exportAppearance(pageObjectNumber, refs, signal) {
7667
+ throwIfAborted(signal);
7668
+ this.requireRefs("annotations.exportAppearance", pageObjectNumber, refs);
7669
+ const { fn, mem } = this.runtime;
7670
+ const pool = this.session.pagePool();
7671
+ const pagePtr = pool.acquire(pageObjectNumber);
7672
+ let exportedPtr = null;
7673
+ let pdfPtr = null;
7674
+ try {
7675
+ const annotPtrs = this.resolveAll(pagePtr, refs);
7676
+ try {
7677
+ exportedPtr = this.withHandleArray(annotPtrs, (arrayPtr) => fn.EPDFPage_ExportAnnotationsAsDocument(pagePtr, arrayPtr, refs.length));
7678
+ } finally {
7679
+ for (const annotPtr of annotPtrs) fn.FPDFPage_CloseAnnot(annotPtr);
7680
+ }
7681
+ if (!exportedPtr) throw new EngineError(EngineErrorCode.InvalidArg, "annotations.exportAppearance: every ref must be a visible annotation of this page with a normal appearance");
7682
+ return withScratch(mem, 4, (sizePtr) => {
7683
+ mem.poke(sizePtr, "i32", 0);
7684
+ pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(exportedPtr, FPDF_NO_INCREMENTAL$1, sizePtr);
7685
+ const size = Number(mem.peek(sizePtr, "i32"));
7686
+ if (!pdfPtr || size <= 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save exported appearances");
7687
+ const bytes = mem.readBytes(pdfPtr, size);
7688
+ const buffer = new ArrayBuffer(bytes.byteLength);
7689
+ new Uint8Array(buffer).set(bytes);
7690
+ return {
7691
+ bytes: buffer,
7692
+ size
7693
+ };
7694
+ });
7695
+ } finally {
7696
+ if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
7697
+ if (exportedPtr) fn.FPDF_CloseDocument(exportedPtr);
7698
+ pool.release(pageObjectNumber);
7699
+ }
7700
+ }
7701
+ /** Resolve every ref on `pagePtr` (NotFound / InvalidReference from the
7702
+ * resolver on caller error); on any failure, close what was opened. */
7703
+ resolveAll(pagePtr, refs) {
7704
+ const { fn } = this.runtime;
7705
+ const annotPtrs = [];
7706
+ try {
7707
+ for (const ref of refs) annotPtrs.push(resolveAnnotPtr(this.runtime, this.session, pagePtr, ref));
7708
+ return annotPtrs;
7709
+ } catch (error) {
7710
+ for (const annotPtr of annotPtrs) fn.FPDFPage_CloseAnnot(annotPtr);
7711
+ throw error;
7712
+ }
7713
+ }
7714
+ /** Marshal an `FPDF_ANNOTATION*` array for the call's duration. */
7715
+ withHandleArray(annotPtrs, body) {
7716
+ const { mem } = this.runtime;
7717
+ const isWasm = this.runtime.kind === "wasm";
7718
+ const ptrSize = isWasm ? 4 : 8;
7719
+ return withScratch(mem, Math.max(1, annotPtrs.length * ptrSize), (arrayPtr) => {
7720
+ annotPtrs.forEach((ptr, i) => isWasm ? mem.poke(arrayPtr, "i32", Number(ptr), i * 4) : mem.poke(arrayPtr, "i64", ptr, i * 8));
7721
+ return body(arrayPtr);
7722
+ });
7723
+ }
7724
+ requireRefs(op, pageObjectNumber, refs) {
7725
+ if (refs.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, `${op} requires at least one ref`);
7726
+ for (const ref of refs) if (ref.pageObjectNumber !== pageObjectNumber) throw new EngineError(EngineErrorCode.InvalidArg, `${op} refs must all target page ${pageObjectNumber}; got ref on page ${ref.pageObjectNumber}`);
7727
+ }
7728
+ };
7729
+ //#endregion
7082
7730
  //#region src/features/pages/PagesFlattener.ts
7083
7731
  const FLATTEN_FAIL = 0;
7084
7732
  const FLATTEN_SUCCESS = 1;
@@ -8137,104 +8785,2174 @@ function layerSaveError(status) {
8137
8785
  return new EngineError(EngineErrorCode.DocOpenFailed, "failed to save layer artifact");
8138
8786
  }
8139
8787
  //#endregion
8140
- //#region src/features/save/DocumentSaver.ts
8788
+ //#region src/runtime/memory/u64.ts
8141
8789
  /**
8142
- * Owns every "serialize the open document" path for a `DocumentSession`.
8143
- * Two distinct outputs:
8144
- *
8145
- * - Layer artifacts (`saveLayerArtifact*`) are the storage-optimized
8146
- * `.layer` delta used by server-side persistence. Only valid for a
8147
- * `layer` session.
8148
- * - Standalone saves (`saveStandalone*`) export a self-contained PDF
8149
- * view (base + applied delta) via `FPDF_SaveAsCopy` /
8150
- * `EPDF_SaveDocumentToOwnedBuffer`, valid for any session kind.
8151
- *
8152
- * Lives in `engine-services` so the browser Worker and the Node
8153
- * `worker_thread` server share the exact same save code path.
8790
+ * 64-bit out-params as two little-endian 32-bit halves: the wasm memory
8791
+ * helpers have no 64-bit accessors (Emscripten's `setValue(i64)` needs
8792
+ * WASM_BIGINT), and the native runtime reads the same layout.
8154
8793
  */
8155
- var DocumentSaver = class {
8156
- constructor(runtime, session) {
8157
- this.runtime = runtime;
8158
- this.session = session;
8794
+ function pokeU64(mem, ptr, value, byteOffset = 0) {
8795
+ const big = BigInt(value);
8796
+ mem.poke(ptr, "i32", Number(big & 4294967295n) | 0, byteOffset);
8797
+ mem.poke(ptr, "i32", Number(big >> 32n & 4294967295n) | 0, byteOffset + 4);
8798
+ }
8799
+ function peekU64(mem, ptr, byteOffset = 0) {
8800
+ const lo = Number(mem.peek(ptr, "i32", byteOffset)) >>> 0;
8801
+ return (Number(mem.peek(ptr, "i32", byteOffset + 4)) >>> 0) * 4294967296 + lo;
8802
+ }
8803
+ //#endregion
8804
+ //#region src/features/signature/internal/readSignatureModel.ts
8805
+ const KIND_DOC_TIMESTAMP = 1;
8806
+ const COVERAGE_BY_CODE = {
8807
+ 0: "whole-revision",
8808
+ 1: "partial",
8809
+ 2: "malformed"
8810
+ };
8811
+ const STRING_FILTER = 0;
8812
+ const STRING_SUBFILTER = 1;
8813
+ const STRING_NAME = 2;
8814
+ const STRING_REASON = 3;
8815
+ const STRING_LOCATION = 4;
8816
+ const STRING_CONTACT_INFO = 5;
8817
+ const STRING_M = 6;
8818
+ const FIELD_ACTION_BY_CODE = {
8819
+ 1: "all",
8820
+ 2: "include",
8821
+ 3: "exclude"
8822
+ };
8823
+ const FIELDS_FIELDMDP = 0;
8824
+ const FIELDS_LOCK = 1;
8825
+ const SV_V = 4;
8826
+ const SV_MDP = 1 << 17;
8827
+ const SV_LIST_SUBFILTER = 0;
8828
+ const SV_LIST_DIGEST_METHOD = 1;
8829
+ const SV_LIST_REASONS = 2;
8830
+ /** The PDF 1.7 seed-value entry set this engine implements. */
8831
+ const SUPPORTED_SEED_VALUE_VERSION = 2;
8832
+ const SV_UNSUPPORTED_REQUIRED = 432;
8833
+ function readWide(runtime, call) {
8834
+ return readUtf16String(runtime.mem, call, "");
8835
+ }
8836
+ function isPermission(value) {
8837
+ return value === 1 || value === 2 || value === 3;
8838
+ }
8839
+ function readByteRange(runtime, model, index) {
8840
+ const { fn, mem } = runtime;
8841
+ return withScratch(mem, 32, (ptr) => {
8842
+ for (let k = 0; k < 4; k++) pokeU64(mem, ptr, 0, k * 8);
8843
+ if (!fn.EPDFSig_GetByteRange(model, index, ptr)) return null;
8844
+ return [
8845
+ peekU64(mem, ptr, 0),
8846
+ peekU64(mem, ptr, 8),
8847
+ peekU64(mem, ptr, 16),
8848
+ peekU64(mem, ptr, 24)
8849
+ ];
8850
+ });
8851
+ }
8852
+ function readLock(runtime, model, index, which) {
8853
+ const { fn } = runtime;
8854
+ const code = which === FIELDS_LOCK ? fn.EPDFSig_GetLockAction(model, index) : fn.EPDFSig_GetFieldMDPAction(model, index);
8855
+ const action = FIELD_ACTION_BY_CODE[code];
8856
+ if (!action) return null;
8857
+ const fields = [];
8858
+ const count = fn.EPDFSig_GetFieldNameCount(model, index, which);
8859
+ for (let n = 0; n < count; n++) fields.push(readWide(runtime, (buf, cap) => fn.EPDFSig_GetFieldNameAt(model, index, which, n, buf, cap)) ?? "");
8860
+ const spec = {
8861
+ action,
8862
+ fields
8863
+ };
8864
+ if (which === FIELDS_LOCK) {
8865
+ const permission = fn.EPDFSig_GetLockPermission(model, index);
8866
+ if (isPermission(permission)) spec.permission = permission;
8159
8867
  }
8160
- saveLayerArtifact() {
8161
- this.requireLayer();
8162
- const { mem, fn } = this.runtime;
8163
- const sizePtr = mem.alloc(4);
8164
- const statusPtr = mem.alloc(4);
8165
- let artifactPtr = null;
8166
- try {
8167
- mem.poke(sizePtr, "i32", 0);
8168
- mem.poke(statusPtr, "i32", -1);
8169
- artifactPtr = fn.EPDFLayer_SaveLayerArtifactToOwnedBuffer(this.session.requireDocPtr(), sizePtr, statusPtr);
8170
- const status = Number(mem.peek(statusPtr, "i32"));
8171
- const size = Number(mem.peek(sizePtr, "i32"));
8172
- if (!artifactPtr || status !== 0 || size <= 0) throw layerSaveError(status);
8173
- const bytes = mem.readBytes(artifactPtr, size);
8174
- const buffer = new ArrayBuffer(bytes.byteLength);
8175
- new Uint8Array(buffer).set(bytes);
8176
- return {
8177
- bytes: buffer,
8178
- size
8179
- };
8180
- } finally {
8181
- if (artifactPtr) fn.EPDF_FreeBuffer(artifactPtr);
8182
- mem.free(statusPtr);
8183
- mem.free(sizePtr);
8184
- }
8868
+ return spec;
8869
+ }
8870
+ function readSeedValue(runtime, model, index) {
8871
+ const { fn } = runtime;
8872
+ if (!fn.EPDFSig_HasSeedValue(model, index)) return null;
8873
+ const requiredFlags = fn.EPDFSig_GetSeedValueRequiredFlags(model, index) >>> 0;
8874
+ const presentFlags = fn.EPDFSig_GetSeedValuePresentFlags(model, index) >>> 0;
8875
+ const version = fn.EPDFSig_GetSeedValueVersion(model, index);
8876
+ const mdp = fn.EPDFSig_GetSeedValueMDP(model, index);
8877
+ const list = (which) => {
8878
+ const count = fn.EPDFSig_GetSeedValueListCount(model, index, which);
8879
+ const items = [];
8880
+ for (let n = 0; n < count; n++) items.push(readWide(runtime, (buf, cap) => fn.EPDFSig_GetSeedValueListAt(model, index, which, n, buf, cap)) ?? "");
8881
+ return items;
8882
+ };
8883
+ return {
8884
+ requiredFlags,
8885
+ presentFlags,
8886
+ version: version > 0 ? version : null,
8887
+ mdp: (presentFlags & SV_MDP) !== 0 && mdp >= 0 && mdp <= 3 ? mdp : null,
8888
+ filter: readWide(runtime, (buf, cap) => fn.EPDFSig_GetSeedValueFilter(model, index, buf, cap)),
8889
+ subFilters: list(SV_LIST_SUBFILTER),
8890
+ digestMethods: list(SV_LIST_DIGEST_METHOD),
8891
+ reasons: list(SV_LIST_REASONS),
8892
+ unsupportedRequired: (requiredFlags & SV_UNSUPPORTED_REQUIRED) !== 0 || (requiredFlags & SV_V) !== 0 && version > SUPPORTED_SEED_VALUE_VERSION
8893
+ };
8894
+ }
8895
+ /** Every signature field of a native signature model, in model order. */
8896
+ function readSignaturesFromModel(runtime, model) {
8897
+ const { fn } = runtime;
8898
+ const count = fn.EPDFSig_Count(model);
8899
+ const out = [];
8900
+ const str = (i, key) => readWide(runtime, (buf, cap) => fn.EPDFSig_GetString(model, i, key, buf, cap));
8901
+ for (let i = 0; i < count; i++) {
8902
+ const signed = fn.EPDFSig_IsSigned(model, i);
8903
+ const widgetObjNum = fn.EPDFSig_GetWidgetObjNum(model, i);
8904
+ const revisionIndex = fn.EPDFSig_GetRevisionIndex(model, i);
8905
+ const docMdp = fn.EPDFSig_GetDocMDPPermission(model, i);
8906
+ out.push({
8907
+ index: i,
8908
+ field: {
8909
+ kind: "objectNumber",
8910
+ fieldObjectNumber: fn.EPDFSig_GetFieldObjNum(model, i)
8911
+ },
8912
+ fieldName: readWide(runtime, (buf, cap) => fn.EPDFSig_GetFieldName(model, i, buf, cap)) ?? "",
8913
+ widget: widgetObjNum > 0 ? {
8914
+ annotObjectNumber: widgetObjNum,
8915
+ pageObjectNumber: fn.EPDFSig_GetWidgetPageObjNum(model, i)
8916
+ } : null,
8917
+ signed,
8918
+ kind: fn.EPDFSig_GetKind(model, i) === KIND_DOC_TIMESTAMP ? "timestamp" : "signature",
8919
+ filter: str(i, STRING_FILTER),
8920
+ subFilter: str(i, STRING_SUBFILTER),
8921
+ byteRange: signed ? readByteRange(runtime, model, i) : null,
8922
+ contentsSize: signed ? Math.max(0, fn.EPDFSig_GetContents(model, i, NULL_PTR, 0)) : 0,
8923
+ coverage: signed ? COVERAGE_BY_CODE[fn.EPDFSig_GetCoverage(model, i)] ?? "malformed" : null,
8924
+ revisionIndex: signed && revisionIndex >= 0 ? revisionIndex : null,
8925
+ signer: {
8926
+ name: str(i, STRING_NAME),
8927
+ reason: str(i, STRING_REASON),
8928
+ location: str(i, STRING_LOCATION),
8929
+ contactInfo: str(i, STRING_CONTACT_INFO),
8930
+ claimedTime: str(i, STRING_M)
8931
+ },
8932
+ docMdp: isPermission(docMdp) ? docMdp : null,
8933
+ catalogCertification: fn.EPDFSig_IsCatalogCertification(model, i),
8934
+ fieldMdp: readLock(runtime, model, i, FIELDS_FIELDMDP),
8935
+ lock: readLock(runtime, model, i, FIELDS_LOCK),
8936
+ seedValue: readSeedValue(runtime, model, i)
8937
+ });
8185
8938
  }
8186
- saveLayerArtifactToFile(path) {
8187
- this.requireLayer();
8188
- const { mem, fn } = this.runtime;
8189
- const statusPtr = mem.alloc(4);
8190
- const writer = this.runtime.fileWrite.toNodeFile(path);
8191
- try {
8192
- mem.poke(statusPtr, "i32", -1);
8193
- const ok = fn.EPDFLayer_SaveLayerArtifact(this.session.requireDocPtr(), writer.ptr, statusPtr);
8194
- const status = Number(mem.peek(statusPtr, "i32"));
8195
- if (!ok || status !== 0) throw layerSaveError(status);
8196
- return { path };
8197
- } finally {
8198
- writer.close();
8199
- mem.free(statusPtr);
8939
+ return out;
8940
+ }
8941
+ /** The DER `/Contents` of signature `index`, copied out; `NotFound` when unsigned or malformed. */
8942
+ function readContentsAt(runtime, model, index) {
8943
+ const { fn, mem } = runtime;
8944
+ const length = fn.EPDFSig_GetContents(model, index, NULL_PTR, 0);
8945
+ if (length <= 0) throw new EngineError(EngineErrorCode.NotFound, "signature has no usable /Contents (malformed encoding)");
8946
+ return withScratch(mem, length, (buf) => {
8947
+ if (fn.EPDFSig_GetContents(model, index, buf, length) !== length) throw new EngineError(EngineErrorCode.Unknown, "failed to read signature contents");
8948
+ const out = new Uint8Array(length);
8949
+ out.set(mem.readBytes(buf, length));
8950
+ return out;
8951
+ });
8952
+ }
8953
+ /** The chained revisions of a document, oldest first; empty when the chain is not valid. */
8954
+ function readRevisions(runtime, docPtr, signatures) {
8955
+ const { fn, mem } = runtime;
8956
+ const count = fn.EPDFDoc_GetRevisionCount(docPtr);
8957
+ const revisions = [];
8958
+ if (count <= 0) return revisions;
8959
+ withScratchN(mem, [8, 8], ([endPtr, xrefPtr]) => {
8960
+ for (let i = 0; i < count; i++) {
8961
+ pokeU64(mem, endPtr, 0);
8962
+ pokeU64(mem, xrefPtr, 0);
8963
+ if (!fn.EPDFDoc_GetRevision(docPtr, i, endPtr, xrefPtr)) throw new EngineError(EngineErrorCode.Unknown, `failed to read revision ${i}`);
8964
+ const sealedBy = signatures.find((s) => s.revisionIndex === i);
8965
+ revisions.push({
8966
+ index: i,
8967
+ end: peekU64(mem, endPtr),
8968
+ xrefOffset: peekU64(mem, xrefPtr),
8969
+ signatureIndex: sealedBy ? sealedBy.index : null
8970
+ });
8200
8971
  }
8201
- }
8202
- saveStandaloneToBuffer(mode) {
8203
- const { mem, fn } = this.runtime;
8204
- const sizePtr = mem.alloc(4);
8205
- let pdfPtr = null;
8972
+ });
8973
+ return revisions;
8974
+ }
8975
+ /** Whether any signature field of the document carries a signed value (fields alone do not count). */
8976
+ function hasSignedSignature(runtime, docPtr) {
8977
+ const { fn } = runtime;
8978
+ return withSignatureModel(runtime, docPtr, (model) => {
8979
+ const count = fn.EPDFSig_Count(model);
8980
+ for (let i = 0; i < count; i++) if (fn.EPDFSig_IsSigned(model, i)) return true;
8981
+ return false;
8982
+ });
8983
+ }
8984
+ /** Run `body` with a fresh signature model of `docPtr`, closing it afterwards. */
8985
+ function withSignatureModel(runtime, docPtr, body) {
8986
+ const model = runtime.fn.EPDFSig_LoadModel(docPtr);
8987
+ if (model === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to build signature model");
8988
+ try {
8989
+ return body(model);
8990
+ } finally {
8991
+ runtime.fn.EPDFSig_CloseModel(model);
8992
+ }
8993
+ }
8994
+ /**
8995
+ * The structure of one document (a revision prefix, usually): catalog,
8996
+ * AcroForm and page-tree object numbers, the pages, the terminal fields
8997
+ * with their widgets, and the signatures. What the rule engine needs to
8998
+ * give every changed object a role.
8999
+ */
9000
+ function readStructure(runtime, docPtr) {
9001
+ const { fn, mem } = runtime;
9002
+ const [root, acroForm, pagesRoot] = withScratchN(mem, [
9003
+ 4,
9004
+ 4,
9005
+ 4
9006
+ ], ([r, a, p]) => {
9007
+ mem.poke(r, "i32", 0);
9008
+ mem.poke(a, "i32", 0);
9009
+ mem.poke(p, "i32", 0);
9010
+ fn.EPDFDoc_GetStructureObjectNumbers(docPtr, r, a, p);
9011
+ return [
9012
+ Number(mem.peek(r, "i32")) >>> 0,
9013
+ Number(mem.peek(a, "i32")) >>> 0,
9014
+ Number(mem.peek(p, "i32")) >>> 0
9015
+ ];
9016
+ });
9017
+ const pages = [];
9018
+ const pageCount = fn.FPDF_GetPageCount(docPtr);
9019
+ for (let i = 0; i < pageCount; i++) pages.push(fn.EPDFDoc_GetPageObjectNumberByIndex(docPtr, i));
9020
+ const fields = [];
9021
+ const formModel = fn.EPDFForm_LoadModel(docPtr);
9022
+ if (formModel !== NULL_PTR) try {
9023
+ const snapshot = readFormSnapshot(runtime, formModel, docPtr);
9024
+ for (const f of snapshot.fields) fields.push({
9025
+ objectNumber: f.fieldObjectNumber,
9026
+ name: f.name,
9027
+ family: f.family,
9028
+ widgets: f.widgets.map((w) => w.annotObjectNumber),
9029
+ flags: f.flags.raw
9030
+ });
9031
+ } finally {
9032
+ fn.EPDFForm_CloseModel(formModel);
9033
+ }
9034
+ return {
9035
+ root,
9036
+ acroForm,
9037
+ pagesRoot,
9038
+ pages,
9039
+ fields,
9040
+ signatures: withSignatureModel(runtime, docPtr, (model) => readSignaturesFromModel(runtime, model))
9041
+ };
9042
+ }
9043
+ //#endregion
9044
+ //#region src/features/signature/internal/signatureModelCache.ts
9045
+ /**
9046
+ * Per-session cache of the native `EPDFSig_LoadModel` snapshot, keyed on
9047
+ * the session's mutation sequence exactly like the form model: the model
9048
+ * walks the revision chain, the field tree and every widget, so it is
9049
+ * built once per document state and rebuilt after any mutation.
9050
+ */
9051
+ const cache$1 = /* @__PURE__ */ new WeakMap();
9052
+ function acquireSignatureModel(runtime, session) {
9053
+ const seq = session.mutationSeq();
9054
+ const hit = cache$1.get(session);
9055
+ if (hit && hit.seq === seq) return hit.ptr;
9056
+ if (hit) {
9057
+ runtime.fn.EPDFSig_CloseModel(hit.ptr);
9058
+ cache$1.delete(session);
9059
+ }
9060
+ const ptr = runtime.fn.EPDFSig_LoadModel(session.requireDocPtr());
9061
+ if (ptr === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to build signature model");
9062
+ cache$1.set(session, {
9063
+ seq,
9064
+ ptr
9065
+ });
9066
+ return ptr;
9067
+ }
9068
+ function disposeSignatureModel(runtime, session) {
9069
+ const hit = cache$1.get(session);
9070
+ if (!hit) return;
9071
+ runtime.fn.EPDFSig_CloseModel(hit.ptr);
9072
+ cache$1.delete(session);
9073
+ }
9074
+ //#endregion
9075
+ //#region src/features/signature/SignatureReader.ts
9076
+ const DIGEST_CODE = {
9077
+ sha1: 0,
9078
+ sha256: 1,
9079
+ sha384: 2,
9080
+ sha512: 3
9081
+ };
9082
+ const DIGEST_LENGTH = {
9083
+ sha1: 20,
9084
+ sha256: 32,
9085
+ sha384: 48,
9086
+ sha512: 64
9087
+ };
9088
+ /**
9089
+ * Reads the signature model of a session: revisions, every signature
9090
+ * field with its signed state, and the protection those signatures
9091
+ * impose. Byte facts (revisions, coverage, digests, loaded bytes) come
9092
+ * from the bytes the document was loaded from — for a layer session the
9093
+ * base plus the delta it was opened with — never from unsaved edits.
9094
+ */
9095
+ var SignatureReader = class {
9096
+ constructor(runtime, session) {
9097
+ this.runtime = runtime;
9098
+ this.session = session;
9099
+ }
9100
+ readSnapshot() {
9101
+ const model = acquireSignatureModel(this.runtime, this.session);
9102
+ const chainValid = this.runtime.fn.EPDFSig_IsRevisionChainValid(model);
9103
+ const signatures = readSignaturesFromModel(this.runtime, model);
9104
+ return {
9105
+ chainValid,
9106
+ revisions: chainValid ? readRevisions(this.runtime, this.session.requireDocPtr(), signatures) : [],
9107
+ signatures,
9108
+ protection: deriveProtection(signatures)
9109
+ };
9110
+ }
9111
+ /** The protection alone, for the open probe and the form write guard. */
9112
+ readProtection() {
9113
+ const model = acquireSignatureModel(this.runtime, this.session);
9114
+ return deriveProtection(readSignaturesFromModel(this.runtime, model));
9115
+ }
9116
+ /** The DER `/Contents` of a signed field, padding stripped. */
9117
+ readContents(ref) {
9118
+ const model = acquireSignatureModel(this.runtime, this.session);
9119
+ const index = this.requireSignedIndex(model, ref);
9120
+ return readContentsAt(this.runtime, model, index).buffer;
9121
+ }
9122
+ /** Hash a signed field's `/ByteRange` straight from the loaded bytes. */
9123
+ digest(ref, algorithm) {
9124
+ const model = acquireSignatureModel(this.runtime, this.session);
9125
+ const index = this.requireSignedIndex(model, ref);
9126
+ const range = readByteRange(this.runtime, model, index);
9127
+ if (!range) throw new EngineError(EngineErrorCode.NotFound, "signature has no usable /ByteRange");
9128
+ return this.digestRange(range, algorithm);
9129
+ }
9130
+ /** The exact bytes of one revision. */
9131
+ revisionBytes(revisionIndex) {
9132
+ const snapshot = this.readSnapshot();
9133
+ if (!snapshot.chainValid) throw new EngineError(EngineErrorCode.MalformedPdf, "revision chain is not valid");
9134
+ const revision = snapshot.revisions[revisionIndex];
9135
+ if (!revision) throw new EngineError(EngineErrorCode.NotFound, `no revision ${revisionIndex}`);
9136
+ return this.readLoadedBytes(0, revision.end);
9137
+ }
9138
+ /** Resolve a signature-field ref to its identity in the current model. */
9139
+ resolveField(ref) {
9140
+ const { fn } = this.runtime;
9141
+ const model = acquireSignatureModel(this.runtime, this.session);
9142
+ const index = this.indexOf(model, ref);
9143
+ if (index < 0) throw new EngineError(EngineErrorCode.NotFound, ref.kind === "objectNumber" ? `signature field not found: object ${ref.fieldObjectNumber}` : `signature field not found: "${ref.name}"`);
9144
+ const widgetObjNum = fn.EPDFSig_GetWidgetObjNum(model, index);
9145
+ return {
9146
+ index,
9147
+ fieldObjectNumber: fn.EPDFSig_GetFieldObjNum(model, index),
9148
+ signed: fn.EPDFSig_IsSigned(model, index),
9149
+ widget: widgetObjNum > 0 ? {
9150
+ annotObjectNumber: widgetObjNum,
9151
+ pageObjectNumber: fn.EPDFSig_GetWidgetPageObjNum(model, index)
9152
+ } : null
9153
+ };
9154
+ }
9155
+ /** One signature by field object number, from the current model. */
9156
+ readSignatureByObjectNumber(fieldObjectNumber) {
9157
+ const found = this.readSnapshot().signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === fieldObjectNumber);
9158
+ if (!found) throw new EngineError(EngineErrorCode.NotFound, `signature field not found: object ${fieldObjectNumber}`);
9159
+ return found;
9160
+ }
9161
+ /** The complete loaded bytes (a plain file, or a layer's base + delta). */
9162
+ loadedBytes() {
9163
+ const size = Number(this.runtime.fn.EPDFDoc_GetLoadedBytesSize(this.session.requireDocPtr()));
9164
+ return this.readLoadedBytes(0, size);
9165
+ }
9166
+ /**
9167
+ * SHA-256 and length of the base the session is on. A layer session
9168
+ * reports its base's hash (supplied by the host or computed once by the
9169
+ * runtime); a plain session hashes its loaded bytes once per load.
9170
+ */
9171
+ version() {
9172
+ const { fn, mem } = this.runtime;
9173
+ const docPtr = this.session.requireDocPtr();
9174
+ const byteLength = Number(fn.EPDFDoc_GetBaseBytesSize(docPtr));
9175
+ if (byteLength <= 0) throw new EngineError(EngineErrorCode.DocNotOpen, "document has no loaded bytes");
9176
+ if (this.session.kind === "layer") return {
9177
+ sha256: withScratch(mem, 32, (out) => {
9178
+ if (!fn.EPDFLayer_GetBaseSha256(docPtr, out)) throw new EngineError(EngineErrorCode.Unknown, "failed to read the base hash");
9179
+ return toHex(mem.readBytes(out, 32));
9180
+ }),
9181
+ byteLength
9182
+ };
9183
+ const cached = this.session.cachedPlainSha256();
9184
+ if (cached) return {
9185
+ sha256: cached,
9186
+ byteLength
9187
+ };
9188
+ const sha256 = toHex(new Uint8Array(this.digestRange([
9189
+ 0,
9190
+ byteLength,
9191
+ byteLength,
9192
+ 0
9193
+ ], "sha256")));
9194
+ this.session.rememberPlainSha256(sha256);
9195
+ return {
9196
+ sha256,
9197
+ byteLength
9198
+ };
9199
+ }
9200
+ digestRange(range, algorithm) {
9201
+ const { fn, mem } = this.runtime;
9202
+ const docPtr = this.session.requireDocPtr();
9203
+ const outLength = DIGEST_LENGTH[algorithm];
9204
+ return withScratchN(mem, [
9205
+ 32,
9206
+ outLength,
9207
+ 8
9208
+ ], ([rangePtr, outPtr, lenPtr]) => {
9209
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, range[k], k * 8);
9210
+ pokeU64(mem, lenPtr, outLength);
9211
+ if (!fn.EPDFSig_DigestByteRange(docPtr, rangePtr, DIGEST_CODE[algorithm], outPtr, lenPtr)) throw new EngineError(EngineErrorCode.InvalidArg, "byte range is not within the loaded bytes");
9212
+ const written = Number(mem.peek(lenPtr, "i32"));
9213
+ return copyOut$1(mem.readBytes(outPtr, written));
9214
+ });
9215
+ }
9216
+ /** `[offset, offset + length)` of the loaded bytes (base + loaded delta for a layer), copied out. */
9217
+ readLoadedBytes(offset, length) {
9218
+ const { fn, mem } = this.runtime;
9219
+ const docPtr = this.session.requireDocPtr();
9220
+ if (length === 0) return /* @__PURE__ */ new ArrayBuffer(0);
9221
+ return withScratch(mem, length, (ptr) => {
9222
+ if (fn.EPDFDoc_ReadLoadedBytes(docPtr, BigInt(offset), ptr, length) !== length) throw new EngineError(EngineErrorCode.Unknown, "failed to read the loaded bytes");
9223
+ return copyOut$1(mem.readBytes(ptr, length));
9224
+ });
9225
+ }
9226
+ indexOf(model, ref) {
9227
+ const { fn } = this.runtime;
9228
+ if (ref.kind === "objectNumber") return fn.EPDFSig_GetIndexByFieldObjNum(model, ref.fieldObjectNumber);
9229
+ const count = fn.EPDFSig_Count(model);
9230
+ for (let i = 0; i < count; i++) if (this.readWide((buf, cap) => fn.EPDFSig_GetFieldName(model, i, buf, cap)) === ref.name) return i;
9231
+ return -1;
9232
+ }
9233
+ requireSignedIndex(model, ref) {
9234
+ const { fn } = this.runtime;
9235
+ const index = this.indexOf(model, ref);
9236
+ if (index < 0) throw new EngineError(EngineErrorCode.NotFound, ref.kind === "objectNumber" ? `signature field not found: object ${ref.fieldObjectNumber}` : `signature field not found: "${ref.name}"`);
9237
+ if (!fn.EPDFSig_IsSigned(model, index)) throw new EngineError(EngineErrorCode.NotFound, "signature field is not signed");
9238
+ return index;
9239
+ }
9240
+ readString(model, index, key) {
9241
+ return this.readWide((buf, cap) => this.runtime.fn.EPDFSig_GetString(model, index, key, buf, cap));
9242
+ }
9243
+ readWide(call) {
9244
+ return readUtf16String(this.runtime.mem, call, "");
9245
+ }
9246
+ };
9247
+ function copyOut$1(view) {
9248
+ const buffer = new ArrayBuffer(view.byteLength);
9249
+ new Uint8Array(buffer).set(view);
9250
+ return buffer;
9251
+ }
9252
+ function toHex(bytes) {
9253
+ let out = "";
9254
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
9255
+ return out;
9256
+ }
9257
+ //#endregion
9258
+ //#region src/features/save/DocumentSaver.ts
9259
+ /** The fork's `EPDFSaveStatus`. */
9260
+ const EPDF_SAVE_WRITTEN = 1;
9261
+ const EPDF_SAVE_UNCHANGED_SINCE_LOAD = 2;
9262
+ /**
9263
+ * Owns every "serialize the open document" path for a `DocumentSession`.
9264
+ * Two distinct outputs:
9265
+ *
9266
+ * - Layer artifacts (`saveLayerArtifact*`) are the storage-optimized
9267
+ * `.layer` delta used by server-side persistence. Only valid for a
9268
+ * `layer` session.
9269
+ * - Standalone saves (`saveStandalone*`) export a self-contained PDF
9270
+ * view (base + applied delta) via `FPDF_SaveAsCopy` /
9271
+ * `EPDF_SaveDocumentToOwnedBuffer`, valid for any session kind.
9272
+ *
9273
+ * Lives in `engine-services` so the browser Worker and the Node
9274
+ * `worker_thread` server share the exact same save code path.
9275
+ */
9276
+ var DocumentSaver = class {
9277
+ constructor(runtime, session) {
9278
+ this.runtime = runtime;
9279
+ this.session = session;
9280
+ }
9281
+ /**
9282
+ * The bytes a save would produce, and whether they are the loaded bytes.
9283
+ * Three answers, cheapest first: nothing was mutated since load (the
9284
+ * session's counter); something was mutated but the document is still
9285
+ * the one it was opened with — the fork's save pass found no reachable
9286
+ * object that differs from its loaded version, an annotation added and
9287
+ * removed again being the common case; or a real delta. The one pass
9288
+ * answers the second and produces the third, comparing only the objects
9289
+ * that were touched. A signed file must come back exactly as sealed, so
9290
+ * the loaded bytes are returned verbatim in the first two cases.
9291
+ */
9292
+ snapshot() {
9293
+ if (!this.session.hasUnsavedEdits()) return {
9294
+ bytes: this.loadedBytes(),
9295
+ source: "loaded"
9296
+ };
9297
+ const saved = this.saveStandaloneToBufferEx("incremental");
9298
+ if (saved.unchangedSinceLoad) return {
9299
+ bytes: this.loadedBytes(),
9300
+ source: "loaded"
9301
+ };
9302
+ return {
9303
+ bytes: saved.bytes,
9304
+ source: "working-copy"
9305
+ };
9306
+ }
9307
+ /** The complete loaded bytes: a plain file, or a layer's base plus the delta it was opened with. */
9308
+ loadedBytes() {
9309
+ return new SignatureReader(this.runtime, this.session).loadedBytes();
9310
+ }
9311
+ /**
9312
+ * The loaded bytes streamed to `path` in chunks: a plain file, or a
9313
+ * layer's base plus the delta it was opened with, never the base alone.
9314
+ * Native only. Replaces whatever `path` held.
9315
+ */
9316
+ copyLoadedBytesToFile(path) {
9317
+ const reader = new SignatureReader(this.runtime, this.session);
9318
+ const size = Number(this.runtime.fn.EPDFDoc_GetLoadedBytesSize(this.session.requireDocPtr()));
9319
+ const chunk = 4 * 1024 * 1024;
9320
+ this.runtime.fileWrite.removeFile(path);
9321
+ for (let offset = 0; offset < size; offset += chunk) {
9322
+ const length = Math.min(chunk, size - offset);
9323
+ this.runtime.fileWrite.appendBytes(path, new Uint8Array(reader.readLoadedBytes(offset, length)));
9324
+ }
9325
+ if (size === 0) this.runtime.fileWrite.appendBytes(path, /* @__PURE__ */ new Uint8Array(0));
9326
+ }
9327
+ /**
9328
+ * `snapshot()` onto a file: `path` ends up holding the document a save
9329
+ * would write — the loaded bytes when nothing changed, else the standalone
9330
+ * save — without the document ever being an in-memory buffer. Native only.
9331
+ */
9332
+ snapshotToFile(path) {
9333
+ if (!this.session.hasUnsavedEdits()) {
9334
+ this.copyLoadedBytesToFile(path);
9335
+ return {
9336
+ path,
9337
+ source: "loaded"
9338
+ };
9339
+ }
9340
+ if (this.saveStandaloneToFileEx(path, "incremental").unchangedSinceLoad) {
9341
+ this.copyLoadedBytesToFile(path);
9342
+ return {
9343
+ path,
9344
+ source: "loaded"
9345
+ };
9346
+ }
9347
+ return {
9348
+ path,
9349
+ source: "working-copy"
9350
+ };
9351
+ }
9352
+ /** Whether this runtime and session can put scratch output beside the base file. */
9353
+ canWriteScratchFiles() {
9354
+ return this.runtime.kind === "native" && this.session.source.base?.kind === "file";
9355
+ }
9356
+ saveLayerArtifact() {
9357
+ const saved = this.saveLayerArtifactImpl(false);
9358
+ if (saved.size <= 0) throw layerSaveError(-1);
9359
+ return {
9360
+ bytes: saved.bytes,
9361
+ size: saved.size
9362
+ };
9363
+ }
9364
+ /**
9365
+ * `saveLayerArtifact` that says whether anything reachable changed since
9366
+ * the layer was opened. When nothing did, `bytes` is empty and the
9367
+ * artifact the session was opened with stands. The artifact written
9368
+ * otherwise is cumulative against the base (empty delta when the layer
9369
+ * came to equal its base).
9370
+ */
9371
+ saveLayerArtifactEx() {
9372
+ return this.saveLayerArtifactImpl(true);
9373
+ }
9374
+ /**
9375
+ * `saveLayerArtifactEx` onto a file: nothing reaches `path` when nothing
9376
+ * changed since load. The delta inside is streamed, never held whole.
9377
+ * Native only.
9378
+ */
9379
+ saveLayerArtifactToFileEx(path) {
9380
+ this.requireLayer();
9381
+ return this.saveLayerToFileImpl(path, "artifact");
9382
+ }
9383
+ /** `saveLayerDeltaEx` onto a file: the cumulative delta, or nothing when nothing changed since load. Native only. */
9384
+ saveLayerDeltaToFileEx(path) {
9385
+ this.requireLayer();
9386
+ return this.saveLayerToFileImpl(path, "delta");
9387
+ }
9388
+ saveLayerToFileImpl(path, what) {
9389
+ const { mem, fn } = this.runtime;
9390
+ const statusPtr = mem.alloc(4);
9391
+ const changedPtr = mem.alloc(4);
9392
+ this.runtime.fileWrite.removeFile(path);
9393
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9394
+ try {
9395
+ mem.poke(statusPtr, "i32", -1);
9396
+ mem.poke(changedPtr, "i32", 1);
9397
+ const ok = what === "artifact" ? fn.EPDFLayer_SaveLayerArtifactEx(this.session.requireDocPtr(), writer.ptr, statusPtr, changedPtr) : fn.EPDFLayer_SaveDeltaEx(this.session.requireDocPtr(), writer.ptr, statusPtr, changedPtr);
9398
+ const status = Number(mem.peek(statusPtr, "i32"));
9399
+ if (!ok || status !== 0) throw layerSaveError(status);
9400
+ return {
9401
+ path,
9402
+ changedSinceLoad: Number(mem.peek(changedPtr, "i32")) !== 0
9403
+ };
9404
+ } finally {
9405
+ writer.close();
9406
+ mem.free(changedPtr);
9407
+ mem.free(statusPtr);
9408
+ }
9409
+ }
9410
+ saveLayerArtifactImpl(reporting) {
9411
+ this.requireLayer();
9412
+ const { mem, fn } = this.runtime;
9413
+ const sizePtr = mem.alloc(4);
9414
+ const statusPtr = mem.alloc(4);
9415
+ const changedPtr = mem.alloc(4);
9416
+ let artifactPtr = null;
9417
+ try {
9418
+ mem.poke(sizePtr, "i32", 0);
9419
+ mem.poke(statusPtr, "i32", -1);
9420
+ mem.poke(changedPtr, "i32", 1);
9421
+ artifactPtr = reporting ? fn.EPDFLayer_SaveLayerArtifactToOwnedBufferEx(this.session.requireDocPtr(), sizePtr, statusPtr, changedPtr) : fn.EPDFLayer_SaveLayerArtifactToOwnedBuffer(this.session.requireDocPtr(), sizePtr, statusPtr);
9422
+ const status = Number(mem.peek(statusPtr, "i32"));
9423
+ const size = Number(mem.peek(sizePtr, "i32"));
9424
+ const changedSinceLoad = reporting ? Number(mem.peek(changedPtr, "i32")) !== 0 : true;
9425
+ if (status !== 0) throw layerSaveError(status);
9426
+ if (reporting && !changedSinceLoad) return {
9427
+ bytes: /* @__PURE__ */ new ArrayBuffer(0),
9428
+ size: 0,
9429
+ changedSinceLoad: false
9430
+ };
9431
+ if (!artifactPtr || size <= 0) throw layerSaveError(status);
9432
+ const bytes = mem.readBytes(artifactPtr, size);
9433
+ const buffer = new ArrayBuffer(bytes.byteLength);
9434
+ new Uint8Array(buffer).set(bytes);
9435
+ return {
9436
+ bytes: buffer,
9437
+ size,
9438
+ changedSinceLoad
9439
+ };
9440
+ } finally {
9441
+ if (artifactPtr) fn.EPDF_FreeBuffer(artifactPtr);
9442
+ mem.free(changedPtr);
9443
+ mem.free(statusPtr);
9444
+ mem.free(sizePtr);
9445
+ }
9446
+ }
9447
+ /**
9448
+ * The raw cumulative delta a layer save writes (every promoted object,
9449
+ * offsets notional from the base's append offset), without the artifact
9450
+ * header: what `EPDFDoc_OpenBaseOverlay` composes with the base. Empty
9451
+ * when nothing was promoted.
9452
+ */
9453
+ saveLayerDelta() {
9454
+ const saved = this.saveLayerDeltaImpl(false);
9455
+ return {
9456
+ bytes: saved.bytes,
9457
+ size: saved.size
9458
+ };
9459
+ }
9460
+ /**
9461
+ * `saveLayerDelta` that says whether anything reachable changed since the
9462
+ * layer was opened; empty bytes when nothing did (keep what you have), or
9463
+ * when the layer came to equal its base (then `changedSinceLoad` is true
9464
+ * and the document IS the base).
9465
+ */
9466
+ saveLayerDeltaEx() {
9467
+ return this.saveLayerDeltaImpl(true);
9468
+ }
9469
+ saveLayerDeltaImpl(reporting) {
9470
+ this.requireLayer();
9471
+ const { mem, fn } = this.runtime;
9472
+ const sizePtr = mem.alloc(4);
9473
+ const statusPtr = mem.alloc(4);
9474
+ const changedPtr = mem.alloc(4);
9475
+ let deltaPtr = null;
9476
+ try {
9477
+ mem.poke(sizePtr, "i32", 0);
9478
+ mem.poke(statusPtr, "i32", -1);
9479
+ mem.poke(changedPtr, "i32", 1);
9480
+ deltaPtr = reporting ? fn.EPDFLayer_SaveDeltaToOwnedBufferEx(this.session.requireDocPtr(), sizePtr, statusPtr, changedPtr) : fn.EPDFLayer_SaveDeltaToOwnedBuffer(this.session.requireDocPtr(), sizePtr, statusPtr);
9481
+ const status = Number(mem.peek(statusPtr, "i32"));
9482
+ const size = Number(mem.peek(sizePtr, "i32"));
9483
+ const changedSinceLoad = reporting ? Number(mem.peek(changedPtr, "i32")) !== 0 : true;
9484
+ if (status !== 0) throw layerSaveError(status);
9485
+ if (!deltaPtr || size <= 0) return {
9486
+ bytes: /* @__PURE__ */ new ArrayBuffer(0),
9487
+ size: 0,
9488
+ changedSinceLoad
9489
+ };
9490
+ const bytes = mem.readBytes(deltaPtr, size);
9491
+ const buffer = new ArrayBuffer(bytes.byteLength);
9492
+ new Uint8Array(buffer).set(bytes);
9493
+ return {
9494
+ bytes: buffer,
9495
+ size,
9496
+ changedSinceLoad
9497
+ };
9498
+ } finally {
9499
+ if (deltaPtr) fn.EPDF_FreeBuffer(deltaPtr);
9500
+ mem.free(changedPtr);
9501
+ mem.free(statusPtr);
9502
+ mem.free(sizePtr);
9503
+ }
9504
+ }
9505
+ saveLayerArtifactToFile(path) {
9506
+ this.requireLayer();
9507
+ const { mem, fn } = this.runtime;
9508
+ const statusPtr = mem.alloc(4);
9509
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9510
+ try {
9511
+ mem.poke(statusPtr, "i32", -1);
9512
+ const ok = fn.EPDFLayer_SaveLayerArtifact(this.session.requireDocPtr(), writer.ptr, statusPtr);
9513
+ const status = Number(mem.peek(statusPtr, "i32"));
9514
+ if (!ok || status !== 0) throw layerSaveError(status);
9515
+ return { path };
9516
+ } finally {
9517
+ writer.close();
9518
+ mem.free(statusPtr);
9519
+ }
9520
+ }
9521
+ saveStandaloneToBuffer(mode) {
9522
+ const { mem, fn } = this.runtime;
9523
+ const sizePtr = mem.alloc(4);
9524
+ let pdfPtr = null;
9525
+ try {
9526
+ mem.poke(sizePtr, "i32", 0);
9527
+ pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(this.session.requireDocPtr(), pdfSaveModeFlags(mode), sizePtr);
9528
+ const size = Number(mem.peek(sizePtr, "i32"));
9529
+ if (!pdfPtr || size <= 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9530
+ const bytes = mem.readBytes(pdfPtr, size);
9531
+ const buffer = new ArrayBuffer(bytes.byteLength);
9532
+ new Uint8Array(buffer).set(bytes);
9533
+ return {
9534
+ bytes: buffer,
9535
+ size
9536
+ };
9537
+ } finally {
9538
+ if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
9539
+ mem.free(sizePtr);
9540
+ }
9541
+ }
9542
+ /**
9543
+ * `saveStandaloneToBuffer` that says whether the save wrote anything. An
9544
+ * incremental save of a layer never rewrites an object equal to its base
9545
+ * twin, and when no reachable object differs from the document the layer
9546
+ * was opened with it writes nothing at all: `unchangedSinceLoad`, empty
9547
+ * bytes — the loaded bytes are the document.
9548
+ */
9549
+ saveStandaloneToBufferEx(mode) {
9550
+ const { mem, fn } = this.runtime;
9551
+ const sizePtr = mem.alloc(4);
9552
+ const statusPtr = mem.alloc(4);
9553
+ let pdfPtr = null;
9554
+ try {
9555
+ mem.poke(sizePtr, "i32", 0);
9556
+ mem.poke(statusPtr, "i32", 0);
9557
+ pdfPtr = fn.EPDF_SaveDocumentToOwnedBufferEx(this.session.requireDocPtr(), pdfSaveModeFlags(mode), 0, sizePtr, statusPtr);
9558
+ const status = Number(mem.peek(statusPtr, "i32"));
9559
+ if (status === EPDF_SAVE_UNCHANGED_SINCE_LOAD) return {
9560
+ bytes: /* @__PURE__ */ new ArrayBuffer(0),
9561
+ size: 0,
9562
+ unchangedSinceLoad: true
9563
+ };
9564
+ const size = Number(mem.peek(sizePtr, "i32"));
9565
+ if (!pdfPtr || size <= 0 || status !== EPDF_SAVE_WRITTEN) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9566
+ const bytes = mem.readBytes(pdfPtr, size);
9567
+ const buffer = new ArrayBuffer(bytes.byteLength);
9568
+ new Uint8Array(buffer).set(bytes);
9569
+ return {
9570
+ bytes: buffer,
9571
+ size,
9572
+ unchangedSinceLoad: false
9573
+ };
9574
+ } finally {
9575
+ if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
9576
+ mem.free(statusPtr);
9577
+ mem.free(sizePtr);
9578
+ }
9579
+ }
9580
+ saveStandaloneToFile(path, mode) {
9581
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9582
+ try {
9583
+ if (!this.runtime.fn.FPDF_SaveAsCopy(this.session.requireDocPtr(), writer.ptr, pdfSaveModeFlags(mode))) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9584
+ return { path };
9585
+ } finally {
9586
+ writer.close();
9587
+ }
9588
+ }
9589
+ /**
9590
+ * `saveStandaloneToFile` with the report of `saveStandaloneToBufferEx`.
9591
+ * On `unchangedSinceLoad` no byte reached the file: the caller streams
9592
+ * the loaded bytes instead.
9593
+ */
9594
+ saveStandaloneToFileEx(path, mode) {
9595
+ const { mem, fn } = this.runtime;
9596
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9597
+ const statusPtr = mem.alloc(4);
9598
+ try {
9599
+ mem.poke(statusPtr, "i32", 0);
9600
+ const ok = fn.EPDF_SaveAsCopyEx(this.session.requireDocPtr(), writer.ptr, pdfSaveModeFlags(mode), statusPtr);
9601
+ const status = Number(mem.peek(statusPtr, "i32"));
9602
+ if (!ok || status === 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9603
+ return {
9604
+ path,
9605
+ unchangedSinceLoad: status === EPDF_SAVE_UNCHANGED_SINCE_LOAD
9606
+ };
9607
+ } finally {
9608
+ mem.free(statusPtr);
9609
+ writer.close();
9610
+ }
9611
+ }
9612
+ requireLayer() {
9613
+ if (this.session.kind !== "layer") throw new EngineError(EngineErrorCode.InvalidArg, "document session is not a layer");
9614
+ }
9615
+ };
9616
+ //#endregion
9617
+ //#region src/features/signature/internal/candidateStore.ts
9618
+ function copyOut(view) {
9619
+ const out = new Uint8Array(view.byteLength);
9620
+ out.set(view);
9621
+ return out;
9622
+ }
9623
+ /** Marshal `EPDFSig_SealSpan` over a span held in the runtime heap. */
9624
+ function sealSpanInHeap(runtime, spanPtr, spanLength, objectOffset, fileLength) {
9625
+ const { fn, mem } = runtime;
9626
+ return withScratchN(mem, [
9627
+ 32,
9628
+ 8,
9629
+ 8
9630
+ ], ([rangePtr, coPtr, chPtr]) => {
9631
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, 0, k * 8);
9632
+ pokeU64(mem, coPtr, 0);
9633
+ pokeU64(mem, chPtr, 0);
9634
+ if (!fn.EPDFSig_SealSpan(spanPtr, BigInt(spanLength), BigInt(objectOffset), BigInt(fileLength), rangePtr, coPtr, chPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to seal the signing candidate");
9635
+ return {
9636
+ byteRange: [
9637
+ peekU64(mem, rangePtr, 0),
9638
+ peekU64(mem, rangePtr, 8),
9639
+ peekU64(mem, rangePtr, 16),
9640
+ peekU64(mem, rangePtr, 24)
9641
+ ],
9642
+ contentsOffset: peekU64(mem, coPtr),
9643
+ contentsHexLength: peekU64(mem, chPtr)
9644
+ };
9645
+ });
9646
+ }
9647
+ /** Marshal `EPDFSig_WriteContents` over a buffer in the heap whose first byte is file offset `bufferOffset`. */
9648
+ function writeContentsInHeap(runtime, bufPtr, bufferLength, bufferOffset, sealed, cms) {
9649
+ const { fn, mem } = runtime;
9650
+ const cmsPtr = mem.alloc(cms.byteLength);
9651
+ try {
9652
+ mem.writeBytes(cmsPtr, cms);
9653
+ if (!fn.EPDFSig_WriteContents(bufPtr, BigInt(bufferLength), BigInt(sealed.contentsOffset - bufferOffset), BigInt(sealed.contentsHexLength), cmsPtr, cms.byteLength)) throw new EngineError(EngineErrorCode.SignatureRefused, "the CMS is not one DER object that fits the reserved /Contents");
9654
+ } finally {
9655
+ mem.free(cmsPtr);
9656
+ }
9657
+ }
9658
+ /**
9659
+ * Hex-encode `cms` into the /Contents hole of a candidate FILE, in place.
9660
+ * Only the hole (`<`, the hex digits, `>`) is read and written back; the
9661
+ * delimiters are checked so a wrong offset patches nothing.
9662
+ */
9663
+ function writeContentsIntoFile(runtime, path, hole, cms) {
9664
+ const { mem } = runtime;
9665
+ const span = runtime.fileAccess.readRange(path, hole.holeOffset, hole.hexLength + 2);
9666
+ if (span.byteLength !== hole.hexLength + 2 || span[0] !== 60 || span[span.byteLength - 1] !== 62) throw new EngineError(EngineErrorCode.InvalidArg, "the byteRange gap is not a <…> /Contents hole of the candidate");
9667
+ const spanPtr = mem.alloc(span.byteLength);
9668
+ try {
9669
+ mem.writeBytes(spanPtr, span);
9670
+ writeContentsInHeap(runtime, spanPtr, span.byteLength, hole.holeOffset, {
9671
+ byteRange: [
9672
+ 0,
9673
+ 0,
9674
+ 0,
9675
+ 0
9676
+ ],
9677
+ contentsOffset: hole.holeOffset + 1,
9678
+ contentsHexLength: hole.hexLength,
9679
+ digest: /* @__PURE__ */ new Uint8Array()
9680
+ }, cms);
9681
+ runtime.fileWrite.writeRange(path, hole.holeOffset, mem.readBytes(spanPtr, span.byteLength));
9682
+ } finally {
9683
+ mem.free(spanPtr);
9684
+ }
9685
+ }
9686
+ var MemoryCandidateStore = class {
9687
+ constructor(runtime, baseDocuments) {
9688
+ this.runtime = runtime;
9689
+ this.baseDocuments = baseDocuments;
9690
+ }
9691
+ save(candidateDocPtr, sigObjNum) {
9692
+ const { mem, fn } = this.runtime;
9693
+ return withScratchN(mem, [
9694
+ 8,
9695
+ 8,
9696
+ 8
9697
+ ], ([sizePtr, offPtr, lenPtr]) => {
9698
+ pokeU64(mem, sizePtr, 0);
9699
+ pokeU64(mem, offPtr, 0);
9700
+ pokeU64(mem, lenPtr, 0);
9701
+ const bufPtr = fn.EPDFSig_SaveCandidateToOwnedBuffer(candidateDocPtr, sigObjNum, sizePtr, offPtr, lenPtr);
9702
+ if (!bufPtr) throw new EngineError(EngineErrorCode.Unknown, "failed to save the signing candidate");
9703
+ try {
9704
+ const size = peekU64(mem, sizePtr);
9705
+ return {
9706
+ kind: "memory",
9707
+ bytes: copyOut(mem.readBytes(bufPtr, size)),
9708
+ size,
9709
+ objectOffset: peekU64(mem, offPtr),
9710
+ objectLength: peekU64(mem, lenPtr)
9711
+ };
9712
+ } finally {
9713
+ fn.EPDF_FreeBuffer(bufPtr);
9714
+ }
9715
+ });
9716
+ }
9717
+ seal(saved, algorithm) {
9718
+ if (saved.kind !== "memory") throw new EngineError(EngineErrorCode.InvalidArg, "not a memory candidate");
9719
+ const { mem, fn } = this.runtime;
9720
+ const bufPtr = mem.alloc(saved.bytes.byteLength);
9721
+ try {
9722
+ mem.writeBytes(bufPtr, saved.bytes);
9723
+ return withScratchN(mem, [
9724
+ 32,
9725
+ 8,
9726
+ 8,
9727
+ 64,
9728
+ 8
9729
+ ], ([rangePtr, coPtr, chPtr, digestPtr, lenPtr]) => {
9730
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, 0, k * 8);
9731
+ pokeU64(mem, coPtr, 0);
9732
+ pokeU64(mem, chPtr, 0);
9733
+ pokeU64(mem, lenPtr, 64);
9734
+ if (!fn.EPDFSig_Seal(bufPtr, BigInt(saved.bytes.byteLength), BigInt(saved.objectOffset), BigInt(saved.objectLength), DIGEST_CODE[algorithm], rangePtr, coPtr, chPtr, digestPtr, lenPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to seal the signing candidate");
9735
+ saved.bytes = copyOut(mem.readBytes(bufPtr, saved.bytes.byteLength));
9736
+ const digestLength = Number(mem.peek(lenPtr, "i32"));
9737
+ return {
9738
+ byteRange: [
9739
+ peekU64(mem, rangePtr, 0),
9740
+ peekU64(mem, rangePtr, 8),
9741
+ peekU64(mem, rangePtr, 16),
9742
+ peekU64(mem, rangePtr, 24)
9743
+ ],
9744
+ contentsOffset: peekU64(mem, coPtr),
9745
+ contentsHexLength: peekU64(mem, chPtr),
9746
+ digest: copyOut(mem.readBytes(digestPtr, digestLength))
9747
+ };
9748
+ });
9749
+ } finally {
9750
+ mem.free(bufPtr);
9751
+ }
9752
+ }
9753
+ writeContents(saved, sealed, cms) {
9754
+ if (saved.kind !== "memory") throw new EngineError(EngineErrorCode.InvalidArg, "not a memory candidate");
9755
+ const { mem } = this.runtime;
9756
+ const bufPtr = mem.alloc(saved.bytes.byteLength);
9757
+ try {
9758
+ mem.writeBytes(bufPtr, saved.bytes);
9759
+ writeContentsInHeap(this.runtime, bufPtr, saved.bytes.byteLength, 0, sealed, cms);
9760
+ saved.bytes = copyOut(mem.readBytes(bufPtr, saved.bytes.byteLength));
9761
+ } finally {
9762
+ mem.free(bufPtr);
9763
+ }
9764
+ }
9765
+ openSealed(saved, password) {
9766
+ if (saved.kind !== "memory") throw new EngineError(EngineErrorCode.InvalidArg, "not a memory candidate");
9767
+ const base = this.baseDocuments.acquireMemoryBase({
9768
+ key: `signed:${generateUuid()}`,
9769
+ bytes: saved.bytes,
9770
+ password
9771
+ });
9772
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
9773
+ }
9774
+ discard() {}
9775
+ };
9776
+ /**
9777
+ * A candidate written next to the session's base file, through the
9778
+ * runtime's local-file surface: the base streams through the writer, the
9779
+ * seal patches the object span in place, the digest streams the file, and
9780
+ * the sealed file becomes a file base. Only the object span and the CMS
9781
+ * are ever held in memory. Node runtimes only.
9782
+ */
9783
+ var FileCandidateStore = class {
9784
+ constructor(runtime, baseDocuments, basePath, candidatePath = defaultCandidatePath) {
9785
+ this.runtime = runtime;
9786
+ this.baseDocuments = baseDocuments;
9787
+ this.basePath = basePath;
9788
+ this.candidatePath = candidatePath;
9789
+ }
9790
+ save(candidateDocPtr, sigObjNum, signingId) {
9791
+ const { mem, fn } = this.runtime;
9792
+ const path = this.candidatePath(this.basePath, signingId);
9793
+ this.runtime.fileWrite.removeFile(path);
9794
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9795
+ try {
9796
+ return withScratchN(mem, [
9797
+ 8,
9798
+ 8,
9799
+ 8
9800
+ ], ([sizePtr, offPtr, lenPtr]) => {
9801
+ pokeU64(mem, sizePtr, 0);
9802
+ pokeU64(mem, offPtr, 0);
9803
+ pokeU64(mem, lenPtr, 0);
9804
+ if (!fn.EPDFSig_SaveCandidate(candidateDocPtr, sigObjNum, writer.ptr, sizePtr, offPtr, lenPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to save the signing candidate");
9805
+ return {
9806
+ kind: "file",
9807
+ path,
9808
+ size: peekU64(mem, sizePtr),
9809
+ objectOffset: peekU64(mem, offPtr),
9810
+ objectLength: peekU64(mem, lenPtr)
9811
+ };
9812
+ });
9813
+ } catch (error) {
9814
+ writer.close();
9815
+ this.runtime.fileWrite.removeFile(path);
9816
+ throw error;
9817
+ } finally {
9818
+ writer.close();
9819
+ }
9820
+ }
9821
+ seal(saved, algorithm) {
9822
+ if (saved.kind !== "file") throw new EngineError(EngineErrorCode.InvalidArg, "not a file candidate");
9823
+ const { mem, fn } = this.runtime;
9824
+ const span = this.runtime.fileAccess.readRange(saved.path, saved.objectOffset, saved.objectLength);
9825
+ if (span.byteLength !== saved.objectLength) throw new EngineError(EngineErrorCode.Unknown, "the signing candidate file is shorter than its object span");
9826
+ const spanPtr = mem.alloc(span.byteLength);
9827
+ let plan;
9828
+ try {
9829
+ mem.writeBytes(spanPtr, span);
9830
+ plan = sealSpanInHeap(this.runtime, spanPtr, span.byteLength, saved.objectOffset, saved.size);
9831
+ this.runtime.fileWrite.writeRange(saved.path, saved.objectOffset, mem.readBytes(spanPtr, span.byteLength));
9832
+ } finally {
9833
+ mem.free(spanPtr);
9834
+ }
9835
+ const access = this.runtime.fileAccess.fromNodeFile(saved.path);
9836
+ try {
9837
+ const digest = withScratchN(mem, [
9838
+ 32,
9839
+ 64,
9840
+ 8
9841
+ ], ([rangePtr, digestPtr, lenPtr]) => {
9842
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, plan.byteRange[k], k * 8);
9843
+ pokeU64(mem, lenPtr, 64);
9844
+ if (!fn.EPDFSig_DigestFileRange(access.ptr, rangePtr, DIGEST_CODE[algorithm], digestPtr, lenPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to digest the signing candidate file");
9845
+ return copyOut(mem.readBytes(digestPtr, Number(mem.peek(lenPtr, "i32"))));
9846
+ });
9847
+ return {
9848
+ ...plan,
9849
+ digest
9850
+ };
9851
+ } finally {
9852
+ access.close();
9853
+ }
9854
+ }
9855
+ writeContents(saved, sealed, cms) {
9856
+ if (saved.kind !== "file") throw new EngineError(EngineErrorCode.InvalidArg, "not a file candidate");
9857
+ const { mem } = this.runtime;
9858
+ const span = this.runtime.fileAccess.readRange(saved.path, saved.objectOffset, saved.objectLength);
9859
+ const spanPtr = mem.alloc(span.byteLength);
9860
+ try {
9861
+ mem.writeBytes(spanPtr, span);
9862
+ writeContentsInHeap(this.runtime, spanPtr, span.byteLength, saved.objectOffset, sealed, cms);
9863
+ this.runtime.fileWrite.writeRange(saved.path, saved.objectOffset, mem.readBytes(spanPtr, span.byteLength));
9864
+ } finally {
9865
+ mem.free(spanPtr);
9866
+ }
9867
+ }
9868
+ openSealed(saved, password) {
9869
+ if (saved.kind !== "file") throw new EngineError(EngineErrorCode.InvalidArg, "not a file candidate");
9870
+ const base = this.baseDocuments.acquireFileBase({
9871
+ key: `signed:${saved.path}`,
9872
+ path: saved.path,
9873
+ password
9874
+ });
9875
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
9876
+ }
9877
+ discard(saved) {
9878
+ if (saved.kind === "file") this.runtime.fileWrite.removeFile(saved.path);
9879
+ }
9880
+ };
9881
+ /** Beside the base file, named by the signing; the host may override this (server storage layout). */
9882
+ function defaultCandidatePath(basePath, signingId) {
9883
+ return `${basePath}.signing-${signingId}.pdf`;
9884
+ }
9885
+ /**
9886
+ * A scratch file beside the base: `<base>.<tag>-<id>.<ext>`. Unique per
9887
+ * id, so a file a document holds open is never rewritten; the caller that
9888
+ * names it registers its removal before anything writes it.
9889
+ */
9890
+ function scratchPath(basePath, tag, id, ext) {
9891
+ return `${basePath}.${tag}-${id}.${ext}`;
9892
+ }
9893
+ //#endregion
9894
+ //#region src/features/signature/internal/appearance.ts
9895
+ /** `EPDF_STAMP_FIT_CONTAIN`: uniform scale, letterboxed and centred in the rect. */
9896
+ const FIT_CONTAIN = 0;
9897
+ /**
9898
+ * Draw one page of a PDF into a widget annotation's normal appearance —
9899
+ * what a signature's mark is, whether the field is being signed
9900
+ * (`SignatureMutator`) or only filled visually (`FormMutator`).
9901
+ *
9902
+ * Two fork calls, the same pair the stamp path makes: `SetAppearanceFromPage`
9903
+ * clones the page into the document as a wrapped Form XObject (page content
9904
+ * and resources in a child form, the outer stream a placement matrix), then
9905
+ * `UpdateAppearanceToRect` writes that placement — the page fitted uniformly
9906
+ * into the widget's rect. Without the second call the page sits in its own
9907
+ * box and the viewer's BBox→/Rect mapping stretches it to the rect's aspect.
9908
+ */
9909
+ function bakeWidgetAppearance(runtime, docPtr, widget, pdf, pageIndex) {
9910
+ const { mem, fn } = runtime;
9911
+ const stack = new CloseStack();
9912
+ try {
9913
+ const pagePtr = fn.EPDFDoc_LoadPageByObjectNumber(docPtr, widget.pageObjectNumber);
9914
+ if (!pagePtr) throw new EngineError(EngineErrorCode.NotFound, "the widget page could not be loaded");
9915
+ stack.push(() => fn.FPDF_ClosePage(pagePtr));
9916
+ const annotPtr = fn.EPDFPage_GetAnnotByObjectNumber(pagePtr, widget.annotObjectNumber);
9917
+ if (!annotPtr) throw new EngineError(EngineErrorCode.NotFound, "the signature widget could not be loaded");
9918
+ stack.push(() => fn.FPDFPage_CloseAnnot(annotPtr));
9919
+ const dataPtr = mem.alloc(pdf.byteLength);
9920
+ stack.push(() => mem.free(dataPtr));
9921
+ mem.writeBytes(dataPtr, pdf);
9922
+ const artworkPtr = fn.FPDF_LoadMemDocument64(dataPtr, pdf.byteLength, "");
9923
+ if (!artworkPtr) throw new EngineError(EngineErrorCode.MalformedPdf, "the appearance PDF could not be opened");
9924
+ stack.push(() => fn.FPDF_CloseDocument(artworkPtr));
9925
+ if (!fn.EPDFAnnot_SetAppearanceFromPage(annotPtr, artworkPtr, pageIndex)) throw new EngineError(EngineErrorCode.InvalidArg, "the appearance page could not be drawn into the widget");
9926
+ if (!fn.EPDFAnnot_UpdateAppearanceToRect(annotPtr, FIT_CONTAIN)) throw new EngineError(EngineErrorCode.Unknown, "the appearance page could not be fitted into the widget");
9927
+ } finally {
9928
+ stack.close();
9929
+ }
9930
+ }
9931
+ //#endregion
9932
+ //#region src/features/signature/internal/sealCheck.ts
9933
+ /**
9934
+ * The one check both signing paths make before sealed bytes become a
9935
+ * version — the in-session complete and the server's session-less
9936
+ * finalize. Reading the result back through the ordinary signature model
9937
+ * proves the bytes, not the writer: the field is signed, its signature
9938
+ * covers the whole LAST revision (the candidate's own, never an earlier
9939
+ * one), its /ByteRange is the one the digest was computed over, and the
9940
+ * /Contents decode to exactly the CMS installed. Anything else is refused
9941
+ * and the bytes are never installed.
9942
+ */
9943
+ function assertSealedSignature(runtime, docPtr, model, expected) {
9944
+ const refuse = (why) => new EngineError(EngineErrorCode.SignatureRefused, `the sealed bytes ${why}`);
9945
+ const signatures = readSignaturesFromModel(runtime, model);
9946
+ const signature = signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === expected.fieldObjectNumber);
9947
+ if (!signature) throw refuse("lost the signature field");
9948
+ if (!signature.signed || signature.coverage !== "whole-revision" || !signature.byteRange) throw refuse("do not carry a whole-revision signature on the field");
9949
+ if (!sameRange(signature.byteRange, expected.byteRange)) throw refuse(`carry /ByteRange [${signature.byteRange.join(" ")}] where [${expected.byteRange.join(" ")}] was sealed`);
9950
+ if (!runtime.fn.EPDFSig_IsRevisionChainValid(model)) throw refuse("have a broken cross-reference chain");
9951
+ const revisions = readRevisions(runtime, docPtr, signatures);
9952
+ if (signature.revisionIndex !== revisions.length - 1) throw refuse("seal an earlier revision than the candidate");
9953
+ if (!bytesEqual(readContentsAt(runtime, model, signature.index), expected.cms)) throw refuse("carry /Contents that are not the CMS installed");
9954
+ return signature;
9955
+ }
9956
+ function sameRange(a, b) {
9957
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
9958
+ }
9959
+ function bytesEqual(a, b) {
9960
+ if (a.byteLength !== b.byteLength) return false;
9961
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
9962
+ return true;
9963
+ }
9964
+ //#endregion
9965
+ //#region src/features/forms/internal/wideStringArray.ts
9966
+ /**
9967
+ * Marshal a `const FPDF_WIDESTRING*` array: encode every string into the
9968
+ * runtime heap, write the pointer table with the runtime's pointer width
9969
+ * (bigint pointers cannot be poked into the wasm 32-bit heap), run |body|,
9970
+ * free everything.
9971
+ */
9972
+ function withWideStringArray(runtime, values, body) {
9973
+ const { mem } = runtime;
9974
+ const stringPtrs = values.map((value) => mem.writeU16String(value));
9975
+ const isWasm = runtime.kind === "wasm";
9976
+ const ptrSize = isWasm ? 4 : 8;
9977
+ try {
9978
+ return withScratch(mem, Math.max(1, stringPtrs.length * ptrSize), (arrayPtr) => {
9979
+ stringPtrs.forEach((ptr, i) => isWasm ? mem.poke(arrayPtr, "i32", Number(ptr), i * 4) : mem.poke(arrayPtr, "i64", ptr, i * 8));
9980
+ return body(arrayPtr, stringPtrs.length);
9981
+ });
9982
+ } finally {
9983
+ for (let i = stringPtrs.length - 1; i >= 0; i--) mem.free(stringPtrs[i]);
9984
+ }
9985
+ }
9986
+ //#endregion
9987
+ //#region src/features/signature/SignatureMutator.ts
9988
+ const SUBFILTER_CODE = {
9989
+ "adbe.pkcs7.detached": 0,
9990
+ "ETSI.CAdES.detached": 1,
9991
+ "ETSI.RFC3161": 2
9992
+ };
9993
+ const FIELD_ACTION_CODE = {
9994
+ all: 1,
9995
+ include: 2,
9996
+ exclude: 3
9997
+ };
9998
+ const DEFAULT_CONTENTS_SIZE = 8192;
9999
+ const MIN_CONTENTS_SIZE = 256;
10000
+ const MAX_CONTENTS_SIZE = 4 * 1024 * 1024;
10001
+ const PREPARE_ILP32 = {
10002
+ bytes: 52,
10003
+ ptrBytes: 4,
10004
+ subfilter: 0,
10005
+ digest: 4,
10006
+ contentsSize: 8,
10007
+ name: 12,
10008
+ reason: 16,
10009
+ location: 20,
10010
+ contactInfo: 24,
10011
+ signingTime: 28,
10012
+ docmdpPermission: 32,
10013
+ fieldmdpAction: 36,
10014
+ fieldmdpFields: 40,
10015
+ fieldmdpFieldCount: 44,
10016
+ lockPermission: 48
10017
+ };
10018
+ const PREPARE_LP64 = {
10019
+ bytes: 80,
10020
+ ptrBytes: 8,
10021
+ subfilter: 0,
10022
+ digest: 4,
10023
+ contentsSize: 8,
10024
+ name: 16,
10025
+ reason: 24,
10026
+ location: 32,
10027
+ contactInfo: 40,
10028
+ signingTime: 48,
10029
+ docmdpPermission: 56,
10030
+ fieldmdpAction: 60,
10031
+ fieldmdpFields: 64,
10032
+ fieldmdpFieldCount: 72,
10033
+ lockPermission: 76
10034
+ };
10035
+ /**
10036
+ * The two-phase signing protocol on a session, local and native alike.
10037
+ *
10038
+ * `prepare` never touches the live document: it builds a CANDIDATE — a
10039
+ * fresh layer over the session's own immutable base (see `openCandidate`)
10040
+ * — writes the signature there, saves it through a `CandidateStore`
10041
+ * (memory locally, a file beside the base on file-backed sessions), seals
10042
+ * it, and parks the saved candidate on the session. `complete` writes the
10043
+ * CMS into it, opens it as a new base with a fresh layer, proves the new
10044
+ * signature seals a whole revision, and installs it as the session's
10045
+ * document. `abort` discards the candidate.
10046
+ */
10047
+ var SignatureMutator = class {
10048
+ constructor(runtime, session, baseDocuments, candidatePath) {
10049
+ this.runtime = runtime;
10050
+ this.session = session;
10051
+ this.baseDocuments = baseDocuments;
10052
+ this.candidatePath = candidatePath;
10053
+ }
10054
+ prepare(input) {
10055
+ if (this.session.pendingSigning) throw new EngineError(EngineErrorCode.SigningPending, `a signing is pending (${this.session.pendingSigning.prepared.signingId}); complete or abort it first`);
10056
+ const reader = new SignatureReader(this.runtime, this.session);
10057
+ const field = reader.resolveField(input.field);
10058
+ if (field.signed) throw new EngineError(EngineErrorCode.SignatureRefused, "the signature field is already signed");
10059
+ if (input.appearance && !field.widget) throw new EngineError(EngineErrorCode.SignatureRefused, "the signature field has no widget to carry an appearance");
10060
+ const algorithm = input.digest ?? "sha256";
10061
+ if (!(algorithm in DIGEST_CODE) || algorithm === "sha1") throw new EngineError(EngineErrorCode.SignatureRefused, `unsupported digest '${algorithm}'`);
10062
+ const subFilter = (input.kind ?? "signature") === "timestamp" ? "ETSI.RFC3161" : input.subFilter ?? "ETSI.CAdES.detached";
10063
+ if (!(subFilter in SUBFILTER_CODE)) throw new EngineError(EngineErrorCode.SignatureRefused, `unsupported subFilter '${subFilter}'`);
10064
+ const contentsSize = input.contentsSize ?? DEFAULT_CONTENTS_SIZE;
10065
+ if (!Number.isInteger(contentsSize) || contentsSize < MIN_CONTENTS_SIZE || contentsSize > MAX_CONTENTS_SIZE) throw new EngineError(EngineErrorCode.SignatureRefused, `contentsSize must be an integer in [${MIN_CONTENTS_SIZE}, ${MAX_CONTENTS_SIZE}]`);
10066
+ const expectedVersion = this.session.versionRef(reader.version().sha256);
10067
+ const signingId = generateUuid();
10068
+ const stack = new CloseStack();
10069
+ try {
10070
+ const candidate = this.openCandidate(signingId, stack);
10071
+ stack.push(() => candidate.close());
10072
+ const attribution = input.attribution ?? input.signer;
10073
+ const valueObjNum = this.callPrepare(candidate.docPtr, field.fieldObjectNumber, {
10074
+ subfilter: SUBFILTER_CODE[subFilter],
10075
+ digest: DIGEST_CODE[algorithm],
10076
+ contentsSize,
10077
+ name: attribution?.name ?? null,
10078
+ reason: attribution?.reason ?? null,
10079
+ location: attribution?.location ?? null,
10080
+ contactInfo: attribution?.contactInfo ?? null,
10081
+ signingTime: input.signingTime ?? null,
10082
+ docmdpPermission: input.certify?.permission ?? 0,
10083
+ fieldmdpAction: input.lock ? FIELD_ACTION_CODE[input.lock.action] : 0,
10084
+ fieldmdpFields: input.lock?.action === "all" ? [] : input.lock?.fields ?? [],
10085
+ lockPermission: input.lock?.permission ?? 0
10086
+ });
10087
+ if (valueObjNum === 0) throw new EngineError(EngineErrorCode.SignatureRefused, "the engine refused to author the signature (the field is signed, read-only or locked, a certification is not allowed here, the seed value requires what is not implemented, or the request is inconsistent)");
10088
+ if (input.appearance && field.widget) bakeWidgetAppearance(this.runtime, candidate.docPtr, field.widget, input.appearance.pdf, input.appearance.pageIndex ?? 0);
10089
+ const store = this.storeFor();
10090
+ const saved = store.save(candidate.docPtr, valueObjNum, signingId);
10091
+ let sealed;
10092
+ try {
10093
+ sealed = store.seal(saved, algorithm);
10094
+ } catch (error) {
10095
+ store.discard(saved);
10096
+ throw error;
10097
+ }
10098
+ const prepared = {
10099
+ signingId,
10100
+ digest: sealed.digest,
10101
+ algorithm,
10102
+ byteRange: sealed.byteRange,
10103
+ contentsSize,
10104
+ subFilter,
10105
+ expectedVersion,
10106
+ expiresAt: null
10107
+ };
10108
+ this.session.pendingSigning = {
10109
+ prepared,
10110
+ fieldObjectNumber: field.fieldObjectNumber,
10111
+ saved,
10112
+ contentsOffset: sealed.contentsOffset,
10113
+ contentsHexLength: sealed.contentsHexLength
10114
+ };
10115
+ return prepared;
10116
+ } finally {
10117
+ stack.close();
10118
+ }
10119
+ }
10120
+ complete(input) {
10121
+ const pending = this.session.pendingSigning;
10122
+ if (!pending || pending.prepared.signingId !== input.signingId) {
10123
+ const last = this.session.lastCompletion;
10124
+ if (last && last.signingId === input.signingId) {
10125
+ if (!bytesEqual(last.cms, input.cms)) throw new EngineError(EngineErrorCode.SignatureRefused, "this signing already completed with a different CMS");
10126
+ return {
10127
+ ...last.result,
10128
+ status: "already-completed"
10129
+ };
10130
+ }
10131
+ throw new EngineError(EngineErrorCode.NotFound, `no pending signing '${input.signingId}'`);
10132
+ }
10133
+ if (!sameVersion(pending.prepared.expectedVersion, input.expectedVersion)) throw new EngineError(EngineErrorCode.SigningVersionMismatch, "expectedVersion is not the version the candidate was prepared on");
10134
+ if (input.cms.byteLength === 0 || input.cms[0] !== 48) throw new EngineError(EngineErrorCode.SignatureRefused, "the CMS is not a DER SEQUENCE");
10135
+ if (input.cms.byteLength > pending.prepared.contentsSize) throw new EngineError(EngineErrorCode.SignatureRefused, `the CMS (${input.cms.byteLength} bytes) does not fit the reserved ${pending.prepared.contentsSize} bytes`);
10136
+ const store = this.storeFor();
10137
+ const sealed = {
10138
+ byteRange: pending.prepared.byteRange,
10139
+ contentsOffset: pending.contentsOffset,
10140
+ contentsHexLength: pending.contentsHexLength,
10141
+ digest: pending.prepared.digest
10142
+ };
10143
+ store.writeContents(pending.saved, sealed, input.cms);
10144
+ const handle = store.openSealed(pending.saved, this.session.password);
10145
+ this.verifySealed(handle, {
10146
+ fieldObjectNumber: pending.fieldObjectNumber,
10147
+ byteRange: pending.prepared.byteRange,
10148
+ cms: input.cms
10149
+ });
10150
+ this.session.install(handle);
10151
+ disposeFormModel(this.runtime, this.session);
10152
+ disposeSignatureModel(this.runtime, this.session);
10153
+ const reader = new SignatureReader(this.runtime, this.session);
10154
+ const snapshot = reader.readSnapshot();
10155
+ const signature = snapshot.signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === pending.fieldObjectNumber);
10156
+ if (!signature) throw new EngineError(EngineErrorCode.Unknown, "the installed document lost the signature field");
10157
+ const result = {
10158
+ status: "completed",
10159
+ signature,
10160
+ version: reader.version(),
10161
+ previous: pending.prepared.expectedVersion,
10162
+ protection: snapshot.protection,
10163
+ meta: {
10164
+ affectedPages: this.session.allRecords().map((r) => this.session.pageState(r.pageObjectNumber)),
10165
+ cacheDelta: null
10166
+ }
10167
+ };
10168
+ this.session.lastCompletion = {
10169
+ signingId: input.signingId,
10170
+ cms: input.cms.slice(),
10171
+ result
10172
+ };
10173
+ return result;
10174
+ }
10175
+ abort(signingId) {
10176
+ const pending = this.session.pendingSigning;
10177
+ if (pending && pending.prepared.signingId === signingId) {
10178
+ this.session.pendingSigning = null;
10179
+ this.storeFor().discard(pending.saved);
10180
+ return { status: "aborted" };
10181
+ }
10182
+ if (this.session.lastCompletion?.signingId === signingId) return { status: "already-completed" };
10183
+ return { status: "unknown" };
10184
+ }
10185
+ callPrepare(docPtr, fieldObjectNumber, opts) {
10186
+ const { mem, fn } = this.runtime;
10187
+ const layout = this.runtime.kind === "wasm" ? PREPARE_ILP32 : PREPARE_LP64;
10188
+ const owned = [];
10189
+ const wide = (value) => {
10190
+ if (value === null) return NULL_PTR;
10191
+ const ptr = mem.writeU16String(value);
10192
+ owned.push(ptr);
10193
+ return ptr;
10194
+ };
10195
+ const utf8 = (value) => {
10196
+ if (value === null) return NULL_PTR;
10197
+ const ptr = mem.writeU8String(value);
10198
+ owned.push(ptr);
10199
+ return ptr;
10200
+ };
10201
+ try {
10202
+ return withWideStringArray(this.runtime, opts.fieldmdpFields, (fieldsPtr, fieldCount) => withScratch(mem, layout.bytes, (structPtr) => {
10203
+ for (let off = 0; off < layout.bytes; off += 4) mem.poke(structPtr, "i32", 0, off);
10204
+ const pokePtr = (offset, ptr) => {
10205
+ if (layout.ptrBytes === 4) mem.poke(structPtr, "i32", Number(ptr), offset);
10206
+ else mem.poke(structPtr, "i64", ptr, offset);
10207
+ };
10208
+ mem.poke(structPtr, "i32", opts.subfilter, layout.subfilter);
10209
+ mem.poke(structPtr, "i32", opts.digest, layout.digest);
10210
+ mem.poke(structPtr, "i32", opts.contentsSize, layout.contentsSize);
10211
+ pokePtr(layout.name, wide(opts.name));
10212
+ pokePtr(layout.reason, wide(opts.reason));
10213
+ pokePtr(layout.location, wide(opts.location));
10214
+ pokePtr(layout.contactInfo, wide(opts.contactInfo));
10215
+ pokePtr(layout.signingTime, utf8(opts.signingTime));
10216
+ mem.poke(structPtr, "i32", opts.docmdpPermission, layout.docmdpPermission);
10217
+ mem.poke(structPtr, "i32", opts.fieldmdpAction, layout.fieldmdpAction);
10218
+ pokePtr(layout.fieldmdpFields, fieldCount > 0 ? fieldsPtr : NULL_PTR);
10219
+ mem.poke(structPtr, "i32", fieldCount, layout.fieldmdpFieldCount);
10220
+ mem.poke(structPtr, "i32", opts.lockPermission, layout.lockPermission);
10221
+ return fn.EPDFSig_Prepare(docPtr, fieldObjectNumber, structPtr);
10222
+ }));
10223
+ } finally {
10224
+ for (let i = owned.length - 1; i >= 0; i--) mem.free(owned[i]);
10225
+ }
10226
+ }
10227
+ /** Draw a page of `pdf` into the widget's appearance stream on the candidate. */
10228
+ /**
10229
+ * File-backed sessions (a file base in the registry: the server) persist
10230
+ * the candidate as a file beside the base; everything else keeps it in
10231
+ * memory. The choice follows the SESSION's base, not the candidate's.
10232
+ */
10233
+ storeFor() {
10234
+ const base = this.session.source.base;
10235
+ if (base?.kind === "file") return new FileCandidateStore(this.runtime, this.baseDocuments, base.path, this.candidatePath);
10236
+ return new MemoryCandidateStore(this.runtime, this.baseDocuments);
10237
+ }
10238
+ /**
10239
+ * Prove the sealed bytes carry a whole-revision signature on the field
10240
+ * before anything is installed; closes the handle on failure.
10241
+ */
10242
+ /**
10243
+ * Read the sealed candidate back before it is installed: the checks in
10244
+ * `assertSealedSignature` are made on a model loaded from the sealed
10245
+ * bytes themselves, so a candidate the writer got wrong never becomes
10246
+ * the session's bytes.
10247
+ */
10248
+ verifySealed(handle, expected) {
10249
+ const { fn } = this.runtime;
10250
+ try {
10251
+ const model = fn.EPDFSig_LoadModel(handle.docPtr);
10252
+ if (!model) throw new EngineError(EngineErrorCode.Unknown, "the sealed bytes have no signature model");
10253
+ try {
10254
+ assertSealedSignature(this.runtime, handle.docPtr, model, expected);
10255
+ } finally {
10256
+ fn.EPDFSig_CloseModel(model);
10257
+ }
10258
+ if (this.session.kind !== "layer") this.session.persistLayerArtifact = false;
10259
+ } catch (error) {
10260
+ handle.close();
10261
+ throw error;
10262
+ }
10263
+ }
10264
+ /**
10265
+ * The candidate the signature is authored on. A layer session reopens a
10266
+ * fresh layer over ITS OWN immutable base (a registry retain, no copy of
10267
+ * the file), fed the layer it was opened with — or, with unsaved edits,
10268
+ * the artifact a save would write, which is cumulative: the edits and
10269
+ * the signature then share one revision, as Acrobat saves a
10270
+ * fill-and-sign. The fallback freezes the complete loaded bytes as a new
10271
+ * memory base: for a plain session, and for a layer whose loaded delta
10272
+ * already holds signed bytes (a layer save rewrites the delta and would
10273
+ * drop them; the fork refuses to prepare such a candidate).
10274
+ */
10275
+ openCandidate(signingId, stack) {
10276
+ const password = this.session.password;
10277
+ const source = this.session.source;
10278
+ const saver = new DocumentSaver(this.runtime, this.session);
10279
+ const scratch = saver.canWriteScratchFiles() && source.base?.kind === "file" ? source.base.path : null;
10280
+ if (this.session.kind === "layer" && source.base && !this.loadedDeltaHoldsSignedBytes()) {
10281
+ const base = this.baseDocuments.retainByKey(source.base.key);
10282
+ if (base) {
10283
+ let layer = source.layer ?? { kind: "fresh" };
10284
+ if (this.session.hasUnsavedEdits()) if (scratch) {
10285
+ const path = scratchPath(scratch, "signing", signingId, "layer");
10286
+ stack.push(() => this.runtime.fileWrite.removeFile(path));
10287
+ if (saver.saveLayerArtifactToFileEx(path).changedSinceLoad) layer = {
10288
+ kind: "artifact-file",
10289
+ path
10290
+ };
10291
+ } else {
10292
+ const artifact = saver.saveLayerArtifactEx();
10293
+ if (artifact.changedSinceLoad) layer = {
10294
+ kind: "artifact",
10295
+ bytes: artifact.bytes
10296
+ };
10297
+ }
10298
+ return openLayerDocument(this.runtime, base, layer, password);
10299
+ }
10300
+ }
10301
+ if (scratch) {
10302
+ const path = scratchPath(scratch, "signing", signingId, "base.pdf");
10303
+ stack.push(() => this.runtime.fileWrite.removeFile(path));
10304
+ saver.snapshotToFile(path);
10305
+ const base = this.baseDocuments.acquireFileBase({
10306
+ key: `candidate:${signingId}`,
10307
+ path,
10308
+ password
10309
+ });
10310
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
10311
+ }
10312
+ const candidateBytes = saver.snapshot().bytes;
10313
+ const base = this.baseDocuments.acquireMemoryBase({
10314
+ key: `candidate:${signingId}`,
10315
+ bytes: new Uint8Array(candidateBytes),
10316
+ password
10317
+ });
10318
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
10319
+ }
10320
+ /** Whether a signature's byte range reaches into the layer's loaded delta (past the base). */
10321
+ loadedDeltaHoldsSignedBytes() {
10322
+ const layer = this.session.source.layer;
10323
+ if (!layer || layer.kind === "fresh") return false;
10324
+ const baseSize = Number(this.runtime.fn.EPDFDoc_GetBaseBytesSize(this.session.requireDocPtr()));
10325
+ return new SignatureReader(this.runtime, this.session).readSnapshot().signatures.some((s) => s.signed && s.byteRange !== null && s.byteRange[2] + s.byteRange[3] > baseSize);
10326
+ }
10327
+ };
10328
+ function sameVersion(a, b) {
10329
+ return a.baseSha256 === b.baseSha256 && a.editsVersion === b.editsVersion;
10330
+ }
10331
+ //#endregion
10332
+ //#region src/features/signature/SignatureAnalyzer.ts
10333
+ const CHANGE_BY_CODE = {
10334
+ 0: "added",
10335
+ 1: "modified",
10336
+ 2: "freed"
10337
+ };
10338
+ const KIND_BY_CODE = {
10339
+ 0: "dictionary",
10340
+ 1: "stream",
10341
+ 2: "array",
10342
+ 3: "scalar",
10343
+ 4: "xref",
10344
+ 5: "objstm",
10345
+ 6: "trailer"
10346
+ };
10347
+ const DIFF_OLD = 0;
10348
+ const DIFF_NEW = 1;
10349
+ /** EPDF_DIFF_READ_OK / FAILED / ABSENT. */
10350
+ const READ_BY_CODE = {
10351
+ 0: "ok",
10352
+ 1: "failed",
10353
+ 2: "absent"
10354
+ };
10355
+ /** The objects an edge may be anchored at: the trailer, the structure of the revision, and every changed object. */
10356
+ function anchorSet(s, changed) {
10357
+ const out = /* @__PURE__ */ new Set([
10358
+ 0,
10359
+ s.root,
10360
+ s.pagesRoot,
10361
+ ...s.pages,
10362
+ ...changed
10363
+ ]);
10364
+ if (s.acroForm) out.add(s.acroForm);
10365
+ for (const f of s.fields) {
10366
+ out.add(f.objectNumber);
10367
+ for (const w of f.widgets) out.add(w);
10368
+ }
10369
+ return out;
10370
+ }
10371
+ /**
10372
+ * Modification analysis over a session: which revisions to compare, the
10373
+ * fork's exact object diff for each adjacent pair, each revision's
10374
+ * structure, and the pure rule engine's verdict per step. Every revision
10375
+ * is opened as its own prefix document from the target's loaded bytes;
10376
+ * `working-copy` first snapshots the session the way a save would.
10377
+ */
10378
+ var SignatureAnalyzer = class {
10379
+ constructor(runtime, session) {
10380
+ this.runtime = runtime;
10381
+ this.session = session;
10382
+ }
10383
+ /**
10384
+ * The signature snapshot of the session's working copy: the unsaved
10385
+ * state snapshotted the way a save would write it, as one more revision
10386
+ * over the loaded bytes. Identical to the loaded-bytes snapshot when
10387
+ * nothing is unsaved.
10388
+ */
10389
+ readWorkingCopySnapshot() {
10390
+ const stack = new CloseStack();
10391
+ try {
10392
+ const copy = this.openWorkingCopy(stack);
10393
+ if (copy.source === "loaded") return new SignatureReader(this.runtime, this.session).readSnapshot();
10394
+ const target = copy.target;
10395
+ return withSignatureModel(this.runtime, target, (model) => {
10396
+ const chainValid = this.runtime.fn.EPDFSig_IsRevisionChainValid(model);
10397
+ const signatures = readSignaturesFromModel(this.runtime, model);
10398
+ return {
10399
+ chainValid,
10400
+ revisions: chainValid ? readRevisions(this.runtime, target, signatures) : [],
10401
+ signatures,
10402
+ protection: deriveProtection(signatures)
10403
+ };
10404
+ });
10405
+ } finally {
10406
+ stack.close();
10407
+ }
10408
+ }
10409
+ analyze(input) {
10410
+ const reader = new SignatureReader(this.runtime, this.session);
10411
+ const snapshot = reader.readSnapshot();
10412
+ const version = reader.version();
10413
+ const mode = input.exploratoryLevel ? "exploratory" : "authoritative";
10414
+ const detail = input.detail ?? "summary";
10415
+ let basisSource = "persisted";
10416
+ const editsVersion = this.session.mutationSeq();
10417
+ let sinceRevision;
10418
+ let sinceSignature = null;
10419
+ if ("signatureIndex" in input.since) {
10420
+ const sig = snapshot.signatures[input.since.signatureIndex];
10421
+ if (!sig) throw new EngineError(EngineErrorCode.NotFound, `no signature ${input.since.signatureIndex}`);
10422
+ if (!sig.signed || sig.revisionIndex === null) throw new EngineError(EngineErrorCode.InvalidArg, `signature ${input.since.signatureIndex} seals no whole revision; nothing to analyze from`);
10423
+ sinceRevision = sig.revisionIndex;
10424
+ sinceSignature = sig.index;
10425
+ } else sinceRevision = input.since.revisionIndex;
10426
+ if (!snapshot.chainValid) {
10427
+ const current = {
10428
+ verdict: "indeterminate",
10429
+ complete: false,
10430
+ findings: [{
10431
+ rule: "revision-chain",
10432
+ verdict: "incomplete",
10433
+ objectNumber: 0,
10434
+ detail: "the cross-reference chain is broken or was rebuilt: no revision can be told from another"
10435
+ }],
10436
+ method: "net-state"
10437
+ };
10438
+ return {
10439
+ mode,
10440
+ policyVersion: SIGNATURE_POLICY_VERSION,
10441
+ basis: {
10442
+ version,
10443
+ editsVersion,
10444
+ source: "persisted"
10445
+ },
10446
+ since: {
10447
+ revisionIndex: sinceRevision,
10448
+ signatureIndex: sinceSignature
10449
+ },
10450
+ until: { revisionIndex: sinceRevision },
10451
+ restrictions: [],
10452
+ current: {
10453
+ ...current,
10454
+ primary: current.findings[0]
10455
+ },
10456
+ later: {
10457
+ revisionCount: 0,
10458
+ undoneObjectNumbers: []
10459
+ },
10460
+ verdict: "indeterminate",
10461
+ steps: []
10462
+ };
10463
+ }
10464
+ if (!Number.isInteger(sinceRevision) || sinceRevision < 0 || sinceRevision >= snapshot.revisions.length) throw new EngineError(EngineErrorCode.InvalidArg, `no revision ${sinceRevision}`);
10465
+ const stack = new CloseStack();
10466
+ try {
10467
+ let target = this.session.requireDocPtr();
10468
+ let revisions = snapshot.revisions;
10469
+ const copy = input.until === "working-copy" ? this.openWorkingCopy(stack) : null;
10470
+ if (copy && copy.source === "working-copy") {
10471
+ basisSource = "working-copy";
10472
+ target = copy.target;
10473
+ const signatures = withSignatureModel(this.runtime, target, (m) => readSignaturesFromModel(this.runtime, m));
10474
+ revisions = readRevisions(this.runtime, target, signatures);
10475
+ if (revisions.length === 0) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy has no valid revision chain");
10476
+ }
10477
+ let untilRevision = revisions.length - 1;
10478
+ if (typeof input.until === "object") {
10479
+ untilRevision = input.until.revisionIndex;
10480
+ if (!Number.isInteger(untilRevision) || untilRevision < sinceRevision || untilRevision >= revisions.length) throw new EngineError(EngineErrorCode.InvalidArg, `no revision ${untilRevision} at or after ${sinceRevision}`);
10481
+ }
10482
+ const pair = new CloseStack();
10483
+ let before;
10484
+ let after;
10485
+ let changes = [];
10486
+ let health;
10487
+ try {
10488
+ const sealed = this.openPrefix(target, revisions, sinceRevision, pair);
10489
+ const judged = untilRevision === revisions.length - 1 ? target : this.openPrefix(target, revisions, untilRevision, pair);
10490
+ before = readStructure(this.runtime, sealed);
10491
+ after = untilRevision === sinceRevision ? before : readStructure(this.runtime, judged);
10492
+ if (untilRevision > sinceRevision) ({changes, health} = this.compare(sealed, judged, before, after));
10493
+ } finally {
10494
+ pair.close();
10495
+ }
10496
+ const judgedSignature = sinceSignature === null ? null : before.signatures[sinceSignature] ?? null;
10497
+ const restrictions = restrictionsFor(before, judgedSignature);
10498
+ const levelOverride = input.exploratoryLevel;
10499
+ const net = evaluateStep({
10500
+ older: sinceRevision,
10501
+ newer: untilRevision,
10502
+ changes,
10503
+ before,
10504
+ after,
10505
+ health,
10506
+ restrictions: {
10507
+ level: restrictions.level,
10508
+ locks: restrictions.locks
10509
+ },
10510
+ levelOverride
10511
+ });
10512
+ const certifies = restrictions.anchors.some((r) => r.source === "docmdp" && r.own);
10513
+ const intermediate = untilRevision - sinceRevision;
10514
+ let steps;
10515
+ if (intermediate <= 1) steps = intermediate === 1 ? [net] : [];
10516
+ else if (certifies || detail === "full") steps = this.walkSteps(target, revisions, sinceRevision, untilRevision, before, {
10517
+ level: restrictions.level,
10518
+ locks: restrictions.locks,
10519
+ levelOverride
10520
+ });
10521
+ else steps = [];
10522
+ const current = certifies ? {
10523
+ ...combine(net, steps),
10524
+ method: "net-state+replay"
10525
+ } : {
10526
+ ...assessmentOf(net),
10527
+ method: "net-state"
10528
+ };
10529
+ return {
10530
+ mode,
10531
+ policyVersion: SIGNATURE_POLICY_VERSION,
10532
+ basis: {
10533
+ version,
10534
+ editsVersion,
10535
+ source: basisSource
10536
+ },
10537
+ since: {
10538
+ revisionIndex: sinceRevision,
10539
+ signatureIndex: sinceSignature
10540
+ },
10541
+ until: { revisionIndex: untilRevision },
10542
+ restrictions: restrictions.anchors,
10543
+ current,
10544
+ later: {
10545
+ revisionCount: intermediate,
10546
+ undoneObjectNumbers: changes.filter(sameEffectiveValue).map((c) => c.objectNumber)
10547
+ },
10548
+ verdict: current.verdict,
10549
+ steps
10550
+ };
10551
+ } finally {
10552
+ stack.close();
10553
+ }
10554
+ }
10555
+ /** Revision `index` of `target`'s bytes as its own document, owned by `stack`. */
10556
+ openPrefix(target, revisions, index, stack) {
10557
+ const prefix = this.runtime.fn.EPDFDoc_OpenRevision(target, BigInt(revisions[index].end));
10558
+ if (prefix === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, `revision ${index} does not open as a document`);
10559
+ stack.push(() => this.runtime.fn.FPDF_CloseDocument(prefix));
10560
+ return prefix;
10561
+ }
10562
+ /**
10563
+ * Every pairwise step between `since` and `until`, two prefix documents
10564
+ * open at a time whatever the revision count. `sealedStructure` is the
10565
+ * structure of `since` (already read); every later structure is read once
10566
+ * and dropped with its document.
10567
+ */
10568
+ walkSteps(target, revisions, since, until, sealedStructure, restrictions) {
10569
+ const { fn } = this.runtime;
10570
+ const steps = [];
10571
+ let older = this.openPrefix(target, revisions, since, new CloseStack());
10572
+ let olderStructure = sealedStructure;
10573
+ try {
10574
+ for (let r = since + 1; r <= until; r++) {
10575
+ const newer = r === revisions.length - 1 ? target : this.openPrefix(target, revisions, r, new CloseStack());
10576
+ try {
10577
+ const newerStructure = readStructure(this.runtime, newer);
10578
+ const { changes, health } = this.compare(older, newer, olderStructure, newerStructure);
10579
+ steps.push(evaluateStep({
10580
+ older: r - 1,
10581
+ newer: r,
10582
+ changes,
10583
+ before: olderStructure,
10584
+ after: newerStructure,
10585
+ health,
10586
+ restrictions: {
10587
+ level: restrictions.level,
10588
+ locks: restrictions.locks
10589
+ },
10590
+ levelOverride: restrictions.levelOverride
10591
+ }));
10592
+ olderStructure = newerStructure;
10593
+ } finally {
10594
+ fn.FPDF_CloseDocument(older);
10595
+ older = newer;
10596
+ }
10597
+ }
10598
+ } finally {
10599
+ if (older !== target) fn.FPDF_CloseDocument(older);
10600
+ }
10601
+ return steps;
10602
+ }
10603
+ /**
10604
+ * The document as a save would write it, opened read-only, and whose
10605
+ * bytes that is. `loaded`: nothing was mutated since load, or the saver's
10606
+ * pass found no reachable object that differs from its loaded version —
10607
+ * the session's own document is the working copy. `working-copy`: a
10608
+ * layer session composes its immutable base with the cumulative delta
10609
+ * the pass emits (`EPDFDoc_OpenBaseOverlay`: no copy of the base, one
10610
+ * cross-reference parse, the loaded delta's revision REPLACED, as an
10611
+ * artifact is), or — when every edit brought the layer back to its base
10612
+ * — a fresh layer over that base; a plain session (unsigned by law 9, so
10613
+ * this is exploratory) materialises a standalone incremental save and
10614
+ * opens it whole.
10615
+ */
10616
+ openWorkingCopy(stack) {
10617
+ const { fn, mem } = this.runtime;
10618
+ const docPtr = this.session.requireDocPtr();
10619
+ if (!this.session.hasUnsavedEdits()) return {
10620
+ target: docPtr,
10621
+ source: "loaded"
10622
+ };
10623
+ const saver = new DocumentSaver(this.runtime, this.session);
10624
+ if (this.session.kind === "layer") {
10625
+ const base = this.session.source.base;
10626
+ if (saver.canWriteScratchFiles() && base?.kind === "file") {
10627
+ const path = scratchPath(base.path, "working-copy", generateUuid(), "delta");
10628
+ stack.push(() => this.runtime.fileWrite.removeFile(path));
10629
+ if (!saver.saveLayerDeltaToFileEx(path).changedSinceLoad) return {
10630
+ target: docPtr,
10631
+ source: "loaded"
10632
+ };
10633
+ if (this.runtime.fileAccess.sizeOf(path) === 0) return this.freshLayerOverBase(stack);
10634
+ const overlay = withUtf8CString(mem, path, (pathPtr) => fn.EPDFDoc_OpenBaseOverlayFromPath(docPtr, pathPtr));
10635
+ if (overlay === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy does not compose with its base");
10636
+ stack.push(() => fn.FPDF_CloseDocument(overlay));
10637
+ return {
10638
+ target: overlay,
10639
+ source: "working-copy"
10640
+ };
10641
+ }
10642
+ const delta = saver.saveLayerDeltaEx();
10643
+ if (!delta.changedSinceLoad) return {
10644
+ target: docPtr,
10645
+ source: "loaded"
10646
+ };
10647
+ if (delta.size === 0) return this.freshLayerOverBase(stack);
10648
+ const bytes = new Uint8Array(delta.bytes);
10649
+ const ptr = mem.alloc(bytes.byteLength);
10650
+ try {
10651
+ mem.writeBytes(ptr, bytes);
10652
+ const overlay = fn.EPDFDoc_OpenBaseOverlay(docPtr, ptr, bytes.byteLength);
10653
+ if (overlay === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy does not compose with its base");
10654
+ stack.push(() => fn.FPDF_CloseDocument(overlay));
10655
+ return {
10656
+ target: overlay,
10657
+ source: "working-copy"
10658
+ };
10659
+ } finally {
10660
+ mem.free(ptr);
10661
+ }
10662
+ }
10663
+ const saved = saver.saveStandaloneToBufferEx("incremental");
10664
+ if (saved.unchangedSinceLoad) return {
10665
+ target: docPtr,
10666
+ source: "loaded"
10667
+ };
10668
+ const opened = openFatMemoryDocument(this.runtime, new Uint8Array(saved.bytes), this.session.password);
10669
+ stack.push(() => opened.close());
10670
+ return {
10671
+ target: opened.docPtr,
10672
+ source: "working-copy"
10673
+ };
10674
+ }
10675
+ /** Changed since load, nothing to write: the layer equals its base, and a fresh layer over it IS that document. */
10676
+ freshLayerOverBase(stack) {
10677
+ const { fn, mem } = this.runtime;
10678
+ const base = fn.EPDFLayer_GetBaseDocument(this.session.requireDocPtr());
10679
+ const statusPtr = mem.alloc(4);
10680
+ try {
10681
+ mem.poke(statusPtr, "i32", -1);
10682
+ const fresh = fn.EPDFLayer_OpenLayer(base, NULL_PTR, this.session.password ?? "", statusPtr);
10683
+ if (fresh === NULL_PTR || Number(mem.peek(statusPtr, "i32")) !== 0) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy could not be opened over its base");
10684
+ stack.push(() => fn.FPDF_CloseDocument(fresh));
10685
+ return {
10686
+ target: fresh,
10687
+ source: "working-copy"
10688
+ };
10689
+ } finally {
10690
+ mem.free(statusPtr);
10691
+ }
10692
+ }
10693
+ /**
10694
+ * The fork's exact object diff between two revision documents,
10695
+ * marshalled: values in pass one, anchored edges in pass two (the anchors
10696
+ * are the changed objects plus each side's structural objects).
10697
+ */
10698
+ compare(older, newer, before, after) {
10699
+ const { fn, mem } = this.runtime;
10700
+ const diff = fn.EPDFDoc_CompareRevisions(older, newer);
10701
+ if (diff === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, "the revisions do not share a byte history");
8206
10702
  try {
8207
- mem.poke(sizePtr, "i32", 0);
8208
- pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(this.session.requireDocPtr(), pdfSaveModeFlags(mode), sizePtr);
8209
- const size = Number(mem.peek(sizePtr, "i32"));
8210
- if (!pdfPtr || size <= 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
8211
- const bytes = mem.readBytes(pdfPtr, size);
8212
- const buffer = new ArrayBuffer(bytes.byteLength);
8213
- new Uint8Array(buffer).set(bytes);
10703
+ const count = fn.EPDFObjectDiff_GetCount(diff);
10704
+ const changes = [];
10705
+ withScratchN(mem, [
10706
+ 4,
10707
+ 4,
10708
+ 4,
10709
+ 4,
10710
+ 4,
10711
+ 4
10712
+ ], ([numPtr, changePtr, kindPtr, oldGenPtr, newGenPtr, streamPtr]) => {
10713
+ for (let i = 0; i < count; i++) {
10714
+ for (const p of [
10715
+ numPtr,
10716
+ changePtr,
10717
+ kindPtr,
10718
+ oldGenPtr,
10719
+ newGenPtr,
10720
+ streamPtr
10721
+ ]) mem.poke(p, "i32", 0);
10722
+ if (!fn.EPDFObjectDiff_GetEntry(diff, i, numPtr, changePtr, kindPtr, oldGenPtr, newGenPtr, streamPtr)) throw new EngineError(EngineErrorCode.Unknown, `failed to read diff entry ${i}`);
10723
+ const objectNumber = Number(mem.peek(numPtr, "i32")) >>> 0;
10724
+ const oldGen = Number(mem.peek(oldGenPtr, "i32"));
10725
+ const newGen = Number(mem.peek(newGenPtr, "i32"));
10726
+ const oldValue = this.readValue(diff, i, DIFF_OLD);
10727
+ const newValue = this.readValue(diff, i, DIFF_NEW);
10728
+ let truncated = oldValue.truncated || newValue.truncated;
10729
+ let oldParsed = null;
10730
+ let newParsed = null;
10731
+ try {
10732
+ oldParsed = oldValue.text === null || oldValue.truncated ? null : parsePdfValue(oldValue.text);
10733
+ newParsed = newValue.text === null || newValue.truncated ? null : parsePdfValue(newValue.text);
10734
+ } catch {
10735
+ truncated = true;
10736
+ }
10737
+ const change = CHANGE_BY_CODE[Number(mem.peek(changePtr, "i32"))] ?? "modified";
10738
+ changes.push({
10739
+ objectNumber,
10740
+ change,
10741
+ kind: KIND_BY_CODE[Number(mem.peek(kindPtr, "i32"))] ?? "scalar",
10742
+ present: {
10743
+ old: change !== "added",
10744
+ new: change !== "freed"
10745
+ },
10746
+ generation: {
10747
+ old: oldGen < 0 ? null : oldGen,
10748
+ new: newGen < 0 ? null : newGen
10749
+ },
10750
+ value: {
10751
+ old: oldParsed,
10752
+ new: newParsed,
10753
+ truncated
10754
+ },
10755
+ raw: {
10756
+ old: oldValue.text,
10757
+ new: newValue.text
10758
+ },
10759
+ read: {
10760
+ old: READ_BY_CODE[fn.EPDFObjectDiff_GetReadStatus(diff, i, DIFF_OLD)] ?? "failed",
10761
+ new: READ_BY_CODE[fn.EPDFObjectDiff_GetReadStatus(diff, i, DIFF_NEW)] ?? "failed"
10762
+ },
10763
+ streamDataChanged: Number(mem.peek(streamPtr, "i32")) !== 0,
10764
+ usage: {
10765
+ old: [],
10766
+ new: []
10767
+ }
10768
+ });
10769
+ }
10770
+ });
8214
10771
  return {
8215
- bytes: buffer,
8216
- size
10772
+ changes: this.withAnchoredUsage(diff, changes, before, after),
10773
+ health: {
10774
+ old: this.readHealth(diff, DIFF_OLD),
10775
+ new: this.readHealth(diff, DIFF_NEW)
10776
+ }
8217
10777
  };
8218
10778
  } finally {
8219
- if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
8220
- mem.free(sizePtr);
10779
+ fn.EPDFObjectDiff_Close(diff);
8221
10780
  }
8222
10781
  }
8223
- saveStandaloneToFile(path, mode) {
8224
- const writer = this.runtime.fileWrite.toNodeFile(path);
10782
+ /** What the fork learned about one revision as a whole (see `RevisionHealth`). */
10783
+ readHealth(diff, which) {
10784
+ const { fn, mem } = this.runtime;
10785
+ return withScratchN(mem, [
10786
+ 4,
10787
+ 4,
10788
+ 4
10789
+ ], ([sparsePtr, barePtr, completePtr]) => {
10790
+ for (const p of [
10791
+ sparsePtr,
10792
+ barePtr,
10793
+ completePtr
10794
+ ]) mem.poke(p, "i32", 0);
10795
+ if (!fn.EPDFObjectDiff_GetRevisionHealth(diff, which, sparsePtr, barePtr, completePtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to read revision health");
10796
+ return {
10797
+ sparseXref: Number(mem.peek(sparsePtr, "i32")) !== 0,
10798
+ bareReferences: Number(mem.peek(barePtr, "i32")) >>> 0,
10799
+ referrersComplete: Number(mem.peek(completePtr, "i32")) !== 0
10800
+ };
10801
+ });
10802
+ }
10803
+ /**
10804
+ * Pass two: every changed object's inbound edges, resolved through
10805
+ * unchanged non-structural objects to the nearest anchor (the fork's
10806
+ * referrer index covers every reachable object, so the walk never leaves
10807
+ * the diff). An object whose edges exceed the resolver's budget is
10808
+ * marked `usageIncomplete` and the evaluator refuses to permit it.
10809
+ */
10810
+ withAnchoredUsage(diff, changes, before, after) {
10811
+ const changed = new Set(changes.map((c) => c.objectNumber));
10812
+ const resolvers = {
10813
+ old: new EdgeResolver((n) => this.readReferrers(diff, DIFF_OLD, n), anchorSet(before, changed)),
10814
+ new: new EdgeResolver((n) => this.readReferrers(diff, DIFF_NEW, n), anchorSet(after, changed))
10815
+ };
10816
+ return changes.map((c) => {
10817
+ const old = c.present.old ? resolvers.old.resolve(c.objectNumber) : [];
10818
+ const now = c.present.new ? resolvers.new.resolve(c.objectNumber) : [];
10819
+ const incomplete = old === USAGE_INCOMPLETE || now === USAGE_INCOMPLETE;
10820
+ return {
10821
+ ...c,
10822
+ usage: {
10823
+ old: old === USAGE_INCOMPLETE ? [] : old,
10824
+ new: now === USAGE_INCOMPLETE ? [] : now
10825
+ },
10826
+ ...incomplete ? { usageIncomplete: true } : {}
10827
+ };
10828
+ });
10829
+ }
10830
+ readValue(diff, index, which) {
10831
+ const { fn, mem } = this.runtime;
10832
+ return withScratch(mem, 4, (truncPtr) => {
10833
+ mem.poke(truncPtr, "i32", 0);
10834
+ const length = fn.EPDFObjectDiff_GetValue(diff, index, which, NULL_PTR, 0, truncPtr);
10835
+ const truncatedProbe = Number(mem.peek(truncPtr, "i32")) !== 0;
10836
+ if (length <= 0) return {
10837
+ text: null,
10838
+ truncated: truncatedProbe
10839
+ };
10840
+ return withScratch(mem, length, (buf) => {
10841
+ mem.poke(truncPtr, "i32", 0);
10842
+ const written = fn.EPDFObjectDiff_GetValue(diff, index, which, buf, length, truncPtr);
10843
+ const truncated = Number(mem.peek(truncPtr, "i32")) !== 0;
10844
+ if (written <= 0) return {
10845
+ text: null,
10846
+ truncated
10847
+ };
10848
+ const bytes = mem.readBytes(buf, written - 1);
10849
+ let text = "";
10850
+ for (const b of bytes) text += String.fromCharCode(b);
10851
+ return {
10852
+ text,
10853
+ truncated
10854
+ };
10855
+ });
10856
+ });
10857
+ }
10858
+ readReferrers(diff, which, objectNumber) {
10859
+ const { fn, mem } = this.runtime;
10860
+ const count = fn.EPDFObjectDiff_GetReferrerCount(diff, which, objectNumber);
10861
+ const out = [];
10862
+ if (count <= 0) return out;
10863
+ withScratch(mem, 4, (parentPtr) => {
10864
+ for (let k = 0; k < count; k++) {
10865
+ mem.poke(parentPtr, "i32", 0);
10866
+ const length = fn.EPDFObjectDiff_GetReferrer(diff, which, objectNumber, k, parentPtr, NULL_PTR, 0);
10867
+ const label = length <= 0 ? "" : withScratch(mem, length, (buf) => {
10868
+ const written = fn.EPDFObjectDiff_GetReferrer(diff, which, objectNumber, k, parentPtr, buf, length);
10869
+ if (written <= 0) return "";
10870
+ const bytes = mem.readBytes(buf, written - 1);
10871
+ let text = "";
10872
+ for (const b of bytes) text += String.fromCharCode(b);
10873
+ return text;
10874
+ });
10875
+ out.push({
10876
+ parent: Number(mem.peek(parentPtr, "i32")) >>> 0,
10877
+ label
10878
+ });
10879
+ }
10880
+ });
10881
+ return out;
10882
+ }
10883
+ };
10884
+ //#endregion
10885
+ //#region src/features/signature/CandidateFinalizer.ts
10886
+ /**
10887
+ * The session-less half of a durable signing. A server prepares a
10888
+ * candidate on one replica, keeps only its tail, and may complete it on
10889
+ * another: the completing replica rebuilds base ⊕ tail into a private
10890
+ * file and hands it here with what the prepare reported. The CMS is
10891
+ * hex-encoded into the /Contents hole in place — never more than the
10892
+ * hole itself is held in memory — and the file is then opened like any
10893
+ * other base and read back through the signature model, so the version
10894
+ * returned is the file's own hash and the signature is what the file
10895
+ * says, not what the writer meant. Nothing here touches a live session.
10896
+ */
10897
+ var CandidateFinalizer = class {
10898
+ constructor(runtime, baseDocuments) {
10899
+ this.runtime = runtime;
10900
+ this.baseDocuments = baseDocuments;
10901
+ }
10902
+ finalize(input) {
10903
+ const [r0, r1, r2, r3] = input.byteRange;
10904
+ const size = this.runtime.fileAccess.sizeOf(input.path);
10905
+ const isOffset = (n) => Number.isSafeInteger(n) && n >= 0;
10906
+ if (!input.byteRange.every(isOffset) || r0 !== 0 || r1 >= r2 || r2 + r3 !== size) throw new EngineError(EngineErrorCode.InvalidArg, `byteRange [${input.byteRange.join(" ")}] does not span a ${size}-byte candidate`);
10907
+ const hexLength = r2 - r1 - 2;
10908
+ if (!Number.isSafeInteger(input.contentsSize) || hexLength !== 2 * input.contentsSize) throw new EngineError(EngineErrorCode.InvalidArg, `the /Contents hole holds ${hexLength} hex digits, not the ${2 * input.contentsSize} of contentsSize`);
10909
+ if (input.cms.byteLength === 0 || input.cms[0] !== 48) throw new EngineError(EngineErrorCode.SignatureRefused, "the CMS is not a DER SEQUENCE");
10910
+ if (input.cms.byteLength > input.contentsSize) throw new EngineError(EngineErrorCode.SignatureRefused, `the CMS (${input.cms.byteLength} bytes) does not fit the reserved ${input.contentsSize} bytes`);
10911
+ writeContentsIntoFile(this.runtime, input.path, {
10912
+ holeOffset: r1,
10913
+ hexLength
10914
+ }, input.cms);
10915
+ const session = new DocumentSession(this.runtime);
10916
+ const base = this.baseDocuments.acquireFileBase({
10917
+ key: `finalize:${generateUuid()}`,
10918
+ path: input.path,
10919
+ password: input.password
10920
+ });
8225
10921
  try {
8226
- if (!this.runtime.fn.FPDF_SaveAsCopy(this.session.requireDocPtr(), writer.ptr, pdfSaveModeFlags(mode))) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
8227
- return { path };
10922
+ session.openFromHandle(openLayerDocument(this.runtime, base, { kind: "fresh" }, input.password));
10923
+ const model = acquireSignatureModel(this.runtime, session);
10924
+ assertSealedSignature(this.runtime, session.requireDocPtr(), model, {
10925
+ fieldObjectNumber: input.fieldObjectNumber,
10926
+ byteRange: input.byteRange,
10927
+ cms: input.cms
10928
+ });
10929
+ const reader = new SignatureReader(this.runtime, session);
10930
+ const snapshot = reader.readSnapshot();
10931
+ return {
10932
+ signature: snapshot.signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === input.fieldObjectNumber),
10933
+ protection: snapshot.protection,
10934
+ version: reader.version()
10935
+ };
8228
10936
  } finally {
8229
- writer.close();
10937
+ session.close();
8230
10938
  }
8231
10939
  }
8232
- requireLayer() {
8233
- if (this.session.kind !== "layer") throw new EngineError(EngineErrorCode.InvalidArg, "document session is not a layer");
8234
- }
8235
10940
  };
8236
10941
  //#endregion
8237
10942
  //#region src/features/fonts/FontRegistrar.ts
10943
+ /** `EPDF_FONT_EMBEDDING_*` codes. Restricted (3) and bitmap-only (4) are
10944
+ * refused at registration, so they never reach a handle. */
10945
+ function permissionFromCode(code) {
10946
+ switch (code) {
10947
+ case 1: return "editable";
10948
+ case 2: return "preview-and-print";
10949
+ default: return "installable";
10950
+ }
10951
+ }
10952
+ /** The engine's family comparison: no case, spaces, hyphens, underscores. */
10953
+ function familyKey(family) {
10954
+ return family.toLowerCase().replace(/[\s\-_'"]/g, "");
10955
+ }
8238
10956
  /**
8239
10957
  * Thread-confined runtime font registry binding.
8240
10958
  *
@@ -8258,6 +10976,7 @@ var FontRegistrar = class {
8258
10976
  this.runtime = runtime;
8259
10977
  this.ids = ids;
8260
10978
  _defineProperty(this, "fileHandles", []);
10979
+ _defineProperty(this, "identities", /* @__PURE__ */ new Map());
8261
10980
  }
8262
10981
  /**
8263
10982
  * Register a font from its bytes. Copies into the WASM heap, hands the
@@ -8276,6 +10995,11 @@ var FontRegistrar = class {
8276
10995
  const id = fn.EPDFFont_RegisterMemFont64(familyName, weight, italic, ptr, bytes.byteLength);
8277
10996
  if (id === 0) throw new EngineError(EngineErrorCode.InvalidArg, `font registration failed (unloadable or no glyphs): ${fontKey}`);
8278
10997
  this.ids.set(fontKey, id);
10998
+ this.identities.set(fontKey, this.readIdentity(id, {
10999
+ familyName,
11000
+ weight,
11001
+ italic
11002
+ }));
8279
11003
  } finally {
8280
11004
  mem.free(ptr);
8281
11005
  }
@@ -8301,8 +11025,72 @@ var FontRegistrar = class {
8301
11025
  throw new EngineError(EngineErrorCode.InvalidArg, `font registration failed (unloadable or no glyphs): ${fontKey} (${path})`);
8302
11026
  }
8303
11027
  this.ids.set(fontKey, id);
11028
+ this.identities.set(fontKey, this.readIdentity(id, {
11029
+ familyName,
11030
+ weight,
11031
+ italic
11032
+ }));
8304
11033
  this.fileHandles.push(access);
8305
11034
  }
11035
+ /** The identity and licence the runtime resolved for a key. Throws if unknown. */
11036
+ describe(fontKey) {
11037
+ const identity = this.identities.get(fontKey);
11038
+ if (!identity) throw new EngineError(EngineErrorCode.InvalidArg, `font not registered on this thread: ${fontKey}`);
11039
+ return identity;
11040
+ }
11041
+ /** {@link describe} for callers that treat an unknown key as "not a key". */
11042
+ describeOrUndefined(fontKey) {
11043
+ return this.identities.get(fontKey);
11044
+ }
11045
+ /**
11046
+ * The key of the registered font whose identity matches a face (family
11047
+ * compared the engine's way; the closest weight, italic first), or
11048
+ * undefined when no registered family matches.
11049
+ */
11050
+ keyForFace(family, weight, italic) {
11051
+ const wanted = familyKey(family);
11052
+ let best;
11053
+ for (const [key, identity] of this.identities) {
11054
+ if (familyKey(identity.familyName) !== wanted) continue;
11055
+ const score = Math.abs(identity.weight - weight) + (identity.italic === italic ? 0 : 1e3);
11056
+ if (!best || score < best.score) best = {
11057
+ key,
11058
+ score
11059
+ };
11060
+ }
11061
+ return best?.key;
11062
+ }
11063
+ /** The application asserts a licence permitting editing with the font. */
11064
+ authorizeEditing(fontKey) {
11065
+ const id = this.requireId(fontKey);
11066
+ if (!this.runtime.fn.EPDFFont_AuthorizeEditing(id)) throw new EngineError(EngineErrorCode.InvalidArg, `authorizeEditing failed: ${fontKey}`);
11067
+ const previous = this.describe(fontKey);
11068
+ const identity = this.readIdentity(id, {
11069
+ familyName: previous.familyName,
11070
+ weight: previous.weight,
11071
+ italic: previous.italic ? 1 : 0
11072
+ });
11073
+ this.identities.set(fontKey, identity);
11074
+ return identity;
11075
+ }
11076
+ readIdentity(id, given) {
11077
+ const { fn, mem } = this.runtime;
11078
+ const attempt = (read, fallback) => {
11079
+ try {
11080
+ return read();
11081
+ } catch {
11082
+ return fallback;
11083
+ }
11084
+ };
11085
+ return {
11086
+ familyName: attempt(() => readUtf8String(mem, (buf, cap) => fn.EPDFFont_GetFamilyName(id, buf, cap)) ?? given.familyName, given.familyName),
11087
+ weight: attempt(() => fn.EPDFFont_GetWeight(id), given.weight || 400),
11088
+ italic: attempt(() => fn.EPDFFont_IsItalic(id), given.italic === 1),
11089
+ embeddingPermission: attempt(() => permissionFromCode(fn.EPDFFont_GetEmbeddingPermission(id)), "installable"),
11090
+ editingAuthorized: attempt(() => fn.EPDFFont_IsEditingAuthorized(id), true),
11091
+ instanced: attempt(() => fn.EPDFFont_IsInstanced(id), false)
11092
+ };
11093
+ }
8306
11094
  /**
8307
11095
  * Register host-owned startup fonts on this thread, in order, optionally
8308
11096
  * adding each to the fallback chain. Order matters: it fixes both the native
@@ -8328,6 +11116,7 @@ var FontRegistrar = class {
8328
11116
  clear() {
8329
11117
  this.runtime.fn.EPDFFont_ClearRegisteredFonts();
8330
11118
  this.ids.clear();
11119
+ this.identities.clear();
8331
11120
  for (const access of this.fileHandles) access.close();
8332
11121
  this.fileHandles.length = 0;
8333
11122
  }
@@ -8480,35 +11269,14 @@ function flagMasks(flags) {
8480
11269
  };
8481
11270
  }
8482
11271
  //#endregion
8483
- //#region src/features/forms/internal/wideStringArray.ts
8484
- /**
8485
- * Marshal a `const FPDF_WIDESTRING*` array: encode every string into the
8486
- * runtime heap, write the pointer table with the runtime's pointer width
8487
- * (bigint pointers cannot be poked into the wasm 32-bit heap), run |body|,
8488
- * free everything.
8489
- */
8490
- function withWideStringArray(runtime, values, body) {
8491
- const { mem } = runtime;
8492
- const stringPtrs = values.map((value) => mem.writeU16String(value));
8493
- const isWasm = runtime.kind === "wasm";
8494
- const ptrSize = isWasm ? 4 : 8;
8495
- try {
8496
- return withScratch(mem, Math.max(1, stringPtrs.length * ptrSize), (arrayPtr) => {
8497
- stringPtrs.forEach((ptr, i) => isWasm ? mem.poke(arrayPtr, "i32", Number(ptr), i * 4) : mem.poke(arrayPtr, "i64", ptr, i * 8));
8498
- return body(arrayPtr, stringPtrs.length);
8499
- });
8500
- } finally {
8501
- for (let i = stringPtrs.length - 1; i >= 0; i--) mem.free(stringPtrs[i]);
8502
- }
8503
- }
8504
- //#endregion
8505
11272
  //#region src/features/forms/FormMutator.ts
8506
11273
  const FAMILY_CODE = {
8507
11274
  checkbox: 2,
8508
11275
  radio: 3,
8509
11276
  text: 4,
8510
11277
  combobox: 5,
8511
- listbox: 6
11278
+ listbox: 6,
11279
+ signature: 7
8512
11280
  };
8513
11281
  /** Widgets a single value write can touch; far above any real form. */
8514
11282
  const CHANGED_WIDGETS_CAPACITY$1 = 256;
@@ -8541,7 +11309,7 @@ var FormMutator = class {
8541
11309
  throwIfAborted(signal);
8542
11310
  const model = acquireFormModel(this.runtime, this.session);
8543
11311
  const resolved = resolveFieldRef(this.runtime, model, ref);
8544
- this.assertWritable(resolved.fieldObjectNumber);
11312
+ this.assertWritable(resolved);
8545
11313
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8546
11314
  if (!FAMILY_BY_VALUE_TYPE[value.type].includes(before.family)) throw new EngineError(EngineErrorCode.InvalidArg, `value type '${value.type}' does not apply to a '${before.family}' field`);
8547
11315
  const changed = this.applyWrite(resolved.fieldObjectNumber, value);
@@ -8551,7 +11319,7 @@ var FormMutator = class {
8551
11319
  throwIfAborted(signal);
8552
11320
  const model = acquireFormModel(this.runtime, this.session);
8553
11321
  const resolved = resolveFieldRef(this.runtime, model, ref);
8554
- this.assertWritable(resolved.fieldObjectNumber);
11322
+ this.assertWritable(resolved);
8555
11323
  const changed = this.withChangedWidgets((buf, cap, countPtr) => this.runtime.fn.EPDFForm_ResetField(this.session.requireDocPtr(), resolved.fieldObjectNumber, buf, cap, countPtr));
8556
11324
  if (changed === null) throw new EngineError(EngineErrorCode.InvalidArg, "form field cannot be reset");
8557
11325
  return this.readBack(resolved.fieldObjectNumber, changed);
@@ -8644,13 +11412,39 @@ var FormMutator = class {
8644
11412
  this.session.noteMutation();
8645
11413
  return { field: this.readBackField(fieldObjectNumber) };
8646
11414
  }
11415
+ /**
11416
+ * Draw a PDF page into every widget of an UNSIGNED signature field: the
11417
+ * visual "sign" of a viewer without a signer. The field's value stays
11418
+ * empty and nothing is sealed; a signed field is refused (its appearance
11419
+ * is part of what the signature covers). Pages whose widgets changed are
11420
+ * reported so their renders re-pin.
11421
+ */
11422
+ setSignatureAppearance(ref, pdf, pageIndex, signal) {
11423
+ throwIfAborted(signal);
11424
+ const docPtr = this.session.requireDocPtr();
11425
+ const model = acquireFormModel(this.runtime, this.session);
11426
+ const resolved = resolveFieldRef(this.runtime, model, ref);
11427
+ this.assertWritable(resolved);
11428
+ const before = readFieldAt(this.runtime, model, resolved.fieldIndex, docPtr);
11429
+ if (before.family !== "signature") throw new EngineError(EngineErrorCode.InvalidArg, `'${before.name}' is a ${before.family} field, not a signature field`);
11430
+ if (withSignatureModel(this.runtime, docPtr, (signatures) => readSignaturesFromModel(this.runtime, signatures).some((s) => s.signed && s.field.kind === "objectNumber" && s.field.fieldObjectNumber === resolved.fieldObjectNumber))) throw new EngineError(EngineErrorCode.InvalidArg, `'${before.name}' is signed; its appearance is sealed with the signature`);
11431
+ if (before.widgets.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, `'${before.name}' has no widget to draw into`);
11432
+ for (const widget of before.widgets) bakeWidgetAppearance(this.runtime, docPtr, widget, pdf, pageIndex);
11433
+ this.session.noteMutation();
11434
+ const pages = [...new Set(before.widgets.map((w) => w.pageObjectNumber))];
11435
+ for (const pon of pages) this.session.bumpRevision(pon);
11436
+ return {
11437
+ field: this.readBackField(resolved.fieldObjectNumber),
11438
+ pages
11439
+ };
11440
+ }
8647
11441
  updateField(ref, patch, signal) {
8648
11442
  throwIfAborted(signal);
8649
11443
  const { fn } = this.runtime;
8650
11444
  const docPtr = this.session.requireDocPtr();
8651
11445
  const model = acquireFormModel(this.runtime, this.session);
8652
11446
  const resolved = resolveFieldRef(this.runtime, model, ref);
8653
- this.assertWritable(resolved.fieldObjectNumber);
11447
+ this.assertWritable(resolved);
8654
11448
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8655
11449
  if (before.family !== patch.family) throw new EngineError(EngineErrorCode.InvalidArg, `patch family '${patch.family}' does not match field family '${before.family}'`);
8656
11450
  const fieldObjectNumber = resolved.fieldObjectNumber;
@@ -8681,7 +11475,7 @@ var FormMutator = class {
8681
11475
  const { fn, mem } = this.runtime;
8682
11476
  const model = acquireFormModel(this.runtime, this.session);
8683
11477
  const resolved = resolveFieldRef(this.runtime, model, ref);
8684
- this.assertWritable(resolved.fieldObjectNumber);
11478
+ this.assertWritable(resolved);
8685
11479
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8686
11480
  if (!withScratchN(mem, [256 * 4, 4], ([buf, countPtr]) => {
8687
11481
  mem.poke(countPtr, "i32", 0);
@@ -8701,7 +11495,7 @@ var FormMutator = class {
8701
11495
  const { fn } = this.runtime;
8702
11496
  const model = acquireFormModel(this.runtime, this.session);
8703
11497
  const resolved = resolveFieldRef(this.runtime, model, ref);
8704
- this.assertWritable(resolved.fieldObjectNumber);
11498
+ this.assertWritable(resolved);
8705
11499
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8706
11500
  const toggle = before.family === "checkbox" || before.family === "radio";
8707
11501
  const state = toggle ? onState ?? (before.family === "checkbox" ? "Yes" : "") : "";
@@ -8715,7 +11509,7 @@ var FormMutator = class {
8715
11509
  const { fn } = this.runtime;
8716
11510
  const model = acquireFormModel(this.runtime, this.session);
8717
11511
  const resolved = resolveFieldRef(this.runtime, model, ref);
8718
- this.assertWritable(resolved.fieldObjectNumber);
11512
+ this.assertWritable(resolved);
8719
11513
  if (!fn.EPDFForm_DetachWidget(this.session.requireDocPtr(), resolved.fieldObjectNumber, widget.annotObjectNumber)) throw new EngineError(EngineErrorCode.InvalidArg, "widget is not attached to this field");
8720
11514
  this.session.noteMutation();
8721
11515
  return { field: this.readBackField(resolved.fieldObjectNumber) };
@@ -8750,8 +11544,30 @@ var FormMutator = class {
8750
11544
  if (fieldIndex < 0) throw new EngineError(EngineErrorCode.Unknown, "form field vanished after write");
8751
11545
  return readFieldAt(this.runtime, fresh, fieldIndex, this.session.requireDocPtr());
8752
11546
  }
8753
- assertWritable(fieldObjectNumber) {
8754
- if (fieldObjectNumber === 0) throw new EngineError(EngineErrorCode.InvalidArg, "form field is stored as a direct object and cannot be written");
11547
+ assertWritable(resolved) {
11548
+ if (resolved.fieldObjectNumber === 0) throw new EngineError(EngineErrorCode.InvalidArg, "form field is stored as a direct object and cannot be written");
11549
+ this.assertNotLockedBySignature(resolved);
11550
+ }
11551
+ /**
11552
+ * A field an earlier signature froze (its FieldMDP, or the /Lock of a
11553
+ * signed field) refuses every write: document-derived authority, the
11554
+ * same way encryption bits are. Off under `signedDocumentPolicy:
11555
+ * 'permit'`. A document whose signature model cannot be built is not
11556
+ * known to be locked.
11557
+ */
11558
+ assertNotLockedBySignature(resolved) {
11559
+ if (this.session.signedDocumentPolicy !== "protect") return;
11560
+ let protection;
11561
+ try {
11562
+ protection = new SignatureReader(this.runtime, this.session).readProtection();
11563
+ } catch {
11564
+ return;
11565
+ }
11566
+ if (protection.fieldLocks.length === 0) return;
11567
+ const model = acquireFormModel(this.runtime, this.session);
11568
+ const name = readUtf16String(this.runtime.mem, (buf, cap) => this.runtime.fn.EPDFForm_GetFieldName(model, resolved.fieldIndex, buf, cap)) ?? "";
11569
+ const lock = fieldLockFor(protection, name);
11570
+ if (lock) throw new EngineError(EngineErrorCode.ProtectedDocument, `form field "${name}" is locked by signature ${lock.signatureIndex} (${lock.source === "fieldmdp" ? "FieldMDP" : "/Lock"})`);
8755
11571
  }
8756
11572
  /** Dispatch the typed native write. Returns the changed widget objnums. */
8757
11573
  applyWrite(fieldObjectNumber, value) {
@@ -9598,6 +12414,7 @@ var WorkerHost = class {
9598
12414
  this.aborts.set(msg.jobId, ctrl);
9599
12415
  let resultPack;
9600
12416
  try {
12417
+ this.assertNoPendingSigning(msg);
9601
12418
  switch (msg.kind) {
9602
12419
  case "open.fatMem":
9603
12420
  case "open.layerMemBase":
@@ -9637,6 +12454,36 @@ var WorkerHost = class {
9637
12454
  case "annotations.move":
9638
12455
  resultPack = this.handleAnnotationsMove(msg, ctrl.signal);
9639
12456
  break;
12457
+ case "signatures.list":
12458
+ resultPack = this.handleSignaturesList(msg);
12459
+ break;
12460
+ case "signatures.contents":
12461
+ resultPack = this.handleSignaturesContents(msg);
12462
+ break;
12463
+ case "signatures.digest":
12464
+ resultPack = this.handleSignaturesDigest(msg);
12465
+ break;
12466
+ case "signatures.revisionBytes":
12467
+ resultPack = this.handleSignaturesRevisionBytes(msg);
12468
+ break;
12469
+ case "document.version":
12470
+ resultPack = this.handleDocumentVersion(msg);
12471
+ break;
12472
+ case "signatures.prepare":
12473
+ resultPack = this.handleSignaturesPrepare(msg);
12474
+ break;
12475
+ case "signatures.complete":
12476
+ resultPack = this.handleSignaturesComplete(msg);
12477
+ break;
12478
+ case "signatures.abort":
12479
+ resultPack = this.handleSignaturesAbort(msg);
12480
+ break;
12481
+ case "signatures.analyze":
12482
+ resultPack = this.handleSignaturesAnalyze(msg);
12483
+ break;
12484
+ case "signatures.finalizeCandidate":
12485
+ resultPack = this.handleSignaturesFinalizeCandidate(msg);
12486
+ break;
9640
12487
  case "forms.list":
9641
12488
  resultPack = this.handleFormsList(msg, ctrl.signal);
9642
12489
  break;
@@ -9664,6 +12511,9 @@ var WorkerHost = class {
9664
12511
  case "forms.updateField":
9665
12512
  resultPack = this.handleFormsUpdateField(msg, ctrl.signal);
9666
12513
  break;
12514
+ case "forms.setSignatureAppearance":
12515
+ resultPack = this.handleFormsSetSignatureAppearance(msg, ctrl.signal);
12516
+ break;
9667
12517
  case "forms.deleteField":
9668
12518
  resultPack = this.handleFormsDeleteField(msg, ctrl.signal);
9669
12519
  break;
@@ -9685,6 +12535,18 @@ var WorkerHost = class {
9685
12535
  case "pages.delete":
9686
12536
  resultPack = this.handlePagesDelete(msg, ctrl.signal);
9687
12537
  break;
12538
+ case "pages.setName":
12539
+ resultPack = this.handlePagesSetName(msg, ctrl.signal);
12540
+ break;
12541
+ case "annotations.flatten":
12542
+ resultPack = this.handleAnnotationsFlatten(msg, ctrl.signal);
12543
+ break;
12544
+ case "annotations.exportAppearance":
12545
+ resultPack = this.handleAnnotationsExportAppearance(msg, ctrl.signal);
12546
+ break;
12547
+ case "pages.removeName":
12548
+ resultPack = this.handlePagesRemoveName(msg, ctrl.signal);
12549
+ break;
9688
12550
  case "pages.flatten":
9689
12551
  resultPack = this.handlePagesFlatten(msg, ctrl.signal);
9690
12552
  break;
@@ -9769,6 +12631,12 @@ var WorkerHost = class {
9769
12631
  case "fonts.clear":
9770
12632
  resultPack = this.handleFontsClear(msg);
9771
12633
  break;
12634
+ case "fonts.authorizeEditing":
12635
+ resultPack = this.handleFontsAuthorizeEditing(msg);
12636
+ break;
12637
+ case "document.setFontSettings":
12638
+ resultPack = this.handleDocumentSetFontSettings(msg);
12639
+ break;
9772
12640
  case "layer.close":
9773
12641
  resultPack = this.handleLayerClose(msg);
9774
12642
  break;
@@ -9800,10 +12668,13 @@ var WorkerHost = class {
9800
12668
  const key = sessionKey(req.docId, req.kind === "open.fatMem" ? void 0 : req.layerName);
9801
12669
  if (this.sessions.has(key)) throw new EngineError(EngineErrorCode.InvalidArg, `document session already open: ${key}`);
9802
12670
  const session = new DocumentSession(this.runtime);
12671
+ session.signedDocumentPolicy = req.signedDocumentPolicy ?? "protect";
12672
+ session.password = req.password;
9803
12673
  if (req.kind === "open.fatMem") {
12674
+ session.sessionKind = req.sessionKind ?? "layer";
9804
12675
  const bytes = new Uint8Array(req.bytes);
9805
12676
  try {
9806
- session.openFromHandle(openFatMemoryDocument(this.runtime, bytes, req.password));
12677
+ this.openSignedAware(session, bytes, req.password);
9807
12678
  } catch (error) {
9808
12679
  if (!isPasswordOpenError(error)) throw error;
9809
12680
  session.parkLocked(bytes);
@@ -9818,14 +12689,16 @@ var WorkerHost = class {
9818
12689
  const base = this.baseDocuments.acquireMemoryBase({
9819
12690
  key: req.baseKey,
9820
12691
  bytes: new Uint8Array(req.baseBytes),
9821
- password: req.password
12692
+ password: req.password,
12693
+ knownSha256: req.baseSha256
9822
12694
  });
9823
12695
  session.openFromHandle(openLayerDocument(this.runtime, base, req.layer, req.password));
9824
12696
  } else {
9825
12697
  const base = this.baseDocuments.acquireFileBase({
9826
12698
  key: req.baseKey,
9827
12699
  path: req.basePath,
9828
- password: req.password
12700
+ password: req.password,
12701
+ knownSha256: req.baseSha256
9829
12702
  });
9830
12703
  session.openFromHandle(openLayerDocument(this.runtime, base, req.layer, req.password));
9831
12704
  }
@@ -9833,9 +12706,160 @@ var WorkerHost = class {
9833
12706
  return wirePack({
9834
12707
  tag: "open",
9835
12708
  docId: req.docId,
9836
- security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password ?? "")
12709
+ security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password ?? ""),
12710
+ protection: this.probeProtection(session)
12711
+ });
12712
+ }
12713
+ /**
12714
+ * Open plain bytes into `session` in the shape it asked for. The default,
12715
+ * `layer`, makes the bytes an immutable base with a fresh layer on top:
12716
+ * reads fall through to the base, a write promotes only what it touches,
12717
+ * a save appends exactly that, and a signature keeps its validity across
12718
+ * later edits because the signature dictionary is never rewritten. The
12719
+ * `plain` shape keeps one in-memory document — unless the bytes already
12720
+ * carry a signature value, which always opens as a layer, because a plain
12721
+ * save rewrites every loaded object into the new revision and strict
12722
+ * validators reject that. Auto-layered sessions serialize no artifact per
12723
+ * mutation: the caller never asked for one.
12724
+ */
12725
+ openSignedAware(session, bytes, password) {
12726
+ if (session.sessionKind === "plain") {
12727
+ const plain = openFatMemoryDocument(this.runtime, bytes, password);
12728
+ let signed = false;
12729
+ try {
12730
+ signed = hasSignedSignature(this.runtime, plain.docPtr);
12731
+ } catch {
12732
+ signed = false;
12733
+ }
12734
+ if (!signed) {
12735
+ session.openFromHandle(plain);
12736
+ return;
12737
+ }
12738
+ plain.close();
12739
+ }
12740
+ const base = this.baseDocuments.acquireMemoryBase({
12741
+ key: `signed-open:${generateUuid()}`,
12742
+ bytes,
12743
+ password
12744
+ });
12745
+ session.openFromHandle(openLayerDocument(this.runtime, base, { kind: "fresh" }, password));
12746
+ session.persistLayerArtifact = false;
12747
+ }
12748
+ /**
12749
+ * What the document's signatures forbid, read at open and after an
12750
+ * unlock so the main-thread guard can subtract capabilities before the
12751
+ * first mutation. A document whose signature model cannot be built
12752
+ * still opens: protection is then `null` (nothing is known to be
12753
+ * signed), and the reads report the failure on their own.
12754
+ */
12755
+ probeProtection(session) {
12756
+ try {
12757
+ return new SignatureReader(this.runtime, session).readProtection();
12758
+ } catch {
12759
+ return null;
12760
+ }
12761
+ }
12762
+ handleSignaturesList(req) {
12763
+ const session = this.requireSession(req);
12764
+ return wirePack({
12765
+ tag: "signatures.list",
12766
+ snapshot: req.workingCopy ? new SignatureAnalyzer(this.runtime, session).readWorkingCopySnapshot() : new SignatureReader(this.runtime, session).readSnapshot()
12767
+ });
12768
+ }
12769
+ handleSignaturesContents(req) {
12770
+ const session = this.requireSession(req);
12771
+ const bytes = new SignatureReader(this.runtime, session).readContents(req.ref);
12772
+ return wirePack({
12773
+ tag: "signatures.contents",
12774
+ bytes
12775
+ }, [bytes]);
12776
+ }
12777
+ handleSignaturesDigest(req) {
12778
+ const session = this.requireSession(req);
12779
+ const digest = new SignatureReader(this.runtime, session).digest(req.ref, req.algorithm);
12780
+ return wirePack({
12781
+ tag: "signatures.digest",
12782
+ digest
12783
+ }, [digest]);
12784
+ }
12785
+ handleSignaturesRevisionBytes(req) {
12786
+ const session = this.requireSession(req);
12787
+ const bytes = new SignatureReader(this.runtime, session).revisionBytes(req.revisionIndex);
12788
+ return wirePack({
12789
+ tag: "signatures.revisionBytes",
12790
+ bytes,
12791
+ size: bytes.byteLength
12792
+ }, [bytes]);
12793
+ }
12794
+ handleDocumentVersion(req) {
12795
+ const session = this.requireSession(req);
12796
+ return wirePack({
12797
+ tag: "document.version",
12798
+ version: new SignatureReader(this.runtime, session).version()
12799
+ });
12800
+ }
12801
+ handleSignaturesPrepare(req) {
12802
+ const session = this.requireSession(req);
12803
+ return wirePack({
12804
+ tag: "signatures.prepare",
12805
+ result: new SignatureMutator(this.runtime, session, this.baseDocuments, this.options.signingCandidatePath).prepare(req.input)
12806
+ });
12807
+ }
12808
+ handleSignaturesComplete(req) {
12809
+ const session = this.requireSession(req);
12810
+ const result = new SignatureMutator(this.runtime, session, this.baseDocuments, this.options.signingCandidatePath).complete(req.input);
12811
+ if (result.status === "already-completed") return wirePack({
12812
+ tag: "signatures.complete",
12813
+ result
12814
+ });
12815
+ return this.finishMutation(session, {
12816
+ tag: "signatures.complete",
12817
+ result
12818
+ }, req.artifactPath);
12819
+ }
12820
+ handleSignaturesAnalyze(req) {
12821
+ const session = this.requireSession(req);
12822
+ return wirePack({
12823
+ tag: "signatures.analyze",
12824
+ analysis: new SignatureAnalyzer(this.runtime, session).analyze(req.input)
12825
+ });
12826
+ }
12827
+ /**
12828
+ * Session-less, like `document.renderPageFile`: the candidate file is
12829
+ * the caller's, patched in place and read back through a transient
12830
+ * session that is never stored in `this.sessions`.
12831
+ */
12832
+ handleSignaturesFinalizeCandidate(req) {
12833
+ return wirePack({
12834
+ tag: "signatures.finalizeCandidate",
12835
+ ...new CandidateFinalizer(this.runtime, this.baseDocuments).finalize({
12836
+ path: req.path,
12837
+ byteRange: req.byteRange,
12838
+ contentsSize: req.contentsSize,
12839
+ fieldObjectNumber: req.fieldObjectNumber,
12840
+ cms: new Uint8Array(req.cms),
12841
+ password: req.password ?? null
12842
+ })
9837
12843
  });
9838
12844
  }
12845
+ handleSignaturesAbort(req) {
12846
+ const session = this.requireSession(req);
12847
+ return wirePack({
12848
+ tag: "signatures.abort",
12849
+ result: new SignatureMutator(this.runtime, session, this.baseDocuments, this.options.signingCandidatePath).abort(req.signingId)
12850
+ });
12851
+ }
12852
+ /**
12853
+ * The dispatch fence for a parked signing candidate. Only mutating kinds
12854
+ * are refused; a request for a session that is not open falls through
12855
+ * to its handler's own `DocNotOpen`.
12856
+ */
12857
+ assertNoPendingSigning(msg) {
12858
+ if (!MUTATING_KINDS.has(msg.kind) || !("docId" in msg)) return;
12859
+ const layerName = "layerName" in msg ? msg.layerName : void 0;
12860
+ const session = this.sessions.get(sessionKey(msg.docId, layerName));
12861
+ if (session?.pendingSigning) throw new EngineError(EngineErrorCode.SigningPending, `a signing is pending (${session.pendingSigning.prepared.signingId}); complete or abort it before mutating the document`);
12862
+ }
9839
12863
  handleMetadataRead(req, signal) {
9840
12864
  const session = this.requireSession(req);
9841
12865
  return wirePack({
@@ -9862,21 +12886,21 @@ var WorkerHost = class {
9862
12886
  const session = this.requireSession(req);
9863
12887
  return wirePack({
9864
12888
  tag: "annotations.listRawAll",
9865
- snapshot: new RawAnnotationReader(this.runtime, session).listAll(signal)
12889
+ snapshot: new RawAnnotationReader(this.runtime, session, this.fonts).listAll(signal)
9866
12890
  });
9867
12891
  }
9868
12892
  handleAnnotationsListRawPage(req, signal) {
9869
12893
  const session = this.requireSession(req);
9870
12894
  return wirePack({
9871
12895
  tag: "annotations.listRawPage",
9872
- snapshot: new RawAnnotationReader(this.runtime, session).listOne(req.pageObjectNumber, signal)
12896
+ snapshot: new RawAnnotationReader(this.runtime, session, this.fonts).listOne(req.pageObjectNumber, signal)
9873
12897
  });
9874
12898
  }
9875
12899
  handleAnnotationsListFullPage(req, signal) {
9876
12900
  const session = this.requireSession(req);
9877
12901
  return wirePack({
9878
12902
  tag: "annotations.listFullPage",
9879
- snapshot: new AnnotationReader(this.runtime, session).list(req.pageObjectNumber, signal)
12903
+ snapshot: new AnnotationReader(this.runtime, session, this.fonts).list(req.pageObjectNumber, signal)
9880
12904
  });
9881
12905
  }
9882
12906
  handleAnnotationsRenderAppearances(req, signal) {
@@ -9912,6 +12936,27 @@ var WorkerHost = class {
9912
12936
  result
9913
12937
  }, req.artifactPath);
9914
12938
  }
12939
+ handleAnnotationsFlatten(req, signal) {
12940
+ const session = this.requireSession(req);
12941
+ const result = new AnnotationFlattener(this.runtime, session).flatten(req.pageObjectNumber, req.refs, req.usage, signal);
12942
+ if (result.meta === null) return wirePack({
12943
+ tag: "annotations.flatten",
12944
+ result
12945
+ });
12946
+ return this.finishMutation(session, {
12947
+ tag: "annotations.flatten",
12948
+ result
12949
+ }, req.artifactPath);
12950
+ }
12951
+ handleAnnotationsExportAppearance(req, signal) {
12952
+ const session = this.requireSession(req);
12953
+ const exported = new AnnotationFlattener(this.runtime, session).exportAppearance(req.pageObjectNumber, req.refs, signal);
12954
+ return wirePack({
12955
+ tag: "annotations.exportAppearance",
12956
+ bytes: exported.bytes,
12957
+ size: exported.size
12958
+ }, [exported.bytes]);
12959
+ }
9915
12960
  handleAnnotationsMove(req, signal) {
9916
12961
  const session = this.requireSession(req);
9917
12962
  const result = new AnnotationMutator(this.runtime, session).move(req.pageObjectNumber, req.refs, req.toIndex, signal);
@@ -9951,6 +12996,26 @@ var WorkerHost = class {
9951
12996
  result
9952
12997
  }, req.artifactPath);
9953
12998
  }
12999
+ handlePagesSetName(req, signal) {
13000
+ const session = this.requireSession(req);
13001
+ const result = new PagesMutator(this.runtime, session).setName({
13002
+ name: req.name,
13003
+ pageObjectNumber: req.pageObjectNumber,
13004
+ ...req.replace !== void 0 ? { replace: req.replace } : {}
13005
+ }, signal);
13006
+ return this.finishMutation(session, {
13007
+ tag: "pages.setName",
13008
+ result
13009
+ }, req.artifactPath);
13010
+ }
13011
+ handlePagesRemoveName(req, signal) {
13012
+ const session = this.requireSession(req);
13013
+ const result = new PagesMutator(this.runtime, session).removeName({ name: req.name }, signal);
13014
+ return this.finishMutation(session, {
13015
+ tag: "pages.removeName",
13016
+ result
13017
+ }, req.artifactPath);
13018
+ }
9954
13019
  handlePagesFlatten(req, signal) {
9955
13020
  const session = this.requireSession(req);
9956
13021
  const result = new PagesFlattener(this.runtime, session).flatten(req.pageObjectNumbers, req.usage, signal);
@@ -10196,6 +13261,14 @@ var WorkerHost = class {
10196
13261
  }
10197
13262
  handleDocumentSaveBuffer(req) {
10198
13263
  const session = this.requireSession(req);
13264
+ if (req.mode === "incremental") {
13265
+ const snap = new DocumentSaver(this.runtime, session).snapshot();
13266
+ return wirePack({
13267
+ tag: "document.saveBuffer",
13268
+ bytes: snap.bytes,
13269
+ size: snap.bytes.byteLength
13270
+ }, [snap.bytes]);
13271
+ }
10199
13272
  const saved = new DocumentSaver(this.runtime, session).saveStandaloneToBuffer(req.mode);
10200
13273
  return wirePack({
10201
13274
  tag: "document.saveBuffer",
@@ -10205,6 +13278,17 @@ var WorkerHost = class {
10205
13278
  }
10206
13279
  handleDocumentSaveFile(req) {
10207
13280
  const session = this.requireSession(req);
13281
+ if (req.mode === "incremental" && (!session.hasUnsavedEdits() || new DocumentSaver(this.runtime, session).saveStandaloneToFileEx(req.path, req.mode).unchangedSinceLoad)) {
13282
+ new DocumentSaver(this.runtime, session).copyLoadedBytesToFile(req.path);
13283
+ return wirePack({
13284
+ tag: "document.saveFile",
13285
+ path: req.path
13286
+ });
13287
+ }
13288
+ if (req.mode === "incremental") return wirePack({
13289
+ tag: "document.saveFile",
13290
+ path: req.path
13291
+ });
10208
13292
  return wirePack({
10209
13293
  tag: "document.saveFile",
10210
13294
  path: new DocumentSaver(this.runtime, session).saveStandaloneToFile(req.path, req.mode).path
@@ -10261,21 +13345,47 @@ var WorkerHost = class {
10261
13345
  const parked = this.sessions.get(sessionKey(req.docId));
10262
13346
  let session;
10263
13347
  if (parked?.isLocked()) {
10264
- parked.openFromHandle(openFatMemoryDocument(this.runtime, parked.lockedBytes(), req.password));
13348
+ this.openSignedAware(parked, parked.lockedBytes(), req.password);
13349
+ parked.password = req.password;
10265
13350
  session = parked;
10266
13351
  } else session = this.requireSession(req);
10267
13352
  return wirePack({
10268
13353
  tag: "document.checkPasswordPermissions",
10269
- security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password, req.mode ?? "any")
13354
+ security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password, req.mode ?? "any"),
13355
+ protection: this.probeProtection(session)
10270
13356
  });
10271
13357
  }
10272
13358
  handleFontsRegister(req) {
10273
13359
  this.fonts.register(req.fontKey, req.familyName, req.weight, req.italic, new Uint8Array(req.data));
10274
13360
  return wirePack({
10275
13361
  tag: "fonts.register",
10276
- fontKey: req.fontKey
13362
+ fontKey: req.fontKey,
13363
+ identity: this.fonts.describe(req.fontKey)
13364
+ });
13365
+ }
13366
+ handleFontsAuthorizeEditing(req) {
13367
+ return wirePack({
13368
+ tag: "fonts.authorizeEditing",
13369
+ identity: this.fonts.authorizeEditing(req.fontKey)
10277
13370
  });
10278
13371
  }
13372
+ /** Per-document font and text-layout settings (session state). */
13373
+ handleDocumentSetFontSettings(req) {
13374
+ const docPtr = this.requireSession(req).requireDocPtr();
13375
+ const { fn } = this.runtime;
13376
+ if (req.embeddingPolicy !== void 0) {
13377
+ const code = {
13378
+ default: 0,
13379
+ subset: 1,
13380
+ full: 2
13381
+ }[req.embeddingPolicy];
13382
+ if (!fn.EPDFDoc_SetFontEmbeddingPolicy(docPtr, code)) throw new EngineError(EngineErrorCode.InvalidArg, "EPDFDoc_SetFontEmbeddingPolicy failed");
13383
+ }
13384
+ if (req.typographicFeatures !== void 0) {
13385
+ if (!fn.EPDFDoc_SetTypographicFeatures(docPtr, req.typographicFeatures)) throw new EngineError(EngineErrorCode.InvalidArg, "EPDFDoc_SetTypographicFeatures failed");
13386
+ }
13387
+ return wirePack({ tag: "document.setFontSettings" });
13388
+ }
10279
13389
  handleFontsAddFallback(req) {
10280
13390
  this.fonts.addFallback(req.fontKey);
10281
13391
  return wirePack({ tag: "fonts.addFallback" });
@@ -10301,6 +13411,7 @@ var WorkerHost = class {
10301
13411
  const session = this.sessions.get(key);
10302
13412
  if (session) {
10303
13413
  disposeFormModel(this.runtime, session);
13414
+ disposeSignatureModel(this.runtime, session);
10304
13415
  session.close();
10305
13416
  this.sessions.delete(key);
10306
13417
  }
@@ -10310,6 +13421,7 @@ var WorkerHost = class {
10310
13421
  for (const [key, session] of Array.from(this.sessions.entries())) {
10311
13422
  if (!sessionKeyBelongsToDoc(key, req.docId)) continue;
10312
13423
  disposeFormModel(this.runtime, session);
13424
+ disposeSignatureModel(this.runtime, session);
10313
13425
  session.close();
10314
13426
  this.sessions.delete(key);
10315
13427
  }
@@ -10320,6 +13432,7 @@ var WorkerHost = class {
10320
13432
  this.destroyed = true;
10321
13433
  for (const session of this.sessions.values()) {
10322
13434
  disposeFormModel(this.runtime, session);
13435
+ disposeSignatureModel(this.runtime, session);
10323
13436
  session.close();
10324
13437
  }
10325
13438
  this.sessions.clear();
@@ -10425,6 +13538,21 @@ var WorkerHost = class {
10425
13538
  }
10426
13539
  }, req.artifactPath);
10427
13540
  }
13541
+ handleFormsSetSignatureAppearance(req, signal) {
13542
+ const session = this.requireSession(req);
13543
+ const { field, pages } = new FormMutator(this.runtime, session).setSignatureAppearance(req.ref, new Uint8Array(req.pdf), req.pageIndex, signal);
13544
+ const meta = {
13545
+ affectedPages: pages.map((pon) => session.pageState(pon)),
13546
+ cacheDelta: null
13547
+ };
13548
+ return this.finishMutation(session, {
13549
+ tag: "forms.setSignatureAppearance",
13550
+ result: {
13551
+ field,
13552
+ meta
13553
+ }
13554
+ }, req.artifactPath);
13555
+ }
10428
13556
  handleFormsDeleteField(req, signal) {
10429
13557
  const session = this.requireSession(req);
10430
13558
  const { deletedFieldObjectNumber, detachedWidgets } = new FormMutator(this.runtime, session).deleteField(req.ref, signal);
@@ -10470,8 +13598,8 @@ var WorkerHost = class {
10470
13598
  }, req.artifactPath);
10471
13599
  }
10472
13600
  finishMutation(session, payload, artifactPath) {
10473
- if (!payload.tag.startsWith("forms.") && payload.tag !== "pages.flatten" && payload.tag !== "redaction.apply") session.noteMutation();
10474
- if (session.kind !== "layer") return wirePack(payload);
13601
+ if (!payload.tag.startsWith("forms.") && payload.tag !== "pages.flatten" && payload.tag !== "redaction.apply" && payload.tag !== "signatures.complete") session.noteMutation();
13602
+ if (session.kind !== "layer" || !session.persistLayerArtifact) return wirePack(payload);
10475
13603
  const saved = this.saveLayerArtifact(session, artifactPath);
10476
13604
  return wirePack({
10477
13605
  ...payload,
@@ -10497,6 +13625,39 @@ const EMPTY_FORM_META = {
10497
13625
  cacheDelta: null
10498
13626
  };
10499
13627
  const BASE_SESSION_SUFFIX = "__base__";
13628
+ /** Every request kind that writes to a session's document. */
13629
+ const MUTATING_KINDS = /* @__PURE__ */ new Set([
13630
+ "metadata.update",
13631
+ "annotations.create",
13632
+ "annotations.update",
13633
+ "annotations.delete",
13634
+ "annotations.move",
13635
+ "annotations.flatten",
13636
+ "forms.setValue",
13637
+ "forms.reset",
13638
+ "forms.applyEffects",
13639
+ "forms.import",
13640
+ "forms.repair",
13641
+ "forms.createField",
13642
+ "forms.updateField",
13643
+ "forms.setSignatureAppearance",
13644
+ "forms.deleteField",
13645
+ "forms.attachWidget",
13646
+ "forms.detachWidget",
13647
+ "pages.move",
13648
+ "pages.rotate",
13649
+ "pages.delete",
13650
+ "pages.setName",
13651
+ "pages.removeName",
13652
+ "pages.flatten",
13653
+ "pages.insert",
13654
+ "pages.insertBlank",
13655
+ "redaction.apply",
13656
+ "attachments.create",
13657
+ "attachments.delete",
13658
+ "pieceInfo.update",
13659
+ "pieceInfo.clear"
13660
+ ]);
10500
13661
  function sessionKey(docId, layerName) {
10501
13662
  return `${docId}::${layerName ? `layer:${layerName}` : BASE_SESSION_SUFFIX}`;
10502
13663
  }
@@ -10525,6 +13686,6 @@ function passwordRequiredProbe() {
10525
13686
  };
10526
13687
  }
10527
13688
  //#endregion
10528
- 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 };
13689
+ export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationFlattener, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CandidateFinalizer, CloseStack, DIGEST_CODE, 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, SignatureAnalyzer, SignatureMutator, SignatureReader, WorkerHost, acquireSignatureModel, buildRunsFromRawGlyphs, computeMutationImpact, destroyLibrary, disposeFormModel, disposeSignatureModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, hasSignedSignature, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, readRevisions, readSignaturesFromModel, readStructure, throwIfAborted, writeAttachmentFilePayload };
10529
13690
 
10530
13691
  //# sourceMappingURL=index.js.map