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