@embedpdf/engine-services 3.0.0-next.12 → 3.0.0-next.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AbortError, EMPTY_TRANSFER, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PAGE_INSERT_BLANK_MAX_COUNT, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, UNKNOWN_WEAK_ANNOTATION_STATE, appearanceImpactOf, buildPageTextLayout, buildSnippet, charMapViolation, charRangeForTextOffsets, decodeSubmitFormFlags, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, normalizePdfRect, pdfQuadBounds, revisionTokensEqual, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, textSegmentsForRange, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
1
+ import { AbortError, EMPTY_TRANSFER, EdgeResolver, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, PAGE_INSERT_BLANK_MAX_COUNT, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, SIGNATURE_POLICY_VERSION, UNKNOWN_WEAK_ANNOTATION_STATE, USAGE_INCOMPLETE, appearanceImpactOf, assertPdfFloat, assertWritableMeasure, assessmentOf, buildPageTextLayout, buildSnippet, charMapViolation, charRangeForTextOffsets, combine, decodeSubmitFormFlags, deriveMeasurementLabel, deriveProtection, evaluateStep, fieldLockFor, foldOptionsFor, foldText, invalidatesWeakIndexRefs, isDimension, isReadout, isValidPageObjectNumber, knownWeakAnnotationState, matchLiteral, matchRegex, measurementReadout, normalizePdfRect, parsePdfValue, pdfQuadBounds, restrictionsFor, revisionTokensEqual, richTextParagraphsFromPlainText, richTextPlainText, sameEffectiveValue, serializeError, shiftsExistingAnnotationIndices, sniffBinaryMetadata, subtypeFromCode, textSegmentsForRange, validateSearchQuery, wirePack } from "@embedpdf/engine-core/runtime";
2
2
  import { NULL_PTR } from "@embedpdf/engine-runtime";
3
3
  //#region src/runtime/lifecycle/bootstrap.ts
4
4
  const initialized = /* @__PURE__ */ new WeakSet();
@@ -270,8 +270,108 @@ var SessionEventPublisher = class {
270
270
  }
271
271
  };
272
272
  //#endregion
273
+ //#region src/runtime/memory/scratch.ts
274
+ /**
275
+ * Allocate a scratch buffer, run `body` with it, and always free it.
276
+ *
277
+ * Every PDFium out-parameter call follows the same shape: malloc a
278
+ * buffer, hand its pointer to the native function, read the result back,
279
+ * free the buffer. Hand-rolling that `try/finally` at each call site is
280
+ * how leaks and copy-paste bugs creep in, so it lives here once.
281
+ */
282
+ function withScratch(mem, bytes, body) {
283
+ const ptr = mem.alloc(bytes);
284
+ try {
285
+ return body(ptr);
286
+ } finally {
287
+ mem.free(ptr);
288
+ }
289
+ }
290
+ /**
291
+ * Like {@link withScratch} but for native calls that take several
292
+ * out-parameters at once (e.g. `FPDFAnnot_GetColor` with one pointer per
293
+ * channel). Buffers are freed in reverse allocation order.
294
+ */
295
+ function withScratchN(mem, sizes, body) {
296
+ const ptrs = sizes.map((size) => mem.alloc(size));
297
+ try {
298
+ return body(ptrs);
299
+ } finally {
300
+ for (let i = ptrs.length - 1; i >= 0; i--) mem.free(ptrs[i]);
301
+ }
302
+ }
303
+ //#endregion
304
+ //#region src/runtime/memory/strings.ts
305
+ /**
306
+ * Read a PDFium UTF-16LE string getter that uses the standard
307
+ * (probe length, allocate, read) ABI. `call(buf, capacity)` must invoke
308
+ * the native function and return the length in BYTES including the
309
+ * trailing NUL (the value PDFium reports when `capacity` is 0).
310
+ *
311
+ * `emptyAs` decides what a present-but-empty value (`len === 2`, a lone
312
+ * UTF-16 NUL) decodes to. This is the one knob different callers actually
313
+ * disagree on: document Info strings treat `''` as a real value, whereas
314
+ * a page label of `''` is indistinguishable from "absent" and reads as
315
+ * `null`. Making it a parameter keeps that decision explicit instead of
316
+ * forking the buffer dance.
317
+ *
318
+ * Returns `null` when the value is absent (`len <= 0`).
319
+ */
320
+ function readUtf16String(mem, call, emptyAs = "") {
321
+ const len = call(NULL_PTR, 0);
322
+ if (len <= 0) return null;
323
+ if (len === 2) return emptyAs;
324
+ return withScratch(mem, len, (buf) => {
325
+ if (call(buf, len) <= 0) return null;
326
+ return mem.readU16String(buf);
327
+ });
328
+ }
329
+ /**
330
+ * UTF-8 sibling of {@link readUtf16String} for the handful of native
331
+ * getters that return UTF-8 (e.g. `EPDF_GetMetaKeyName`). UTF-8 has no
332
+ * fixed empty-NUL width to special-case, so an empty value decodes to
333
+ * `''`; absent (`len <= 0`) is `null`.
334
+ */
335
+ function readUtf8String(mem, call) {
336
+ const len = call(NULL_PTR, 0);
337
+ if (len <= 0) return null;
338
+ return withScratch(mem, len, (buf) => {
339
+ if (call(buf, len) <= 0) return null;
340
+ return mem.readU8String(buf);
341
+ });
342
+ }
343
+ /**
344
+ * Write side of the UTF-16 convention: encode `value` into a scratch
345
+ * buffer, hand the pointer to `set`, and always free it. `set` returns
346
+ * the native call's success flag, which is propagated.
347
+ */
348
+ function writeUtf16String(mem, value, set) {
349
+ const ptr = mem.writeU16String(value);
350
+ try {
351
+ return set(ptr);
352
+ } finally {
353
+ mem.free(ptr);
354
+ }
355
+ }
356
+ /**
357
+ * A NUL-terminated UTF-8 copy of `value` in runtime memory for the duration
358
+ * of `fn` (a `FPDF_STRING` parameter: a file system path, say).
359
+ */
360
+ function withUtf8CString(mem, value, fn) {
361
+ const encoded = new TextEncoder().encode(value);
362
+ const bytes = new Uint8Array(encoded.byteLength + 1);
363
+ bytes.set(encoded);
364
+ const ptr = mem.alloc(bytes.byteLength);
365
+ try {
366
+ mem.writeBytes(ptr, bytes);
367
+ return fn(ptr);
368
+ } finally {
369
+ mem.free(ptr);
370
+ }
371
+ }
372
+ //#endregion
273
373
  //#region src/document-session/lifecycle/PdfDocumentOpener.ts
274
- const FPDF_ERR_PASSWORD$1 = 4;
374
+ const FPDF_ERR_PASSWORD$2 = 4;
275
375
  var CloseStack = class {
276
376
  constructor() {
277
377
  _defineProperty(this, "actions", []);
@@ -306,7 +406,7 @@ function openFatMemoryDocument(runtime, bytes, password = null) {
306
406
  mem.writeBytes(dataPtr, bytes);
307
407
  const docPtr = fn.FPDF_LoadMemDocument64(dataPtr, bytes.byteLength, password ?? "");
308
408
  if (!docPtr) {
309
- if (fn.FPDF_GetLastError() === FPDF_ERR_PASSWORD$1) throw password ? new EngineError(EngineErrorCode.DocPasswordIncorrect, "incorrect document password") : new EngineError(EngineErrorCode.DocPasswordRequired, "document requires a password");
409
+ if (fn.FPDF_GetLastError() === FPDF_ERR_PASSWORD$2) throw password ? new EngineError(EngineErrorCode.DocPasswordIncorrect, "incorrect document password") : new EngineError(EngineErrorCode.DocPasswordRequired, "document requires a password");
310
410
  throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to open document");
311
411
  }
312
412
  setRuntimeOwnerPermissionsIfEncrypted(runtime, docPtr);
@@ -314,6 +414,11 @@ function openFatMemoryDocument(runtime, bytes, password = null) {
314
414
  return {
315
415
  kind: "fat-memory",
316
416
  docPtr,
417
+ source: {
418
+ base: null,
419
+ layer: null,
420
+ password
421
+ },
317
422
  close: () => stack.close()
318
423
  };
319
424
  } catch (error) {
@@ -331,6 +436,7 @@ function openLayerDocument(runtime, base, layer = { kind: "fresh" }, password =
331
436
  mem.poke(statusPtr, "i32", -1);
332
437
  let docPtr;
333
438
  if (layer.kind === "fresh") docPtr = fn.EPDFLayer_OpenLayer(base.basePtr, NULL_PTR, password ?? "", statusPtr);
439
+ else if (layer.kind === "artifact-file" && runtime.kind === "native") docPtr = withUtf8CString(mem, layer.path, (pathPtr) => fn.EPDFLayer_OpenLayerArtifactFromPath(base.basePtr, pathPtr, password ?? "", statusPtr));
334
440
  else {
335
441
  layerAccess = layer.kind === "artifact-file" ? runtime.fileAccess.fromNodeFile(layer.path) : runtime.fileAccess.fromMemory(layer.bytes);
336
442
  stack.push(() => layerAccess?.close());
@@ -340,9 +446,22 @@ function openLayerDocument(runtime, base, layer = { kind: "fresh" }, password =
340
446
  if (!docPtr || status !== 0) throw layerOpenError(status);
341
447
  setRuntimeOwnerPermissionsIfEncrypted(runtime, docPtr);
342
448
  stack.push(() => fn.FPDF_CloseDocument(docPtr));
449
+ const baseSource = base.kind === "file" && base.path !== void 0 ? {
450
+ kind: "file",
451
+ key: base.key,
452
+ path: base.path
453
+ } : {
454
+ kind: "memory",
455
+ key: base.key
456
+ };
343
457
  return {
344
458
  kind: "layer",
345
459
  docPtr,
460
+ source: {
461
+ base: baseSource,
462
+ layer,
463
+ password
464
+ },
346
465
  close: () => stack.close()
347
466
  };
348
467
  } catch (error) {
@@ -495,22 +614,13 @@ var LocalRevisionAuthority = class {
495
614
  };
496
615
  //#endregion
497
616
  //#region src/document-session/DocumentSession.ts
498
- /**
499
- * Owns the lifecycle of a single open PDFium document and the v3
500
- * identity machinery: page registry (pageObjectNumber <-> pageIndex),
501
- * `RevisionAuthority` (per-page generation counters), and `PagePtrPool`
502
- * (refcounted pagePtr access).
503
- *
504
- * Both the local browser Worker and the server worker_thread instantiate
505
- * this exactly the same way; the only thing that differs is the
506
- * underlying PdfRuntimeModule (WASM vs native).
507
- */
508
617
  var DocumentSession = class {
509
618
  constructor(runtime, sessionId) {
510
619
  this.runtime = runtime;
511
620
  _defineProperty(this, "docPtr", null);
512
621
  _defineProperty(this, "closeDocument", null);
513
622
  _defineProperty(this, "_kind", null);
623
+ _defineProperty(this, "_source", null);
514
624
  _defineProperty(this, "_sessionId", void 0);
515
625
  _defineProperty(this, "recordsByObjectNumber", /* @__PURE__ */ new Map());
516
626
  _defineProperty(this, "recordsByIndex", /* @__PURE__ */ new Map());
@@ -518,6 +628,59 @@ var DocumentSession = class {
518
628
  _defineProperty(this, "revisions", null);
519
629
  _defineProperty(this, "mutationSeqCounter", 0);
520
630
  _defineProperty(this, "pages", null);
631
+ _defineProperty(
632
+ this,
633
+ /**
634
+ * Whether writers honour what the document's signatures forbid (locked
635
+ * fields, structural edits). Set at open from the engine option; the
636
+ * main-thread guard subtracts the matching capabilities.
637
+ */
638
+ "signedDocumentPolicy",
639
+ "protect"
640
+ );
641
+ _defineProperty(
642
+ this,
643
+ /**
644
+ * The shape the caller asked for when opening plain bytes. `layer` (the
645
+ * default) opens them as an immutable base with a fresh layer; `plain`
646
+ * keeps one in-memory document unless the bytes already carry a
647
+ * signature, which always opens as a layer.
648
+ */
649
+ "sessionKind",
650
+ "layer"
651
+ );
652
+ _defineProperty(
653
+ this,
654
+ /** The password the document was opened with; signing candidates open with the same one. */
655
+ "password",
656
+ null
657
+ );
658
+ _defineProperty(
659
+ this,
660
+ /**
661
+ * Whether mutations on a layer session also serialize the layer artifact
662
+ * into their response (what a server persists). False for a session that
663
+ * became a layer only because its document is signed (see
664
+ * `WorkerHost.openSignedAware`): the caller opened plain bytes and never
665
+ * asked for artifacts.
666
+ */
667
+ "persistLayerArtifact",
668
+ true
669
+ );
670
+ _defineProperty(this, "loadedSeq", 0);
671
+ _defineProperty(this, "plainSha256", null);
672
+ _defineProperty(
673
+ this,
674
+ /** The signing candidate parked by `signatures.prepare`, if any. */
675
+ "pendingSigning",
676
+ null
677
+ );
678
+ _defineProperty(
679
+ this,
680
+ /** The last completed signing, so a replayed `complete` answers `already-completed`. */
681
+ "lastCompletion",
682
+ null
683
+ );
521
684
  _defineProperty(this, "parkedBytes", null);
522
685
  _defineProperty(this, "retained", []);
523
686
  this._sessionId = sessionId ?? generateSessionId();
@@ -528,6 +691,15 @@ var DocumentSession = class {
528
691
  get kind() {
529
692
  return this._kind;
530
693
  }
694
+ /**
695
+ * Where the current bytes come from: the immutable base (registry key,
696
+ * file path for file bases) and the layer the session was opened with.
697
+ * One interpretation for signing candidates, overlays and verbatim reads.
698
+ */
699
+ get source() {
700
+ if (!this._source) throw new EngineError(EngineErrorCode.DocNotOpen, "document is not open");
701
+ return this._source;
702
+ }
531
703
  isOpen() {
532
704
  return this.docPtr !== null;
533
705
  }
@@ -542,9 +714,76 @@ var DocumentSession = class {
542
714
  this.docPtr = handle.docPtr;
543
715
  this.closeDocument = () => handle.close();
544
716
  this._kind = handle.kind;
717
+ this._source = handle.source;
545
718
  this.revisions = new LocalRevisionAuthority(this._sessionId);
546
719
  this.pages = new PagePtrPool(this.runtime, handle.docPtr);
547
720
  this.parkedBytes = null;
721
+ this.loadedSeq = this.mutationSeqCounter;
722
+ }
723
+ /**
724
+ * Whether anything was mutated since the current bytes were loaded. When
725
+ * false, the loaded bytes ARE the document: a save returns them verbatim
726
+ * and a signing candidate is built straight on them.
727
+ */
728
+ hasUnsavedEdits() {
729
+ return this.mutationSeqCounter !== this.loadedSeq;
730
+ }
731
+ /** Cached hash of the current loaded bytes, or `null` when not computed since the last load. */
732
+ cachedPlainSha256() {
733
+ return this.plainSha256 && this.plainSha256.loadedSeq === this.loadedSeq ? this.plainSha256.sha256 : null;
734
+ }
735
+ rememberPlainSha256(sha256) {
736
+ this.plainSha256 = {
737
+ loadedSeq: this.loadedSeq,
738
+ sha256
739
+ };
740
+ }
741
+ /**
742
+ * Re-point this session at new bytes — the one operation that changes
743
+ * what an open session is backed by, used when a completed signature
744
+ * installs its sealed file. The session id, its page object numbers
745
+ * (an incremental save never renumbers) and its retained resources
746
+ * survive; the old document is closed, every page is re-pinned (its
747
+ * revision bumped, its cached pointer dropped), and the mutation
748
+ * sequence advances so every version-keyed cache rebuilds.
749
+ */
750
+ install(handle) {
751
+ if (!this.docPtr) {
752
+ handle.close();
753
+ throw new EngineError(EngineErrorCode.DocNotOpen, "no document to replace");
754
+ }
755
+ const previousPages = Array.from(this.recordsByObjectNumber.keys());
756
+ let firstError = null;
757
+ try {
758
+ this.pages?.closeAll();
759
+ } catch (error) {
760
+ firstError = error;
761
+ }
762
+ try {
763
+ this.closeDocument?.();
764
+ } catch (error) {
765
+ firstError ?? (firstError = error);
766
+ }
767
+ this.docPtr = handle.docPtr;
768
+ this.closeDocument = () => handle.close();
769
+ this._kind = handle.kind;
770
+ this._source = handle.source;
771
+ this.pages = new PagePtrPool(this.runtime, handle.docPtr);
772
+ this.recordsByIndex.clear();
773
+ this.recordsByObjectNumber.clear();
774
+ this.fullyEnumerated = false;
775
+ for (const pon of previousPages) this.requireRevisions().bump(pon);
776
+ this.mutationSeqCounter++;
777
+ this.loadedSeq = this.mutationSeqCounter;
778
+ this.pendingSigning = null;
779
+ if (firstError) throw firstError;
780
+ }
781
+ /** The two publish fences a candidate is built on. */
782
+ versionRef(baseSha256) {
783
+ return {
784
+ baseSha256,
785
+ editsVersion: this.mutationSeqCounter
786
+ };
548
787
  }
549
788
  /**
550
789
  * Park this session in the password-locked state: the document could not
@@ -704,6 +943,11 @@ var DocumentSession = class {
704
943
  } finally {
705
944
  this.pages = null;
706
945
  }
946
+ if (this.pendingSigning?.saved.kind === "file") try {
947
+ this.runtime.fileWrite.removeFile(this.pendingSigning.saved.path);
948
+ } catch (error) {
949
+ firstError ?? (firstError = error);
950
+ }
707
951
  try {
708
952
  this.closeDocument?.();
709
953
  } catch (error) {
@@ -712,7 +956,10 @@ var DocumentSession = class {
712
956
  this.closeDocument = null;
713
957
  this.docPtr = null;
714
958
  this._kind = null;
959
+ this._source = null;
715
960
  this.parkedBytes = null;
961
+ this.pendingSigning = null;
962
+ this.lastCompletion = null;
716
963
  this.revisions?.clear();
717
964
  this.revisions = null;
718
965
  this.recordsByIndex.clear();
@@ -738,6 +985,17 @@ function generateSessionId() {
738
985
  }
739
986
  //#endregion
740
987
  //#region src/document-session/lifecycle/BaseDocumentRegistry.ts
988
+ const FPDF_ERR_PASSWORD$1 = 4;
989
+ /**
990
+ * A password failure is a recoverable STATE the caller can act on (prompt,
991
+ * unlock), never a generic open failure — the same distinction
992
+ * `openFatMemoryDocument` draws, so a plain-bytes open that becomes a base
993
+ * parks and unlocks exactly like it always did.
994
+ */
995
+ function baseOpenError(runtime, password) {
996
+ if (runtime.fn.FPDF_GetLastError() === FPDF_ERR_PASSWORD$1) return password ? new EngineError(EngineErrorCode.DocPasswordIncorrect, "incorrect document password") : new EngineError(EngineErrorCode.DocPasswordRequired, "document requires a password");
997
+ return new EngineError(EngineErrorCode.DocOpenFailed, "failed to open base document");
998
+ }
741
999
  var BaseDocumentRegistry = class {
742
1000
  constructor(runtime) {
743
1001
  this.runtime = runtime;
@@ -753,10 +1011,17 @@ var BaseDocumentRegistry = class {
753
1011
  try {
754
1012
  mem.writeBytes(dataPtr, opts.bytes);
755
1013
  const basePtr = fn.EPDF_LoadMemBaseDocument64(dataPtr, opts.bytes.byteLength, opts.password ?? "");
756
- if (!basePtr) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to open base document");
1014
+ if (!basePtr) throw baseOpenError(this.runtime, opts.password);
757
1015
  setRuntimeOwnerPermissionsIfEncrypted(this.runtime, basePtr);
758
1016
  stack.push(() => fn.EPDF_ReleaseBaseDocument(basePtr));
759
- return this.insert(opts.key, basePtr, () => stack.close());
1017
+ this.supplyKnownSha(basePtr, opts.knownSha256);
1018
+ return this.insert({
1019
+ key: opts.key,
1020
+ kind: "memory",
1021
+ basePtr,
1022
+ refs: 1,
1023
+ close: () => stack.close()
1024
+ });
760
1025
  } catch (error) {
761
1026
  stack.close();
762
1027
  throw error;
@@ -772,15 +1037,50 @@ var BaseDocumentRegistry = class {
772
1037
  access = this.runtime.fileAccess.fromNodeFile(opts.path);
773
1038
  stack.push(() => access?.close());
774
1039
  const basePtr = fn.EPDF_LoadBaseDocument(access.ptr, opts.password ?? "");
775
- if (!basePtr) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to open base document");
1040
+ if (!basePtr) throw baseOpenError(this.runtime, opts.password);
776
1041
  setRuntimeOwnerPermissionsIfEncrypted(this.runtime, basePtr);
777
1042
  stack.push(() => fn.EPDF_ReleaseBaseDocument(basePtr));
778
- return this.insert(opts.key, basePtr, () => stack.close());
1043
+ this.supplyKnownSha(basePtr, opts.knownSha256);
1044
+ return this.insert({
1045
+ key: opts.key,
1046
+ kind: "file",
1047
+ path: opts.path,
1048
+ basePtr,
1049
+ refs: 1,
1050
+ close: () => stack.close()
1051
+ });
779
1052
  } catch (error) {
780
1053
  stack.close();
781
1054
  throw error;
782
1055
  }
783
1056
  }
1057
+ /**
1058
+ * One more retain on a base already in the registry (a session holds one
1059
+ * for its lifetime, so its key is live while it is open), or null. The
1060
+ * way a signing candidate reopens over its session's own base without
1061
+ * copying a byte.
1062
+ */
1063
+ retainByKey(key) {
1064
+ return this.retain(key);
1065
+ }
1066
+ /**
1067
+ * Hand the runtime a hash the host already verified for these exact
1068
+ * bytes, so it never hashes them itself. Malformed values are ignored:
1069
+ * the runtime then hashes lazily on first use, which is always correct.
1070
+ */
1071
+ supplyKnownSha(basePtr, sha256Hex) {
1072
+ if (!sha256Hex || !/^[0-9a-fA-F]{64}$/.test(sha256Hex)) return;
1073
+ const { mem, fn } = this.runtime;
1074
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
1075
+ for (let i = 0; i < 32; i++) bytes[i] = parseInt(sha256Hex.slice(i * 2, i * 2 + 2), 16);
1076
+ const ptr = mem.alloc(32);
1077
+ try {
1078
+ mem.writeBytes(ptr, bytes);
1079
+ fn.EPDF_SetBaseDocumentSha256(basePtr, ptr);
1080
+ } finally {
1081
+ mem.free(ptr);
1082
+ }
1083
+ }
784
1084
  releaseAll() {
785
1085
  const entries = Array.from(this.entries.values());
786
1086
  this.entries.clear();
@@ -802,20 +1102,16 @@ var BaseDocumentRegistry = class {
802
1102
  entry.refs++;
803
1103
  return this.handleFor(entry);
804
1104
  }
805
- insert(key, basePtr, close) {
806
- const entry = {
807
- key,
808
- basePtr,
809
- refs: 1,
810
- close
811
- };
812
- this.entries.set(key, entry);
1105
+ insert(entry) {
1106
+ this.entries.set(entry.key, entry);
813
1107
  return this.handleFor(entry);
814
1108
  }
815
1109
  handleFor(entry) {
816
1110
  let released = false;
817
1111
  return {
818
1112
  key: entry.key,
1113
+ kind: entry.kind,
1114
+ ...entry.path !== void 0 ? { path: entry.path } : {},
819
1115
  basePtr: entry.basePtr,
820
1116
  release: () => {
821
1117
  if (released) return;
@@ -831,90 +1127,6 @@ var BaseDocumentRegistry = class {
831
1127
  }
832
1128
  };
833
1129
  //#endregion
834
- //#region src/runtime/memory/scratch.ts
835
- /**
836
- * Allocate a scratch buffer, run `body` with it, and always free it.
837
- *
838
- * Every PDFium out-parameter call follows the same shape: malloc a
839
- * buffer, hand its pointer to the native function, read the result back,
840
- * free the buffer. Hand-rolling that `try/finally` at each call site is
841
- * how leaks and copy-paste bugs creep in, so it lives here once.
842
- */
843
- function withScratch(mem, bytes, body) {
844
- const ptr = mem.alloc(bytes);
845
- try {
846
- return body(ptr);
847
- } finally {
848
- mem.free(ptr);
849
- }
850
- }
851
- /**
852
- * Like {@link withScratch} but for native calls that take several
853
- * out-parameters at once (e.g. `FPDFAnnot_GetColor` with one pointer per
854
- * channel). Buffers are freed in reverse allocation order.
855
- */
856
- function withScratchN(mem, sizes, body) {
857
- const ptrs = sizes.map((size) => mem.alloc(size));
858
- try {
859
- return body(ptrs);
860
- } finally {
861
- for (let i = ptrs.length - 1; i >= 0; i--) mem.free(ptrs[i]);
862
- }
863
- }
864
- //#endregion
865
- //#region src/runtime/memory/strings.ts
866
- /**
867
- * Read a PDFium UTF-16LE string getter that uses the standard
868
- * (probe length, allocate, read) ABI. `call(buf, capacity)` must invoke
869
- * the native function and return the length in BYTES including the
870
- * trailing NUL (the value PDFium reports when `capacity` is 0).
871
- *
872
- * `emptyAs` decides what a present-but-empty value (`len === 2`, a lone
873
- * UTF-16 NUL) decodes to. This is the one knob different callers actually
874
- * disagree on: document Info strings treat `''` as a real value, whereas
875
- * a page label of `''` is indistinguishable from "absent" and reads as
876
- * `null`. Making it a parameter keeps that decision explicit instead of
877
- * forking the buffer dance.
878
- *
879
- * Returns `null` when the value is absent (`len <= 0`).
880
- */
881
- function readUtf16String(mem, call, emptyAs = "") {
882
- const len = call(NULL_PTR, 0);
883
- if (len <= 0) return null;
884
- if (len === 2) return emptyAs;
885
- return withScratch(mem, len, (buf) => {
886
- if (call(buf, len) <= 0) return null;
887
- return mem.readU16String(buf);
888
- });
889
- }
890
- /**
891
- * UTF-8 sibling of {@link readUtf16String} for the handful of native
892
- * getters that return UTF-8 (e.g. `EPDF_GetMetaKeyName`). UTF-8 has no
893
- * fixed empty-NUL width to special-case, so an empty value decodes to
894
- * `''`; absent (`len <= 0`) is `null`.
895
- */
896
- function readUtf8String(mem, call) {
897
- const len = call(NULL_PTR, 0);
898
- if (len <= 0) return null;
899
- return withScratch(mem, len, (buf) => {
900
- if (call(buf, len) <= 0) return null;
901
- return mem.readU8String(buf);
902
- });
903
- }
904
- /**
905
- * Write side of the UTF-16 convention: encode `value` into a scratch
906
- * buffer, hand the pointer to `set`, and always free it. `set` returns
907
- * the native call's success flag, which is propagated.
908
- */
909
- function writeUtf16String(mem, value, set) {
910
- const ptr = mem.writeU16String(value);
911
- try {
912
- return set(ptr);
913
- } finally {
914
- mem.free(ptr);
915
- }
916
- }
917
- //#endregion
918
1130
  //#region src/features/metadata/internal/readMetadataText.ts
919
1131
  /**
920
1132
  * Read a key from the document's Info dictionary. Returns null if the key
@@ -2171,8 +2383,11 @@ var PagesInserter = class {
2171
2383
  //#endregion
2172
2384
  //#region src/features/annotations/internal/read/annotationReadContext.ts
2173
2385
  /** Build the read context both read paths (list loop, mutator read-back) use. */
2174
- function readContextFor(session) {
2175
- return { docPtr: session.requireDocPtr() };
2386
+ function readContextFor(session, fonts) {
2387
+ return {
2388
+ docPtr: session.requireDocPtr(),
2389
+ ...fonts ? { fonts } : {}
2390
+ };
2176
2391
  }
2177
2392
  //#endregion
2178
2393
  //#region src/features/annotations/internal/annotationFlagBits.ts
@@ -3013,18 +3228,218 @@ function standardFontFromCode(code) {
3013
3228
  return CODE_TO_FONT[code] ?? "helvetica";
3014
3229
  }
3015
3230
  //#endregion
3016
- //#region src/features/annotations/internal/textAlignment.ts
3017
- /**
3018
- * `/Q` quadding codes (ISO 32000 §12.7.3.3) mapped to the wire-stable
3019
- * `TextAlignment` strings. Kept in engine-services so engine-core stays
3020
- * PDFium-free (mirrors `lineEnding.ts`).
3021
- *
3022
- * Left=0, Center=1, Right=2
3023
- */
3024
- const Q_LEFT = 0;
3025
- const Q_CENTER = 1;
3026
- const Q_RIGHT = 2;
3027
- function textAlignmentToCode(align) {
3231
+ //#region src/features/annotations/internal/richTextWire.ts
3232
+ const STANDARD_FACES = {
3233
+ courier: {
3234
+ family: "Courier",
3235
+ weight: 400,
3236
+ italic: false
3237
+ },
3238
+ "courier-bold": {
3239
+ family: "Courier",
3240
+ weight: 700,
3241
+ italic: false
3242
+ },
3243
+ "courier-bold-oblique": {
3244
+ family: "Courier",
3245
+ weight: 700,
3246
+ italic: true
3247
+ },
3248
+ "courier-oblique": {
3249
+ family: "Courier",
3250
+ weight: 400,
3251
+ italic: true
3252
+ },
3253
+ helvetica: {
3254
+ family: "Helvetica",
3255
+ weight: 400,
3256
+ italic: false
3257
+ },
3258
+ "helvetica-bold": {
3259
+ family: "Helvetica",
3260
+ weight: 700,
3261
+ italic: false
3262
+ },
3263
+ "helvetica-bold-oblique": {
3264
+ family: "Helvetica",
3265
+ weight: 700,
3266
+ italic: true
3267
+ },
3268
+ "helvetica-oblique": {
3269
+ family: "Helvetica",
3270
+ weight: 400,
3271
+ italic: true
3272
+ },
3273
+ "times-roman": {
3274
+ family: "Times",
3275
+ weight: 400,
3276
+ italic: false
3277
+ },
3278
+ "times-bold": {
3279
+ family: "Times",
3280
+ weight: 700,
3281
+ italic: false
3282
+ },
3283
+ "times-bold-italic": {
3284
+ family: "Times",
3285
+ weight: 700,
3286
+ italic: true
3287
+ },
3288
+ "times-italic": {
3289
+ family: "Times",
3290
+ weight: 400,
3291
+ italic: true
3292
+ },
3293
+ symbol: {
3294
+ family: "Symbol",
3295
+ weight: 400,
3296
+ italic: false
3297
+ },
3298
+ "zapf-dingbats": {
3299
+ family: "ZapfDingbats",
3300
+ weight: 400,
3301
+ italic: false
3302
+ }
3303
+ };
3304
+ /** Family names compare without case, spaces, hyphens and underscores
3305
+ * (the engine's own rule): "Noto Sans" == "NotoSans" == "noto-sans". */
3306
+ function familyKey$1(family) {
3307
+ return family.toLowerCase().replace(/[\s\-_'"]/g, "");
3308
+ }
3309
+ const STANDARD_FAMILY_KEYS = {
3310
+ helvetica: "Helvetica",
3311
+ arial: "Helvetica",
3312
+ arialmt: "Helvetica",
3313
+ sansserif: "Helvetica",
3314
+ times: "Times",
3315
+ timesroman: "Times",
3316
+ timesnewroman: "Times",
3317
+ timesnewromanpsmt: "Times",
3318
+ serif: "Times",
3319
+ courier: "Courier",
3320
+ couriernew: "Courier",
3321
+ couriernewpsmt: "Courier",
3322
+ monospace: "Courier",
3323
+ symbol: "Symbol",
3324
+ zapfdingbats: "ZapfDingbats",
3325
+ dingbats: "ZapfDingbats"
3326
+ };
3327
+ /**
3328
+ * The face a DTO `fontFamily` (or a rich run's `family`) asks for: a
3329
+ * standard font's family/weight/italic, a registered key's resolved
3330
+ * identity, or — for anything else — the string itself as a family name
3331
+ * (a face the document embeds, or one the engine will substitute).
3332
+ */
3333
+ function faceForFreeTextFont(font, describe) {
3334
+ if (isStandardFont(font)) {
3335
+ const face = STANDARD_FACES[font];
3336
+ return {
3337
+ family: face.family,
3338
+ weight: face.weight,
3339
+ italic: face.italic
3340
+ };
3341
+ }
3342
+ const registered = describe?.(font);
3343
+ if (registered) return {
3344
+ family: registered.familyName,
3345
+ weight: registered.weight,
3346
+ italic: registered.italic
3347
+ };
3348
+ return { family: font };
3349
+ }
3350
+ /**
3351
+ * The DTO `fontFamily` for a body face: the registered `key` whose identity
3352
+ * matches, else the standard font's kebab name, else the family itself.
3353
+ */
3354
+ function freeTextFontForFace(face, keyForFace) {
3355
+ const key = keyForFace?.(face.family, face.weight, face.italic);
3356
+ if (key !== void 0) return key;
3357
+ const standard = STANDARD_FAMILY_KEYS[familyKey$1(face.family)];
3358
+ if (standard) {
3359
+ const bold = face.weight >= 600;
3360
+ for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard && spec.weight >= 600 === bold && spec.italic === face.italic) return name;
3361
+ for (const [name, spec] of Object.entries(STANDARD_FACES)) if (spec.family === standard) return name;
3362
+ }
3363
+ return face.family;
3364
+ }
3365
+ /** A run style or body with its `family` (and the weight/italic a key or
3366
+ * standard name implies) rewritten to what the engine resolves by. */
3367
+ function withEngineFace(style, describe) {
3368
+ if (style.family === void 0) return style;
3369
+ const face = faceForFreeTextFont(style.family, describe);
3370
+ const out = {
3371
+ ...style,
3372
+ family: face.family
3373
+ };
3374
+ if (face.weight !== void 0 && style.weight === void 0) out.weight = face.weight;
3375
+ if (face.italic !== void 0 && style.italic === void 0) out.italic = face.italic;
3376
+ return out;
3377
+ }
3378
+ /** The engine's JSON for a write: the input shape with faces resolved. */
3379
+ function engineRichTextJson(input, describe) {
3380
+ const body = input.body ? withEngineFace(input.body, describe) : void 0;
3381
+ const paragraphs = input.paragraphs.map((p) => ({
3382
+ ...p,
3383
+ runs: p.runs.map((r) => r.style ? {
3384
+ ...r,
3385
+ style: withEngineFace(r.style, describe)
3386
+ } : r)
3387
+ }));
3388
+ return JSON.stringify(body ? {
3389
+ body,
3390
+ paragraphs
3391
+ } : { paragraphs });
3392
+ }
3393
+ /** The engine's JSON, parsed into the DTO shape (its `source` and
3394
+ * `diagnostics` dropped). `null` when it is not what the engine writes. */
3395
+ function parseEngineRichText(json) {
3396
+ let raw;
3397
+ try {
3398
+ raw = JSON.parse(json);
3399
+ } catch {
3400
+ return null;
3401
+ }
3402
+ if (!raw || typeof raw !== "object" || !raw.body || !Array.isArray(raw.paragraphs)) return null;
3403
+ const { source: _source, ...bodyRest } = raw.body;
3404
+ return {
3405
+ body: bodyRest,
3406
+ paragraphs: raw.paragraphs.map((p) => {
3407
+ const { runs, ...props } = p;
3408
+ return {
3409
+ ...props,
3410
+ runs: (runs ?? []).map((r) => r)
3411
+ };
3412
+ })
3413
+ };
3414
+ }
3415
+ /** Read an annotation's rich text through `EPDFAnnot_GetRichTextJSON`. */
3416
+ function readEngineRichText(fn, mem, annotPtr) {
3417
+ const json = readUtf8String(mem, (buf, cap) => fn.EPDFAnnot_GetRichTextJSON(annotPtr, buf, cap));
3418
+ return json ? parseEngineRichText(json) : null;
3419
+ }
3420
+ /**
3421
+ * A draft or patch that carries both `contents` and `richText` must agree:
3422
+ * `contents` is the plain projection of the rich text (plan §4.7). A stale
3423
+ * DTO echo fails loud here instead of quietly restoring old text.
3424
+ */
3425
+ function assertRichTextAgreement(input) {
3426
+ if (input.subtype !== "free-text" || input.richText === void 0) return;
3427
+ if (input.contents === void 0 || input.contents === null) return;
3428
+ if (input.contents !== richTextPlainText(input.richText)) throw new EngineError(EngineErrorCode.InvalidArg, "free-text: `contents` must equal the plain projection of `richText` when both are given");
3429
+ }
3430
+ //#endregion
3431
+ //#region src/features/annotations/internal/textAlignment.ts
3432
+ /**
3433
+ * `/Q` quadding codes (ISO 32000 §12.7.3.3) mapped to the wire-stable
3434
+ * `TextAlignment` strings. Kept in engine-services so engine-core stays
3435
+ * PDFium-free (mirrors `lineEnding.ts`).
3436
+ *
3437
+ * Left=0, Center=1, Right=2
3438
+ */
3439
+ const Q_LEFT = 0;
3440
+ const Q_CENTER = 1;
3441
+ const Q_RIGHT = 2;
3442
+ function textAlignmentToCode(align) {
3028
3443
  switch (align) {
3029
3444
  case "center": return Q_CENTER;
3030
3445
  case "right": return Q_RIGHT;
@@ -3139,17 +3554,36 @@ const DEFAULT_FONT_SIZE$2 = 12;
3139
3554
  function colorsEqual(a, b) {
3140
3555
  return a.r === b.r && a.g === b.g && a.b === b.b;
3141
3556
  }
3142
- function readFreeText(fn, mem, annotPtr, base) {
3557
+ function readFreeText(fn, mem, annotPtr, base, _subtypeCode, ctx) {
3143
3558
  const da = readDefaultAppearance(fn, mem, annotPtr);
3144
3559
  const color = da?.color ?? { ...DEFAULT_FREETEXT_COLOR$1 };
3145
- const fontFamily = da ? standardFontFromCode(da.fontCode) : DEFAULT_STANDARD_FONT;
3146
- const fontSize = da && da.fontSize > 0 ? da.fontSize : DEFAULT_FONT_SIZE$2;
3560
+ const rich = readEngineRichText(fn, mem, annotPtr);
3561
+ const fonts = ctx?.fonts;
3562
+ const fontFamily = rich ? freeTextFontForFace(rich.body, fonts ? (family, weight, italic) => fonts.keyForFace(family, weight, italic) : void 0) : da ? standardFontFromCode(da.fontCode) : DEFAULT_STANDARD_FONT;
3563
+ const fontSize = rich && rich.body.size > 0 ? rich.body.size : da && da.fontSize > 0 ? da.fontSize : DEFAULT_FONT_SIZE$2;
3564
+ const richText = rich ?? {
3565
+ body: {
3566
+ family: "Helvetica",
3567
+ weight: 400,
3568
+ italic: false,
3569
+ size: fontSize,
3570
+ color: "#000000",
3571
+ decoration: [],
3572
+ script: "normal",
3573
+ letterSpacing: 0,
3574
+ horizontalScale: 1,
3575
+ align: "left",
3576
+ dir: "ltr"
3577
+ },
3578
+ paragraphs: [{ runs: [{ text: base.contents ?? "" }] }]
3579
+ };
3147
3580
  const textColor = readAnnotColor(fn, mem, annotPtr, FPDFANNOT_COLORTYPE.TextColor);
3148
3581
  const fontColor = textColor && !colorsEqual(textColor, color) ? textColor : void 0;
3149
3582
  const background = readAnnotColor(fn, mem, annotPtr, FPDFANNOT_COLORTYPE.Color);
3150
3583
  const ca = readAnnotOpacity(fn, mem, annotPtr);
3151
3584
  const opacity = ca == null ? 1 : Math.max(0, Math.min(1, ca));
3152
- const textAlign = textAlignmentFromCode(readTextAlignment(fn, annotPtr));
3585
+ const quadding = textAlignmentFromCode(readTextAlignment(fn, annotPtr));
3586
+ const textAlign = rich && rich.body.align !== "justify" ? rich.body.align : quadding;
3153
3587
  const intent = freeTextIntentFromName(readIntent(fn, mem, annotPtr));
3154
3588
  const points = readCalloutLine(fn, mem, annotPtr);
3155
3589
  const calloutLine = points.length === 2 ? [points[0], points[1]] : points.length === 3 ? [
@@ -3168,6 +3602,7 @@ function readFreeText(fn, mem, annotPtr, base) {
3168
3602
  fontFamily,
3169
3603
  fontSize,
3170
3604
  textAlign,
3605
+ richText,
3171
3606
  color,
3172
3607
  ...fontColor !== void 0 ? { fontColor } : {},
3173
3608
  interiorColor: background ?? null,
@@ -3208,6 +3643,181 @@ function readInk(fn, mem, annotPtr, base) {
3208
3643
  };
3209
3644
  }
3210
3645
  //#endregion
3646
+ //#region src/features/measure/internal/measureCodec.ts
3647
+ const MEASURE_AXES = {
3648
+ x: 0,
3649
+ y: 1,
3650
+ distance: 2,
3651
+ area: 3,
3652
+ angle: 4,
3653
+ slope: 5
3654
+ };
3655
+ const FRACTIONS = [
3656
+ "decimal",
3657
+ "fraction",
3658
+ "round",
3659
+ "truncate"
3660
+ ];
3661
+ const TEXT = [
3662
+ "thousands",
3663
+ "decimal",
3664
+ "prefixSpacing",
3665
+ "suffixSpacing"
3666
+ ];
3667
+ /** A native rejection after validated input is an internal invariant, never caller validation. */
3668
+ function requireMeasureWrite(ok) {
3669
+ if (!ok) throw new EngineError(EngineErrorCode.Unknown, "Measurement native write failed after preflight");
3670
+ }
3671
+ function readMeasureNumber(mem, call, type = "f32") {
3672
+ return withScratch(mem, 4, (p) => call(p) ? Number(mem.peek(p, type)) : void 0);
3673
+ }
3674
+ function withMeasurePoint(mem, point, call) {
3675
+ if (!point) return call(NULL_PTR);
3676
+ return withScratch(mem, 8, (p) => {
3677
+ mem.poke(p, "f32", point.x);
3678
+ mem.poke(p, "f32", point.y, 4);
3679
+ return call(p);
3680
+ });
3681
+ }
3682
+ function readMeasure(fn, mem, handle) {
3683
+ if (!handle) return void 0;
3684
+ const subtype = fn.EPDFMeasure_GetSubtype(handle);
3685
+ if (subtype !== 1) return { subtype: subtype === 2 ? "GEO" : "unknown" };
3686
+ const formats = (axis) => Array.from({ length: fn.EPDFMeasure_CountFormats(handle, axis) }, (_, i) => {
3687
+ const f = fn.EPDFMeasure_GetFormat(handle, axis, i);
3688
+ const out = { unit: readUtf16String(mem, (b, n) => fn.EPDFNumberFormat_GetUnit(f, b, n)) ?? "" };
3689
+ const conversion = readMeasureNumber(mem, (p) => fn.EPDFNumberFormat_GetConversion(f, p));
3690
+ if (conversion !== void 0) out.conversion = conversion;
3691
+ const fraction = readMeasureNumber(mem, (p) => fn.EPDFNumberFormat_GetFraction(f, p), "i32");
3692
+ if (fraction !== void 0 && FRACTIONS[fraction]) out.fraction = FRACTIONS[fraction];
3693
+ const precision = readMeasureNumber(mem, (p) => fn.EPDFNumberFormat_GetPrecision(f, p), "i32");
3694
+ if (precision !== void 0) out.precision = precision;
3695
+ const fixed = readMeasureNumber(mem, (p) => fn.EPDFNumberFormat_GetFixedDenominator(f, p), "i32");
3696
+ if (fixed !== void 0) out.fixed = !!fixed;
3697
+ const position = readMeasureNumber(mem, (p) => fn.EPDFNumberFormat_GetLabelPosition(f, p), "i32");
3698
+ if (position !== void 0) out.labelPosition = position === 1 ? "prefix" : "suffix";
3699
+ for (const [code, key] of TEXT.entries()) {
3700
+ const text = readUtf16String(mem, (b, n) => fn.EPDFNumberFormat_GetText(f, code, b, n));
3701
+ if (text !== null) out[key] = text;
3702
+ }
3703
+ return out;
3704
+ });
3705
+ const ratio = readUtf16String(mem, (b, n) => fn.EPDFMeasure_GetRatio(handle, b, n));
3706
+ const origin = withScratch(mem, 8, (p) => fn.EPDFMeasure_GetOrigin(handle, p) ? {
3707
+ x: Number(mem.peek(p, "f32")),
3708
+ y: Number(mem.peek(p, "f32", 4))
3709
+ } : void 0);
3710
+ const cyx = readMeasureNumber(mem, (p) => fn.EPDFMeasure_GetCYX(handle, p));
3711
+ const result = {
3712
+ subtype: "RL",
3713
+ x: formats(0),
3714
+ distance: formats(2),
3715
+ area: formats(3),
3716
+ ...ratio !== null ? { ratio } : {},
3717
+ ...origin ? { origin } : {},
3718
+ ...cyx !== void 0 ? { cyx } : {}
3719
+ };
3720
+ for (const key of [
3721
+ "y",
3722
+ "angle",
3723
+ "slope"
3724
+ ]) {
3725
+ const list = formats(MEASURE_AXES[key]);
3726
+ if (list.length) result[key] = list;
3727
+ }
3728
+ return result;
3729
+ }
3730
+ /** The caller supplies a freshly created/reset handle and has completed preflight. */
3731
+ function writeMeasure(fn, mem, handle, value) {
3732
+ requireMeasureWrite(handle);
3733
+ if (value.ratio !== void 0) requireMeasureWrite(writeUtf16String(mem, value.ratio, (p) => fn.EPDFMeasure_SetRatio(handle, p)));
3734
+ if (value.origin) requireMeasureWrite(withMeasurePoint(mem, value.origin, (p) => fn.EPDFMeasure_SetOrigin(handle, p)));
3735
+ if (value.cyx !== void 0) withScratch(mem, 4, (p) => {
3736
+ mem.poke(p, "f32", value.cyx);
3737
+ requireMeasureWrite(fn.EPDFMeasure_SetCYX(handle, p));
3738
+ });
3739
+ for (const [key, axis] of Object.entries(MEASURE_AXES)) for (const nf of value[key] ?? []) {
3740
+ const text = mem.writeU16String(nf.unit);
3741
+ let f;
3742
+ try {
3743
+ f = fn.EPDFMeasure_AddFormat(handle, axis, text, nf.conversion);
3744
+ } finally {
3745
+ mem.free(text);
3746
+ }
3747
+ requireMeasureWrite(f);
3748
+ if (nf.fraction !== void 0) requireMeasureWrite(fn.EPDFNumberFormat_SetFraction(f, FRACTIONS.indexOf(nf.fraction)));
3749
+ if (nf.precision !== void 0) requireMeasureWrite(fn.EPDFNumberFormat_SetPrecision(f, nf.precision));
3750
+ if (nf.fixed !== void 0) requireMeasureWrite(fn.EPDFNumberFormat_SetFixedDenominator(f, nf.fixed));
3751
+ if (nf.labelPosition !== void 0) requireMeasureWrite(fn.EPDFNumberFormat_SetLabelPosition(f, nf.labelPosition === "prefix" ? 1 : 0));
3752
+ for (const [code, key] of TEXT.entries()) if (nf[key] !== void 0) requireMeasureWrite(writeUtf16String(mem, nf[key], (p) => fn.EPDFNumberFormat_SetText(f, code, p)));
3753
+ }
3754
+ }
3755
+ //#endregion
3756
+ //#region src/features/annotations/internal/read/readMeasurementFields.ts
3757
+ function readAnnotationMeasure(fn, mem, annot) {
3758
+ const measure = readMeasure(fn, mem, fn.EPDFAnnot_GetMeasure(annot));
3759
+ return measure ? { measure } : {};
3760
+ }
3761
+ function readLineCaption(fn, mem, annot) {
3762
+ if (![
3763
+ "Cap",
3764
+ "CP",
3765
+ "CO"
3766
+ ].some((key) => fn.FPDFAnnot_HasKey(annot, key))) return void 0;
3767
+ return withScratchN(mem, [
3768
+ 4,
3769
+ 4,
3770
+ 8
3771
+ ], ([enabled, position, offset]) => {
3772
+ if (!fn.EPDFAnnot_GetLineCaption(annot, enabled, position, offset)) return void 0;
3773
+ return {
3774
+ enabled: !!mem.peek(enabled, "i32"),
3775
+ position: mem.peek(position, "i32") === 1 ? "top" : "inline",
3776
+ ...fn.FPDFAnnot_HasKey(annot, "CO") ? { offset: {
3777
+ along: Number(mem.peek(offset, "f32")),
3778
+ perpendicular: Number(mem.peek(offset, "f32", 4))
3779
+ } } : {}
3780
+ };
3781
+ });
3782
+ }
3783
+ function readShapeCaption(fn, mem, annot) {
3784
+ return withScratchN(mem, [
3785
+ 4,
3786
+ 4,
3787
+ 8
3788
+ ], ([enabled, hasCenter, center]) => {
3789
+ if (!fn.EPDFAnnot_GetShapeCaption(annot, enabled, hasCenter, center)) return void 0;
3790
+ const value = !!mem.peek(enabled, "i32"), manual = !!mem.peek(hasCenter, "i32");
3791
+ if (!fn.EPDFAnnot_GetEmbedMetadataBoolean(annot, "MeasurementCaption", enabled) && !manual) return void 0;
3792
+ return {
3793
+ enabled: value,
3794
+ ...manual ? { center: {
3795
+ x: Number(mem.peek(center, "f32")),
3796
+ y: Number(mem.peek(center, "f32", 4))
3797
+ } } : {}
3798
+ };
3799
+ });
3800
+ }
3801
+ function readLineLeader(fn, mem, annot) {
3802
+ if (![
3803
+ "LL",
3804
+ "LLE",
3805
+ "LLO"
3806
+ ].some((key) => fn.FPDFAnnot_HasKey(annot, key))) return void 0;
3807
+ return withScratchN(mem, [
3808
+ 4,
3809
+ 4,
3810
+ 4
3811
+ ], ([length, extension, offset]) => {
3812
+ if (!fn.EPDFAnnot_GetLineLeader(annot, length, extension, offset)) return void 0;
3813
+ return {
3814
+ length: Number(mem.peek(length, "f32")),
3815
+ extension: Number(mem.peek(extension, "f32")),
3816
+ offset: Number(mem.peek(offset, "f32"))
3817
+ };
3818
+ });
3819
+ }
3820
+ //#endregion
3211
3821
  //#region src/features/annotations/internal/read/readLineAnnotation.ts
3212
3822
  /** Fallback `/L` when the annotation has no line geometry. */
3213
3823
  const ZERO_LINE = {
@@ -3222,8 +3832,15 @@ const ZERO_LINE = {
3222
3832
  };
3223
3833
  function readLine(fn, mem, annotPtr, base) {
3224
3834
  const rotation = readAnnotationRotation(fn, mem, annotPtr);
3835
+ const intent = readIntent(fn, mem, annotPtr);
3836
+ const caption = readLineCaption(fn, mem, annotPtr);
3837
+ const leader = readLineLeader(fn, mem, annotPtr);
3225
3838
  return {
3226
3839
  ...base,
3840
+ ...readAnnotationMeasure(fn, mem, annotPtr),
3841
+ ...intent === "LineDimension" || intent === "LineArrow" ? { intent } : {},
3842
+ ...caption ? { caption } : {},
3843
+ ...leader ? { leader } : {},
3227
3844
  subtype: "line",
3228
3845
  ...readFilledStyleExtras(fn, mem, annotPtr),
3229
3846
  linePoints: readLine$1(fn, mem, annotPtr) ?? ZERO_LINE,
@@ -3562,16 +4179,26 @@ function readVertexExtras(fn, mem, annotPtr) {
3562
4179
  };
3563
4180
  }
3564
4181
  function readPolygon(fn, mem, annotPtr, base) {
4182
+ const caption = readShapeCaption(fn, mem, annotPtr);
4183
+ const intent = readIntent(fn, mem, annotPtr);
3565
4184
  return {
3566
4185
  ...base,
4186
+ ...readAnnotationMeasure(fn, mem, annotPtr),
4187
+ ...caption ? { caption } : {},
4188
+ ...intent === "PolygonDimension" || intent === "PolygonCloud" ? { intent } : {},
3567
4189
  subtype: "polygon",
3568
4190
  ...readVertexExtras(fn, mem, annotPtr),
3569
4191
  cloudyIntensity: readBorderEffect(fn, mem, annotPtr)
3570
4192
  };
3571
4193
  }
3572
4194
  function readPolyline(fn, mem, annotPtr, base) {
4195
+ const caption = readShapeCaption(fn, mem, annotPtr);
4196
+ const intent = readIntent(fn, mem, annotPtr);
3573
4197
  return {
3574
4198
  ...base,
4199
+ ...readAnnotationMeasure(fn, mem, annotPtr),
4200
+ ...caption ? { caption } : {},
4201
+ ...intent === "PolyLineDimension" ? { intent } : {},
3575
4202
  subtype: "polyline",
3576
4203
  ...readVertexExtras(fn, mem, annotPtr),
3577
4204
  lineEndings: readLineEndings(fn, mem, annotPtr)
@@ -3680,18 +4307,18 @@ function pickReader(rawSubtypeCode) {
3680
4307
  * per session, closed before replacement and on session teardown via
3681
4308
  * {@link disposeFormModel}.
3682
4309
  */
3683
- const cache$1 = /* @__PURE__ */ new WeakMap();
4310
+ const cache$2 = /* @__PURE__ */ new WeakMap();
3684
4311
  function acquireFormModel(runtime, session) {
3685
4312
  const seq = session.mutationSeq();
3686
- const hit = cache$1.get(session);
4313
+ const hit = cache$2.get(session);
3687
4314
  if (hit && hit.seq === seq) return hit.ptr;
3688
4315
  if (hit) {
3689
4316
  runtime.fn.EPDFForm_CloseModel(hit.ptr);
3690
- cache$1.delete(session);
4317
+ cache$2.delete(session);
3691
4318
  }
3692
4319
  const ptr = runtime.fn.EPDFForm_LoadModel(session.requireDocPtr());
3693
4320
  if (ptr === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to build form model");
3694
- cache$1.set(session, {
4321
+ cache$2.set(session, {
3695
4322
  seq,
3696
4323
  ptr
3697
4324
  });
@@ -3699,10 +4326,10 @@ function acquireFormModel(runtime, session) {
3699
4326
  }
3700
4327
  /** Close and forget the session's cached model, if any. */
3701
4328
  function disposeFormModel(runtime, session) {
3702
- const hit = cache$1.get(session);
4329
+ const hit = cache$2.get(session);
3703
4330
  if (!hit) return;
3704
4331
  runtime.fn.EPDFForm_CloseModel(hit.ptr);
3705
- cache$1.delete(session);
4332
+ cache$2.delete(session);
3706
4333
  }
3707
4334
  //#endregion
3708
4335
  //#region src/features/forms/internal/readFormSnapshot.ts
@@ -3731,7 +4358,7 @@ const FF_EDIT = 1 << 18;
3731
4358
  const FF_MULTI_SELECT = 1 << 21;
3732
4359
  const FF_COMB = 1 << 24;
3733
4360
  const FF_RADIOS_IN_UNISON = 1 << 25;
3734
- function readWide(runtime, call) {
4361
+ function readWide$1(runtime, call) {
3735
4362
  return readUtf16String(runtime.mem, call) ?? "";
3736
4363
  }
3737
4364
  function readWideOrNull(runtime, call) {
@@ -3745,7 +4372,7 @@ function readToggleWidgets(runtime, model, fieldIndex) {
3745
4372
  annotObjectNumber: fn.EPDFForm_GetFieldWidgetObjNum(model, fieldIndex, w),
3746
4373
  pageObjectNumber: fn.EPDFForm_GetFieldWidgetPageObjNum(model, fieldIndex, w),
3747
4374
  onState: readUtf8String(runtime.mem, (buf, cap) => fn.EPDFForm_GetFieldWidgetOnState(model, fieldIndex, w, buf, cap)) ?? "",
3748
- exportValue: readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldWidgetExportValue(model, fieldIndex, w, buf, cap)),
4375
+ exportValue: readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldWidgetExportValue(model, fieldIndex, w, buf, cap)),
3749
4376
  checked: fn.EPDFForm_IsFieldWidgetChecked(model, fieldIndex, w)
3750
4377
  });
3751
4378
  return widgets;
@@ -3765,8 +4392,8 @@ function readOptions(runtime, model, fieldIndex) {
3765
4392
  const count = fn.EPDFForm_CountFieldOptions(model, fieldIndex);
3766
4393
  const options = [];
3767
4394
  for (let o = 0; o < count; o++) options.push({
3768
- label: readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionLabel(model, fieldIndex, o, buf, cap)),
3769
- value: readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionValue(model, fieldIndex, o, buf, cap)),
4395
+ label: readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionLabel(model, fieldIndex, o, buf, cap)),
4396
+ value: readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldOptionValue(model, fieldIndex, o, buf, cap)),
3770
4397
  selected: fn.EPDFForm_IsFieldOptionSelected(model, fieldIndex, o)
3771
4398
  });
3772
4399
  return options;
@@ -3775,7 +4402,7 @@ function readOptions(runtime, model, fieldIndex) {
3775
4402
  function readFieldAt(runtime, model, fieldIndex, docPtr, actionBudget = new ActionReadBudgetTracker()) {
3776
4403
  const { fn } = runtime;
3777
4404
  const fieldObjectNumber = fn.EPDFForm_GetFieldObjNum(model, fieldIndex);
3778
- const name = readWide(runtime, (buf, cap) => fn.EPDFForm_GetFieldName(model, fieldIndex, buf, cap));
4405
+ const name = readWide$1(runtime, (buf, cap) => fn.EPDFForm_GetFieldName(model, fieldIndex, buf, cap));
3779
4406
  const rawFlags = fn.EPDFForm_GetFieldFlags(model, fieldIndex);
3780
4407
  const family = FAMILY_BY_CODE[fn.EPDFForm_GetFieldFamily(model, fieldIndex)] ?? "unknown";
3781
4408
  const valueEntry = readValueEntry(runtime, model, fieldIndex, false);
@@ -3906,7 +4533,7 @@ function readValueEntry(runtime, model, fieldIndex, isDefault) {
3906
4533
  if (kind !== 1 && kind !== 2) return { kind: "unsupported" };
3907
4534
  const count = isDefault ? fn.EPDFForm_CountFieldDefaultValues(model, fieldIndex) : fn.EPDFForm_CountFieldValues(model, fieldIndex);
3908
4535
  const values = [];
3909
- 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)));
4536
+ 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)));
3910
4537
  return kind === 1 ? {
3911
4538
  kind: "scalar",
3912
4539
  value: values[0] ?? ""
@@ -4247,13 +4874,13 @@ function readAnnotationActions(fn, mem, docPtr, annotPtr, budget) {
4247
4874
  * releasing any enclosing `pagePtr`.
4248
4875
  */
4249
4876
  function collectPageAnnotations(input) {
4250
- const { runtime, session, pageObjectNumber, count, getAnnotPtrAt, signal } = input;
4877
+ const { runtime, session, pageObjectNumber, count, getAnnotPtrAt, signal, fonts } = input;
4251
4878
  const { fn, mem } = runtime;
4252
4879
  const annotations = [];
4253
4880
  let hasWeak = false;
4254
4881
  const revision = session.pageState(pageObjectNumber).revision;
4255
4882
  const actionBudget = new ActionReadBudgetTracker();
4256
- const readCtx = readContextFor(session);
4883
+ const readCtx = readContextFor(session, fonts);
4257
4884
  for (let i = 0; i < count; i++) {
4258
4885
  throwIfAborted(signal);
4259
4886
  const annotPtr = getAnnotPtrAt(i);
@@ -4289,9 +4916,10 @@ function collectPageAnnotations(input) {
4289
4916
  * as they land - the public DTO surface stays the same.
4290
4917
  */
4291
4918
  var AnnotationReader = class {
4292
- constructor(runtime, session) {
4919
+ constructor(runtime, session, fonts) {
4293
4920
  this.runtime = runtime;
4294
4921
  this.session = session;
4922
+ this.fonts = fonts;
4295
4923
  }
4296
4924
  list(pageObjectNumber, signal) {
4297
4925
  throwIfAborted(signal);
@@ -4306,7 +4934,8 @@ var AnnotationReader = class {
4306
4934
  pageObjectNumber,
4307
4935
  count,
4308
4936
  getAnnotPtrAt: (i) => fn.FPDFPage_GetAnnot(pagePtr, i),
4309
- signal
4937
+ signal,
4938
+ ...this.fonts ? { fonts: this.fonts } : {}
4310
4939
  });
4311
4940
  } finally {
4312
4941
  pool.release(pageObjectNumber);
@@ -4635,9 +5264,10 @@ function normalizeScale(scale) {
4635
5264
  * actually need a pagePtr to materialise their fields.
4636
5265
  */
4637
5266
  var RawAnnotationReader = class {
4638
- constructor(runtime, session) {
5267
+ constructor(runtime, session, fonts) {
4639
5268
  this.runtime = runtime;
4640
5269
  this.session = session;
5270
+ this.fonts = fonts;
4641
5271
  }
4642
5272
  listAll(signal) {
4643
5273
  throwIfAborted(signal);
@@ -4663,11 +5293,157 @@ var RawAnnotationReader = class {
4663
5293
  pageObjectNumber,
4664
5294
  count,
4665
5295
  getAnnotPtrAt: (i) => fn.EPDFPage_GetAnnotRaw(docPtr, record.pageIndex, i),
4666
- signal
5296
+ signal,
5297
+ ...this.fonts ? { fonts: this.fonts } : {}
4667
5298
  });
4668
5299
  }
4669
5300
  };
4670
5301
  //#endregion
5302
+ //#region src/features/annotations/internal/mutations/prepareMeasurementMutation.ts
5303
+ const kinds = /* @__PURE__ */ new Set([
5304
+ "line",
5305
+ "polygon",
5306
+ "polyline"
5307
+ ]);
5308
+ const inputs = [
5309
+ "linePoints",
5310
+ "vertices",
5311
+ "measure",
5312
+ "intent",
5313
+ "contents"
5314
+ ];
5315
+ function validate(value) {
5316
+ if (!kinds.has(value.subtype)) return;
5317
+ const v = value;
5318
+ try {
5319
+ if (v.measure !== void 0 && v.measure !== null) assertWritableMeasure(v.measure);
5320
+ if (v.caption != null && (typeof v.caption !== "object" || Array.isArray(v.caption))) throw new RangeError("Invalid caption");
5321
+ if (v.subtype === "line" && v.leader != null && (typeof v.leader !== "object" || Array.isArray(v.leader))) throw new RangeError("Invalid line leader");
5322
+ const intents = v.subtype === "line" ? ["LineDimension", "LineArrow"] : v.subtype === "polygon" ? ["PolygonDimension", "PolygonCloud"] : ["PolyLineDimension"];
5323
+ if (v.intent != null && !intents.includes(v.intent)) throw new RangeError("Invalid measurement intent");
5324
+ if (v.caption) {
5325
+ if (v.caption.enabled !== void 0 && typeof v.caption.enabled !== "boolean") throw new RangeError("Invalid caption visibility");
5326
+ if (v.subtype === "line") {
5327
+ if (v.caption.position != null && !["inline", "top"].includes(v.caption.position)) throw new RangeError("Invalid caption position");
5328
+ if (v.caption.offset) {
5329
+ assertPdfFloat(v.caption.offset.along);
5330
+ assertPdfFloat(v.caption.offset.perpendicular);
5331
+ }
5332
+ } else if (v.caption.center) {
5333
+ assertPdfFloat(v.caption.center.x);
5334
+ assertPdfFloat(v.caption.center.y);
5335
+ }
5336
+ }
5337
+ if (v.subtype === "line" && v.leader) {
5338
+ for (const n of [
5339
+ v.leader.length,
5340
+ v.leader.extension ?? 0,
5341
+ v.leader.offset ?? 0
5342
+ ]) assertPdfFloat(n);
5343
+ if ((v.leader.extension ?? 0) < 0 || (v.leader.offset ?? 0) < 0) throw new RangeError("Leader extension and offset must be nonnegative");
5344
+ }
5345
+ const points = v.subtype === "line" ? v.linePoints ? [v.linePoints.start, v.linePoints.end] : [] : v.vertices ?? [];
5346
+ for (const p of points) {
5347
+ assertPdfFloat(p.x);
5348
+ assertPdfFloat(p.y);
5349
+ }
5350
+ } catch (error) {
5351
+ throw new EngineError(EngineErrorCode.InvalidArg, error.message);
5352
+ }
5353
+ }
5354
+ /** Infer a rigid transform only when ALL corresponding vertices agree. Vertex edits leave a manual center fixed. */
5355
+ function moveCenter(before, after, center) {
5356
+ if (before.length !== after.length || before.length < 2) return void 0;
5357
+ const a = before[0], b = after[0];
5358
+ const index = before.findIndex((p) => Math.hypot(p.x - a.x, p.y - a.y) > 1e-6);
5359
+ if (index < 0) return void 0;
5360
+ const u = {
5361
+ x: before[index].x - a.x,
5362
+ y: before[index].y - a.y
5363
+ };
5364
+ const v = {
5365
+ x: after[index].x - b.x,
5366
+ y: after[index].y - b.y
5367
+ };
5368
+ const length = u.x * u.x + u.y * u.y;
5369
+ const cos = (u.x * v.x + u.y * v.y) / length, sin = (u.x * v.y - u.y * v.x) / length;
5370
+ if (Math.abs(cos * cos + sin * sin - 1) > 1e-5) return void 0;
5371
+ const transform = (p) => ({
5372
+ x: b.x + cos * (p.x - a.x) - sin * (p.y - a.y),
5373
+ y: b.y + sin * (p.x - a.x) + cos * (p.y - a.y)
5374
+ });
5375
+ if (!before.every((p, i) => {
5376
+ const t = transform(p);
5377
+ return Math.hypot(t.x - after[i].x, t.y - after[i].y) <= .001;
5378
+ })) return void 0;
5379
+ return transform(center);
5380
+ }
5381
+ function prepareMeasurementDraft(draft) {
5382
+ validate(draft);
5383
+ if ((draft.subtype === "line" || draft.subtype === "polygon" || draft.subtype === "polyline") && draft.caption && draft.caption.enabled === void 0) throw new EngineError(EngineErrorCode.InvalidArg, "A new caption requires enabled");
5384
+ return deriveMeasurementLabel(draft);
5385
+ }
5386
+ function prepareMeasurementPatch(fn, annot, current, patch) {
5387
+ if (patch.subtype !== current.subtype) throw new EngineError(EngineErrorCode.InvalidArg, "Annotation subtype cannot change");
5388
+ validate(patch);
5389
+ if (patch.subtype !== "line" && patch.subtype !== "polygon" && patch.subtype !== "polyline" || current.subtype !== "line" && current.subtype !== "polygon" && current.subtype !== "polyline") return patch;
5390
+ if (patch.measure && current.measure && current.measure.subtype !== "RL") throw new EngineError(EngineErrorCode.InvalidArg, "Remove the foreign measure explicitly before replacing it");
5391
+ if (patch.subtype !== "line" && patch.caption && fn.FPDFAnnot_HasKey(annot, "EMBD_Metadata") && fn.FPDFAnnot_GetValueType(annot, "EMBD_Metadata") !== 6) throw new EngineError(EngineErrorCode.InvalidArg, "Malformed annotation metadata cannot store a caption");
5392
+ if (patch.subtype !== "line" && current.subtype !== "line" && patch.vertices && patch.caption !== null && patch.caption?.center === void 0 && current.caption?.center) {
5393
+ const center = moveCenter(current.vertices, patch.vertices, current.caption.center);
5394
+ if (center) patch = {
5395
+ ...patch,
5396
+ caption: {
5397
+ ...current.caption,
5398
+ ...patch.caption,
5399
+ center
5400
+ }
5401
+ };
5402
+ }
5403
+ if (patch.caption) {
5404
+ if (patch.subtype === "line" && current.subtype === "line") {
5405
+ const c = {
5406
+ enabled: false,
5407
+ ...current.caption,
5408
+ ...patch.caption
5409
+ };
5410
+ patch = {
5411
+ ...patch,
5412
+ caption: {
5413
+ enabled: c.enabled,
5414
+ position: c.position ?? "inline",
5415
+ ...c.offset ? { offset: c.offset } : {}
5416
+ }
5417
+ };
5418
+ } else if (patch.subtype !== "line" && current.subtype !== "line") {
5419
+ const c = {
5420
+ enabled: false,
5421
+ ...current.caption,
5422
+ ...patch.caption
5423
+ };
5424
+ patch = {
5425
+ ...patch,
5426
+ caption: {
5427
+ enabled: c.enabled,
5428
+ ...c.center ? { center: c.center } : {}
5429
+ }
5430
+ };
5431
+ }
5432
+ }
5433
+ if (inputs.some((key) => patch[key] !== void 0)) {
5434
+ const readout = measurementReadout({
5435
+ ...current,
5436
+ ...patch
5437
+ });
5438
+ if (isReadout(readout)) patch = {
5439
+ ...patch,
5440
+ contents: readout.label
5441
+ };
5442
+ }
5443
+ validate(patch);
5444
+ return patch;
5445
+ }
5446
+ //#endregion
4671
5447
  //#region src/features/annotations/internal/write/annotationWritePrimitives.ts
4672
5448
  /**
4673
5449
  * Write-side twin of `annotationReadPrimitives.ts`. Every annotation
@@ -5604,6 +6380,20 @@ const DEFAULT_FREETEXT_COLOR = {
5604
6380
  b: 0
5605
6381
  };
5606
6382
  /**
6383
+ * Write rich text through the engine's rich writer: `/RC`, `/DS`, `/DA`,
6384
+ * `/Contents` and the appearance, all or nothing. Faces in the input are
6385
+ * resolved from keys / standard names to the identities the engine names.
6386
+ */
6387
+ function writeRichText(fn, annotPtr, input, ctx) {
6388
+ const json = engineRichTextJson(input, ctx?.describeRegisteredFont);
6389
+ if (!fn.EPDFAnnot_SetRichTextJSON(annotPtr, json)) throw new EngineError(EngineErrorCode.InvalidArg, "rich text could not be written (empty rect, or a font resource could not be built)");
6390
+ }
6391
+ /** `#RRGGBB` for a rich body colour. */
6392
+ function hexColor(color) {
6393
+ const hex = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
6394
+ return `#${hex(color.r)}${hex(color.g)}${hex(color.b)}`.toUpperCase();
6395
+ }
6396
+ /**
5607
6397
  * Apply a free-text draft to a freshly-created annotation. Colour model:
5608
6398
  * - `color` -> `/DA` colour = border + default text colour.
5609
6399
  * - `fontColor` (optional) -> `TextColor` channel, overriding text only;
@@ -5645,6 +6435,7 @@ function applyFreeTextDraft(fn, mem, annotPtr, draft, ctx) {
5645
6435
  rotation: draft.rotation,
5646
6436
  unrotatedRect: draft.unrotatedRect
5647
6437
  });
6438
+ writeRichText(fn, annotPtr, draft.richText ?? { paragraphs: richTextParagraphsFromPlainText(draft.contents ?? "") }, ctx);
5648
6439
  }
5649
6440
  /**
5650
6441
  * Apply a free-text patch to an existing annotation. Only present fields are
@@ -5671,11 +6462,30 @@ function applyFreeTextPatch(fn, mem, annotPtr, patch, ctx) {
5671
6462
  else setAnnotColor(fn, annotPtr, patch.interiorColor, FPDFANNOT_COLORTYPE.Color);
5672
6463
  if (patch.opacity !== void 0) setAnnotOpacity(fn, annotPtr, patch.opacity);
5673
6464
  applyBorderPatch(fn, mem, annotPtr, patch);
5674
- if (patch.fontFamily !== void 0 || patch.fontSize !== void 0 || patch.color !== void 0) {
6465
+ const current = readEngineRichText(fn, mem, annotPtr);
6466
+ if (patch.color !== void 0) {
5675
6467
  const cur = readDefaultAppearance(fn, mem, annotPtr);
5676
- 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);
6468
+ applyDefaultAppearance(fn, annotPtr, cur ? standardFontFromCode(cur.fontCode) : DEFAULT_STANDARD_FONT, cur && cur.fontSize > 0 ? cur.fontSize : 12, patch.color, ctx);
5677
6469
  }
5678
6470
  if (patch.fontColor !== void 0) setAnnotColor(fn, annotPtr, patch.fontColor, FPDFANNOT_COLORTYPE.TextColor);
6471
+ if (patch.richText !== void 0) writeRichText(fn, annotPtr, patch.richText, ctx);
6472
+ else if (patch.contents !== void 0) writeRichText(fn, annotPtr, { paragraphs: richTextParagraphsFromPlainText(patch.contents ?? "") }, ctx);
6473
+ else if (current && (patch.fontFamily !== void 0 || patch.fontSize !== void 0 || patch.fontColor !== void 0 || patch.textAlign !== void 0)) {
6474
+ const body = { ...current.body };
6475
+ if (patch.fontFamily !== void 0) {
6476
+ const face = faceForFreeTextFont(patch.fontFamily, ctx?.describeRegisteredFont);
6477
+ body.family = face.family;
6478
+ if (face.weight !== void 0) body.weight = face.weight;
6479
+ if (face.italic !== void 0) body.italic = face.italic;
6480
+ }
6481
+ if (patch.fontSize !== void 0) body.size = patch.fontSize;
6482
+ if (patch.fontColor !== void 0) body.color = hexColor(patch.fontColor);
6483
+ if (patch.textAlign !== void 0) body.align = patch.textAlign;
6484
+ writeRichText(fn, annotPtr, {
6485
+ body,
6486
+ paragraphs: current.paragraphs
6487
+ }, ctx);
6488
+ }
5679
6489
  if (patch.textAlign !== void 0) setTextAlignment(fn, annotPtr, textAlignmentToCode(patch.textAlign));
5680
6490
  if (patch.intent !== void 0) setIntent(fn, annotPtr, freeTextIntentToName(patch.intent));
5681
6491
  if (patch.rectDifferences === null) clearRectangleDifferences(fn, annotPtr);
@@ -5728,6 +6538,36 @@ function isInkSubtype(subtype) {
5728
6538
  return subtype === "ink";
5729
6539
  }
5730
6540
  //#endregion
6541
+ //#region src/features/annotations/internal/write/writeMeasurementFields.ts
6542
+ /** Receives normalized complete captions from the mutator, never partial state. */
6543
+ function writeMeasurementFields(fn, mem, annot, value) {
6544
+ if (value.intent !== void 0) if (value.intent === null) fn.EPDFAnnot_RemoveKey(annot, "IT");
6545
+ else requireMeasureWrite(fn.EPDFAnnot_SetIntent(annot, value.intent));
6546
+ if (value.measure !== void 0) if (value.measure === null) requireMeasureWrite(fn.EPDFAnnot_RemoveMeasure(annot));
6547
+ else writeMeasure(fn, mem, fn.EPDFAnnot_AddMeasure(annot), value.measure);
6548
+ if (value.subtype === "line") {
6549
+ if (value.leader !== void 0) requireMeasureWrite(fn.EPDFAnnot_SetLineLeader(annot, value.leader?.length ?? 0, value.leader?.extension ?? 0, value.leader?.offset ?? 0));
6550
+ if (value.caption !== void 0) if (value.caption === null) for (const key of [
6551
+ "Cap",
6552
+ "CP",
6553
+ "CO"
6554
+ ]) fn.EPDFAnnot_RemoveKey(annot, key);
6555
+ else {
6556
+ const c = value.caption;
6557
+ requireMeasureWrite(withMeasurePoint(mem, c.offset ? {
6558
+ x: c.offset.along,
6559
+ y: c.offset.perpendicular
6560
+ } : void 0, (p) => fn.EPDFAnnot_SetLineCaption(annot, c.enabled ?? false, c.position === "top" ? 1 : 0, p)));
6561
+ }
6562
+ } else if (value.caption !== void 0) if (value.caption === null) {
6563
+ fn.EPDFAnnot_ClearEmbedMetadataKey(annot, "MeasurementCaption");
6564
+ fn.EPDFAnnot_ClearEmbedMetadataKey(annot, "MeasurementCaptionCenter");
6565
+ } else {
6566
+ const c = value.caption;
6567
+ requireMeasureWrite(withMeasurePoint(mem, c.center ?? void 0, (p) => fn.EPDFAnnot_SetShapeCaption(annot, c.enabled ?? false, p)));
6568
+ }
6569
+ }
6570
+ //#endregion
5731
6571
  //#region src/features/annotations/internal/write/writeLineAnnotation.ts
5732
6572
  /** Default line endings when a line draft omits them. */
5733
6573
  const DEFAULT_LINE_ENDINGS$1 = {
@@ -5744,6 +6584,7 @@ const DEFAULT_LINE_ENDINGS$1 = {
5744
6584
  */
5745
6585
  function applyLineDraft(fn, mem, annotPtr, draft) {
5746
6586
  applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
6587
+ writeMeasurementFields(fn, mem, annotPtr, draft);
5747
6588
  setAnnotRect(fn, mem, annotPtr, draft.rect);
5748
6589
  applyFilledStyleDraft(fn, mem, annotPtr, draft);
5749
6590
  setLine(fn, mem, annotPtr, draft.linePoints);
@@ -5752,6 +6593,7 @@ function applyLineDraft(fn, mem, annotPtr, draft) {
5752
6593
  }
5753
6594
  function applyLinePatch(fn, mem, annotPtr, patch) {
5754
6595
  applyAnnotationBasePatch(fn, mem, annotPtr, patch);
6596
+ writeMeasurementFields(fn, mem, annotPtr, patch);
5755
6597
  if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
5756
6598
  applyFilledStylePatch(fn, mem, annotPtr, patch);
5757
6599
  if (patch.linePoints !== void 0) setLine(fn, mem, annotPtr, patch.linePoints);
@@ -6410,6 +7252,7 @@ const DEFAULT_LINE_ENDINGS = {
6410
7252
  */
6411
7253
  function applyPolygonDraft(fn, mem, annotPtr, draft) {
6412
7254
  applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
7255
+ writeMeasurementFields(fn, mem, annotPtr, draft);
6413
7256
  setAnnotRect(fn, mem, annotPtr, draft.rect);
6414
7257
  applyFilledStyleDraft(fn, mem, annotPtr, draft);
6415
7258
  setVertices(fn, mem, annotPtr, draft.vertices);
@@ -6418,6 +7261,7 @@ function applyPolygonDraft(fn, mem, annotPtr, draft) {
6418
7261
  }
6419
7262
  function applyPolygonPatch(fn, mem, annotPtr, patch) {
6420
7263
  applyAnnotationBasePatch(fn, mem, annotPtr, patch);
7264
+ writeMeasurementFields(fn, mem, annotPtr, patch);
6421
7265
  if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
6422
7266
  applyFilledStylePatch(fn, mem, annotPtr, patch);
6423
7267
  if (patch.vertices !== void 0) setVertices(fn, mem, annotPtr, patch.vertices);
@@ -6435,6 +7279,7 @@ function applyPolygonPatch(fn, mem, annotPtr, patch) {
6435
7279
  */
6436
7280
  function applyPolylineDraft(fn, mem, annotPtr, draft) {
6437
7281
  applyAnnotationBaseDraft(fn, mem, annotPtr, draft);
7282
+ writeMeasurementFields(fn, mem, annotPtr, draft);
6438
7283
  setAnnotRect(fn, mem, annotPtr, draft.rect);
6439
7284
  applyFilledStyleDraft(fn, mem, annotPtr, draft);
6440
7285
  setVertices(fn, mem, annotPtr, draft.vertices);
@@ -6443,6 +7288,7 @@ function applyPolylineDraft(fn, mem, annotPtr, draft) {
6443
7288
  }
6444
7289
  function applyPolylinePatch(fn, mem, annotPtr, patch) {
6445
7290
  applyAnnotationBasePatch(fn, mem, annotPtr, patch);
7291
+ writeMeasurementFields(fn, mem, annotPtr, patch);
6446
7292
  if (patch.rect !== void 0) setAnnotRect(fn, mem, annotPtr, patch.rect);
6447
7293
  applyFilledStylePatch(fn, mem, annotPtr, patch);
6448
7294
  if (patch.vertices !== void 0) setVertices(fn, mem, annotPtr, patch.vertices);
@@ -6725,7 +7571,10 @@ var AnnotationMutator = class {
6725
7571
  writeContext(pagePtr, resources) {
6726
7572
  const fonts = this.fonts;
6727
7573
  return {
6728
- ...fonts ? { resolveRegisteredFontId: (key) => fonts.idFor(key) } : {},
7574
+ ...fonts ? {
7575
+ resolveRegisteredFontId: (key) => fonts.idFor(key),
7576
+ describeRegisteredFont: (key) => fonts.describeOrUndefined(key)
7577
+ } : {},
6729
7578
  docPtr: this.session.requireDocPtr(),
6730
7579
  pagePtr,
6731
7580
  ...resources ? { resources } : {}
@@ -6741,6 +7590,8 @@ var AnnotationMutator = class {
6741
7590
  this.ensureKnownWeakStateFromPage(pageObjectNumber, pagePtr);
6742
7591
  const writeCtx = this.writeContext(pagePtr, resources);
6743
7592
  preflightDraft(draft, writeCtx);
7593
+ assertRichTextAgreement(draft);
7594
+ draft = prepareMeasurementDraft(draft);
6744
7595
  const pageStateBefore = this.session.pageState(pageObjectNumber);
6745
7596
  throwIfAborted(signal);
6746
7597
  const annotPtr = fn.EPDFPage_CreateAnnot(pagePtr, subtypeCode);
@@ -6763,7 +7614,7 @@ var AnnotationMutator = class {
6763
7614
  if (newObjNum <= 0) throw new EngineError(EngineErrorCode.Unknown, `EPDFPage_CreateAnnot produced a direct object (no objectNumber); fork helper invariant broken`);
6764
7615
  newIndex = fn.FPDFPage_GetAnnotIndex(pagePtr, annotPtr);
6765
7616
  if (newIndex < 0) throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_GetAnnotIndex returned ${newIndex} for freshly created annotation`);
6766
- dto = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session));
7617
+ dto = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session, this.fonts));
6767
7618
  joinWidgetFieldNumbers(this.runtime, this.session, [dto]);
6768
7619
  } finally {
6769
7620
  fn.FPDFPage_CloseAnnot(annotPtr);
@@ -6798,15 +7649,21 @@ var AnnotationMutator = class {
6798
7649
  throwIfAborted(signal);
6799
7650
  const writeCtx = this.writeContext(pagePtr, resources);
6800
7651
  preflightPatch(patch, writeCtx);
7652
+ assertRichTextAgreement(patch);
6801
7653
  this.ensureKnownWeakStateFromPage(ref.pageObjectNumber, pagePtr);
6802
7654
  const pageStateBefore = this.session.pageState(ref.pageObjectNumber);
6803
- throwIfAborted(signal);
6804
- const stableId = this.captureOrStampStableId(annotPtr);
6805
7655
  const previousBlendMode = blendModeFromCode(fn.EPDFAnnot_GetBlendMode(annotPtr));
6806
7656
  const preIndex = fn.FPDFPage_GetAnnotIndex(pagePtr, annotPtr);
6807
7657
  if (preIndex < 0) throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_GetAnnotIndex returned ${preIndex} before update`);
6808
- const currentDto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, preIndex, pageStateBefore.revision, readContextFor(this.session));
7658
+ const currentDto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, preIndex, pageStateBefore.revision, readContextFor(this.session, this.fonts));
7659
+ patch = prepareMeasurementPatch(fn, annotPtr, currentDto, patch);
7660
+ throwIfAborted(signal);
7661
+ const stableId = this.captureOrStampStableId(annotPtr);
6809
7662
  applyPatch(fn, mem, annotPtr, patch, writeCtx);
7663
+ if (patch.contents !== void 0 && patch.contents !== currentDto.contents && isDimension({
7664
+ ...currentDto,
7665
+ ...patch
7666
+ })) fn.EPDFAnnot_RemoveKey(annotPtr, "RC");
6810
7667
  let linkedParentId = null;
6811
7668
  if (patch.inReplyTo !== void 0 || patch.replyType !== void 0) linkedParentId = writeAnnotationRelationship(this.runtime, this.session, pagePtr, annotPtr, ref.pageObjectNumber, {
6812
7669
  inReplyTo: patch.inReplyTo,
@@ -6832,7 +7689,7 @@ var AnnotationMutator = class {
6832
7689
  annotPtr = resolveAnnotPtr(this.runtime, this.session, pagePtr, ref);
6833
7690
  const newIndex = fn.FPDFPage_GetAnnotIndex(pagePtr, annotPtr);
6834
7691
  if (newIndex < 0) throw new EngineError(EngineErrorCode.Unknown, `FPDFPage_GetAnnotIndex returned ${newIndex} after update`);
6835
- const dto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session));
7692
+ const dto = readAnnotationFromPtr(fn, mem, annotPtr, ref.pageObjectNumber, newIndex, pageStateBefore.revision, readContextFor(this.session, this.fonts));
6836
7693
  joinWidgetFieldNumbers(this.runtime, this.session, [dto]);
6837
7694
  this.recordWeakStateFromPage(ref.pageObjectNumber, pagePtr);
6838
7695
  const meta = computeMutationImpact({
@@ -7045,7 +7902,7 @@ var AnnotationMutator = class {
7045
7902
  const annotPtr = fn.FPDFPage_GetAnnot(pagePtr, newIdx);
7046
7903
  if (!annotPtr) throw new EngineError(EngineErrorCode.Unknown, `failed to re-read moved annotation at index ${newIdx}`);
7047
7904
  try {
7048
- moved[i] = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIdx, bumpedRev, readContextFor(this.session));
7905
+ moved[i] = readAnnotationFromPtr(fn, mem, annotPtr, pageObjectNumber, newIdx, bumpedRev, readContextFor(this.session, this.fonts));
7049
7906
  } finally {
7050
7907
  fn.FPDFPage_CloseAnnot(annotPtr);
7051
7908
  }
@@ -8307,104 +9164,2176 @@ function layerSaveError(status) {
8307
9164
  return new EngineError(EngineErrorCode.DocOpenFailed, "failed to save layer artifact");
8308
9165
  }
8309
9166
  //#endregion
8310
- //#region src/features/save/DocumentSaver.ts
9167
+ //#region src/features/save/internal/readSavedBuffer.ts
9168
+ /** Keep the runtime's owned copy when it already occupies an entire ArrayBuffer. */
9169
+ function readSavedBuffer(mem, ptr, size) {
9170
+ const bytes = mem.readBytes(ptr, size);
9171
+ if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) return bytes.buffer;
9172
+ return new Uint8Array(bytes).buffer;
9173
+ }
9174
+ //#endregion
9175
+ //#region src/runtime/memory/u64.ts
8311
9176
  /**
8312
- * Owns every "serialize the open document" path for a `DocumentSession`.
8313
- * Two distinct outputs:
8314
- *
8315
- * - Layer artifacts (`saveLayerArtifact*`) are the storage-optimized
8316
- * `.layer` delta used by server-side persistence. Only valid for a
8317
- * `layer` session.
8318
- * - Standalone saves (`saveStandalone*`) export a self-contained PDF
8319
- * view (base + applied delta) via `FPDF_SaveAsCopy` /
8320
- * `EPDF_SaveDocumentToOwnedBuffer`, valid for any session kind.
8321
- *
8322
- * Lives in `engine-services` so the browser Worker and the Node
8323
- * `worker_thread` server share the exact same save code path.
9177
+ * 64-bit out-params as two little-endian 32-bit halves: the wasm memory
9178
+ * helpers have no 64-bit accessors (Emscripten's `setValue(i64)` needs
9179
+ * WASM_BIGINT), and the native runtime reads the same layout.
8324
9180
  */
8325
- var DocumentSaver = class {
8326
- constructor(runtime, session) {
8327
- this.runtime = runtime;
8328
- this.session = session;
9181
+ function pokeU64(mem, ptr, value, byteOffset = 0) {
9182
+ const big = BigInt(value);
9183
+ mem.poke(ptr, "i32", Number(big & 4294967295n) | 0, byteOffset);
9184
+ mem.poke(ptr, "i32", Number(big >> 32n & 4294967295n) | 0, byteOffset + 4);
9185
+ }
9186
+ function peekU64(mem, ptr, byteOffset = 0) {
9187
+ const lo = Number(mem.peek(ptr, "i32", byteOffset)) >>> 0;
9188
+ return (Number(mem.peek(ptr, "i32", byteOffset + 4)) >>> 0) * 4294967296 + lo;
9189
+ }
9190
+ //#endregion
9191
+ //#region src/features/signature/internal/readSignatureModel.ts
9192
+ const KIND_DOC_TIMESTAMP = 1;
9193
+ const COVERAGE_BY_CODE = {
9194
+ 0: "whole-revision",
9195
+ 1: "partial",
9196
+ 2: "malformed"
9197
+ };
9198
+ const STRING_FILTER = 0;
9199
+ const STRING_SUBFILTER = 1;
9200
+ const STRING_NAME = 2;
9201
+ const STRING_REASON = 3;
9202
+ const STRING_LOCATION = 4;
9203
+ const STRING_CONTACT_INFO = 5;
9204
+ const STRING_M = 6;
9205
+ const FIELD_ACTION_BY_CODE = {
9206
+ 1: "all",
9207
+ 2: "include",
9208
+ 3: "exclude"
9209
+ };
9210
+ const FIELDS_FIELDMDP = 0;
9211
+ const FIELDS_LOCK = 1;
9212
+ const SV_V = 4;
9213
+ const SV_MDP = 1 << 17;
9214
+ const SV_LIST_SUBFILTER = 0;
9215
+ const SV_LIST_DIGEST_METHOD = 1;
9216
+ const SV_LIST_REASONS = 2;
9217
+ /** The PDF 1.7 seed-value entry set this engine implements. */
9218
+ const SUPPORTED_SEED_VALUE_VERSION = 2;
9219
+ const SV_UNSUPPORTED_REQUIRED = 432;
9220
+ function readWide(runtime, call) {
9221
+ return readUtf16String(runtime.mem, call, "");
9222
+ }
9223
+ function isPermission(value) {
9224
+ return value === 1 || value === 2 || value === 3;
9225
+ }
9226
+ function readByteRange(runtime, model, index) {
9227
+ const { fn, mem } = runtime;
9228
+ return withScratch(mem, 32, (ptr) => {
9229
+ for (let k = 0; k < 4; k++) pokeU64(mem, ptr, 0, k * 8);
9230
+ if (!fn.EPDFSig_GetByteRange(model, index, ptr)) return null;
9231
+ return [
9232
+ peekU64(mem, ptr, 0),
9233
+ peekU64(mem, ptr, 8),
9234
+ peekU64(mem, ptr, 16),
9235
+ peekU64(mem, ptr, 24)
9236
+ ];
9237
+ });
9238
+ }
9239
+ function readLock(runtime, model, index, which) {
9240
+ const { fn } = runtime;
9241
+ const code = which === FIELDS_LOCK ? fn.EPDFSig_GetLockAction(model, index) : fn.EPDFSig_GetFieldMDPAction(model, index);
9242
+ const action = FIELD_ACTION_BY_CODE[code];
9243
+ if (!action) return null;
9244
+ const fields = [];
9245
+ const count = fn.EPDFSig_GetFieldNameCount(model, index, which);
9246
+ for (let n = 0; n < count; n++) fields.push(readWide(runtime, (buf, cap) => fn.EPDFSig_GetFieldNameAt(model, index, which, n, buf, cap)) ?? "");
9247
+ const spec = {
9248
+ action,
9249
+ fields
9250
+ };
9251
+ if (which === FIELDS_LOCK) {
9252
+ const permission = fn.EPDFSig_GetLockPermission(model, index);
9253
+ if (isPermission(permission)) spec.permission = permission;
8329
9254
  }
8330
- saveLayerArtifact() {
8331
- this.requireLayer();
8332
- const { mem, fn } = this.runtime;
8333
- const sizePtr = mem.alloc(4);
8334
- const statusPtr = mem.alloc(4);
8335
- let artifactPtr = null;
8336
- try {
8337
- mem.poke(sizePtr, "i32", 0);
8338
- mem.poke(statusPtr, "i32", -1);
8339
- artifactPtr = fn.EPDFLayer_SaveLayerArtifactToOwnedBuffer(this.session.requireDocPtr(), sizePtr, statusPtr);
8340
- const status = Number(mem.peek(statusPtr, "i32"));
8341
- const size = Number(mem.peek(sizePtr, "i32"));
8342
- if (!artifactPtr || status !== 0 || size <= 0) throw layerSaveError(status);
8343
- const bytes = mem.readBytes(artifactPtr, size);
8344
- const buffer = new ArrayBuffer(bytes.byteLength);
8345
- new Uint8Array(buffer).set(bytes);
8346
- return {
8347
- bytes: buffer,
8348
- size
8349
- };
8350
- } finally {
8351
- if (artifactPtr) fn.EPDF_FreeBuffer(artifactPtr);
8352
- mem.free(statusPtr);
8353
- mem.free(sizePtr);
8354
- }
9255
+ return spec;
9256
+ }
9257
+ function readSeedValue(runtime, model, index) {
9258
+ const { fn } = runtime;
9259
+ if (!fn.EPDFSig_HasSeedValue(model, index)) return null;
9260
+ const requiredFlags = fn.EPDFSig_GetSeedValueRequiredFlags(model, index) >>> 0;
9261
+ const presentFlags = fn.EPDFSig_GetSeedValuePresentFlags(model, index) >>> 0;
9262
+ const version = fn.EPDFSig_GetSeedValueVersion(model, index);
9263
+ const mdp = fn.EPDFSig_GetSeedValueMDP(model, index);
9264
+ const list = (which) => {
9265
+ const count = fn.EPDFSig_GetSeedValueListCount(model, index, which);
9266
+ const items = [];
9267
+ for (let n = 0; n < count; n++) items.push(readWide(runtime, (buf, cap) => fn.EPDFSig_GetSeedValueListAt(model, index, which, n, buf, cap)) ?? "");
9268
+ return items;
9269
+ };
9270
+ return {
9271
+ requiredFlags,
9272
+ presentFlags,
9273
+ version: version > 0 ? version : null,
9274
+ mdp: (presentFlags & SV_MDP) !== 0 && mdp >= 0 && mdp <= 3 ? mdp : null,
9275
+ filter: readWide(runtime, (buf, cap) => fn.EPDFSig_GetSeedValueFilter(model, index, buf, cap)),
9276
+ subFilters: list(SV_LIST_SUBFILTER),
9277
+ digestMethods: list(SV_LIST_DIGEST_METHOD),
9278
+ reasons: list(SV_LIST_REASONS),
9279
+ unsupportedRequired: (requiredFlags & SV_UNSUPPORTED_REQUIRED) !== 0 || (requiredFlags & SV_V) !== 0 && version > SUPPORTED_SEED_VALUE_VERSION
9280
+ };
9281
+ }
9282
+ /** Every signature field of a native signature model, in model order. */
9283
+ function readSignaturesFromModel(runtime, model) {
9284
+ const { fn } = runtime;
9285
+ const count = fn.EPDFSig_Count(model);
9286
+ const out = [];
9287
+ const str = (i, key) => readWide(runtime, (buf, cap) => fn.EPDFSig_GetString(model, i, key, buf, cap));
9288
+ for (let i = 0; i < count; i++) {
9289
+ const signed = fn.EPDFSig_IsSigned(model, i);
9290
+ const widgetObjNum = fn.EPDFSig_GetWidgetObjNum(model, i);
9291
+ const revisionIndex = fn.EPDFSig_GetRevisionIndex(model, i);
9292
+ const docMdp = fn.EPDFSig_GetDocMDPPermission(model, i);
9293
+ out.push({
9294
+ index: i,
9295
+ field: {
9296
+ kind: "objectNumber",
9297
+ fieldObjectNumber: fn.EPDFSig_GetFieldObjNum(model, i)
9298
+ },
9299
+ fieldName: readWide(runtime, (buf, cap) => fn.EPDFSig_GetFieldName(model, i, buf, cap)) ?? "",
9300
+ widget: widgetObjNum > 0 ? {
9301
+ annotObjectNumber: widgetObjNum,
9302
+ pageObjectNumber: fn.EPDFSig_GetWidgetPageObjNum(model, i)
9303
+ } : null,
9304
+ signed,
9305
+ kind: fn.EPDFSig_GetKind(model, i) === KIND_DOC_TIMESTAMP ? "timestamp" : "signature",
9306
+ filter: str(i, STRING_FILTER),
9307
+ subFilter: str(i, STRING_SUBFILTER),
9308
+ byteRange: signed ? readByteRange(runtime, model, i) : null,
9309
+ contentsSize: signed ? Math.max(0, fn.EPDFSig_GetContents(model, i, NULL_PTR, 0)) : 0,
9310
+ coverage: signed ? COVERAGE_BY_CODE[fn.EPDFSig_GetCoverage(model, i)] ?? "malformed" : null,
9311
+ revisionIndex: signed && revisionIndex >= 0 ? revisionIndex : null,
9312
+ signer: {
9313
+ name: str(i, STRING_NAME),
9314
+ reason: str(i, STRING_REASON),
9315
+ location: str(i, STRING_LOCATION),
9316
+ contactInfo: str(i, STRING_CONTACT_INFO),
9317
+ claimedTime: str(i, STRING_M)
9318
+ },
9319
+ docMdp: isPermission(docMdp) ? docMdp : null,
9320
+ catalogCertification: fn.EPDFSig_IsCatalogCertification(model, i),
9321
+ fieldMdp: readLock(runtime, model, i, FIELDS_FIELDMDP),
9322
+ lock: readLock(runtime, model, i, FIELDS_LOCK),
9323
+ seedValue: readSeedValue(runtime, model, i)
9324
+ });
8355
9325
  }
8356
- saveLayerArtifactToFile(path) {
8357
- this.requireLayer();
8358
- const { mem, fn } = this.runtime;
8359
- const statusPtr = mem.alloc(4);
8360
- const writer = this.runtime.fileWrite.toNodeFile(path);
8361
- try {
8362
- mem.poke(statusPtr, "i32", -1);
8363
- const ok = fn.EPDFLayer_SaveLayerArtifact(this.session.requireDocPtr(), writer.ptr, statusPtr);
8364
- const status = Number(mem.peek(statusPtr, "i32"));
8365
- if (!ok || status !== 0) throw layerSaveError(status);
8366
- return { path };
8367
- } finally {
8368
- writer.close();
8369
- mem.free(statusPtr);
9326
+ return out;
9327
+ }
9328
+ /** The DER `/Contents` of signature `index`, copied out; `NotFound` when unsigned or malformed. */
9329
+ function readContentsAt(runtime, model, index) {
9330
+ const { fn, mem } = runtime;
9331
+ const length = fn.EPDFSig_GetContents(model, index, NULL_PTR, 0);
9332
+ if (length <= 0) throw new EngineError(EngineErrorCode.NotFound, "signature has no usable /Contents (malformed encoding)");
9333
+ return withScratch(mem, length, (buf) => {
9334
+ if (fn.EPDFSig_GetContents(model, index, buf, length) !== length) throw new EngineError(EngineErrorCode.Unknown, "failed to read signature contents");
9335
+ const out = new Uint8Array(length);
9336
+ out.set(mem.readBytes(buf, length));
9337
+ return out;
9338
+ });
9339
+ }
9340
+ /** The chained revisions of a document, oldest first; empty when the chain is not valid. */
9341
+ function readRevisions(runtime, docPtr, signatures) {
9342
+ const { fn, mem } = runtime;
9343
+ const count = fn.EPDFDoc_GetRevisionCount(docPtr);
9344
+ const revisions = [];
9345
+ if (count <= 0) return revisions;
9346
+ withScratchN(mem, [8, 8], ([endPtr, xrefPtr]) => {
9347
+ for (let i = 0; i < count; i++) {
9348
+ pokeU64(mem, endPtr, 0);
9349
+ pokeU64(mem, xrefPtr, 0);
9350
+ if (!fn.EPDFDoc_GetRevision(docPtr, i, endPtr, xrefPtr)) throw new EngineError(EngineErrorCode.Unknown, `failed to read revision ${i}`);
9351
+ const sealedBy = signatures.find((s) => s.revisionIndex === i);
9352
+ revisions.push({
9353
+ index: i,
9354
+ end: peekU64(mem, endPtr),
9355
+ xrefOffset: peekU64(mem, xrefPtr),
9356
+ signatureIndex: sealedBy ? sealedBy.index : null
9357
+ });
8370
9358
  }
9359
+ });
9360
+ return revisions;
9361
+ }
9362
+ /** Whether any signature field of the document carries a signed value (fields alone do not count). */
9363
+ function hasSignedSignature(runtime, docPtr) {
9364
+ const { fn } = runtime;
9365
+ return withSignatureModel(runtime, docPtr, (model) => {
9366
+ const count = fn.EPDFSig_Count(model);
9367
+ for (let i = 0; i < count; i++) if (fn.EPDFSig_IsSigned(model, i)) return true;
9368
+ return false;
9369
+ });
9370
+ }
9371
+ /** Run `body` with a fresh signature model of `docPtr`, closing it afterwards. */
9372
+ function withSignatureModel(runtime, docPtr, body) {
9373
+ const model = runtime.fn.EPDFSig_LoadModel(docPtr);
9374
+ if (model === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to build signature model");
9375
+ try {
9376
+ return body(model);
9377
+ } finally {
9378
+ runtime.fn.EPDFSig_CloseModel(model);
8371
9379
  }
8372
- saveStandaloneToBuffer(mode) {
8373
- const { mem, fn } = this.runtime;
8374
- const sizePtr = mem.alloc(4);
9380
+ }
9381
+ /**
9382
+ * The structure of one document (a revision prefix, usually): catalog,
9383
+ * AcroForm and page-tree object numbers, the pages, the terminal fields
9384
+ * with their widgets, and the signatures. What the rule engine needs to
9385
+ * give every changed object a role.
9386
+ */
9387
+ function readStructure(runtime, docPtr) {
9388
+ const { fn, mem } = runtime;
9389
+ const [root, acroForm, pagesRoot] = withScratchN(mem, [
9390
+ 4,
9391
+ 4,
9392
+ 4
9393
+ ], ([r, a, p]) => {
9394
+ mem.poke(r, "i32", 0);
9395
+ mem.poke(a, "i32", 0);
9396
+ mem.poke(p, "i32", 0);
9397
+ fn.EPDFDoc_GetStructureObjectNumbers(docPtr, r, a, p);
9398
+ return [
9399
+ Number(mem.peek(r, "i32")) >>> 0,
9400
+ Number(mem.peek(a, "i32")) >>> 0,
9401
+ Number(mem.peek(p, "i32")) >>> 0
9402
+ ];
9403
+ });
9404
+ const pages = [];
9405
+ const pageCount = fn.FPDF_GetPageCount(docPtr);
9406
+ for (let i = 0; i < pageCount; i++) pages.push(fn.EPDFDoc_GetPageObjectNumberByIndex(docPtr, i));
9407
+ const fields = [];
9408
+ const formModel = fn.EPDFForm_LoadModel(docPtr);
9409
+ if (formModel !== NULL_PTR) try {
9410
+ const snapshot = readFormSnapshot(runtime, formModel, docPtr);
9411
+ for (const f of snapshot.fields) fields.push({
9412
+ objectNumber: f.fieldObjectNumber,
9413
+ name: f.name,
9414
+ family: f.family,
9415
+ widgets: f.widgets.map((w) => w.annotObjectNumber),
9416
+ flags: f.flags.raw
9417
+ });
9418
+ } finally {
9419
+ fn.EPDFForm_CloseModel(formModel);
9420
+ }
9421
+ return {
9422
+ root,
9423
+ acroForm,
9424
+ pagesRoot,
9425
+ pages,
9426
+ fields,
9427
+ signatures: withSignatureModel(runtime, docPtr, (model) => readSignaturesFromModel(runtime, model))
9428
+ };
9429
+ }
9430
+ //#endregion
9431
+ //#region src/features/signature/internal/signatureModelCache.ts
9432
+ /**
9433
+ * Per-session cache of the native `EPDFSig_LoadModel` snapshot, keyed on
9434
+ * the session's mutation sequence exactly like the form model: the model
9435
+ * walks the revision chain, the field tree and every widget, so it is
9436
+ * built once per document state and rebuilt after any mutation.
9437
+ */
9438
+ const cache$1 = /* @__PURE__ */ new WeakMap();
9439
+ function acquireSignatureModel(runtime, session) {
9440
+ const seq = session.mutationSeq();
9441
+ const hit = cache$1.get(session);
9442
+ if (hit && hit.seq === seq) return hit.ptr;
9443
+ if (hit) {
9444
+ runtime.fn.EPDFSig_CloseModel(hit.ptr);
9445
+ cache$1.delete(session);
9446
+ }
9447
+ const ptr = runtime.fn.EPDFSig_LoadModel(session.requireDocPtr());
9448
+ if (ptr === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to build signature model");
9449
+ cache$1.set(session, {
9450
+ seq,
9451
+ ptr
9452
+ });
9453
+ return ptr;
9454
+ }
9455
+ function disposeSignatureModel(runtime, session) {
9456
+ const hit = cache$1.get(session);
9457
+ if (!hit) return;
9458
+ runtime.fn.EPDFSig_CloseModel(hit.ptr);
9459
+ cache$1.delete(session);
9460
+ }
9461
+ //#endregion
9462
+ //#region src/features/signature/SignatureReader.ts
9463
+ const DIGEST_CODE = {
9464
+ sha1: 0,
9465
+ sha256: 1,
9466
+ sha384: 2,
9467
+ sha512: 3
9468
+ };
9469
+ const DIGEST_LENGTH = {
9470
+ sha1: 20,
9471
+ sha256: 32,
9472
+ sha384: 48,
9473
+ sha512: 64
9474
+ };
9475
+ /**
9476
+ * Reads the signature model of a session: revisions, every signature
9477
+ * field with its signed state, and the protection those signatures
9478
+ * impose. Byte facts (revisions, coverage, digests, loaded bytes) come
9479
+ * from the bytes the document was loaded from — for a layer session the
9480
+ * base plus the delta it was opened with — never from unsaved edits.
9481
+ */
9482
+ var SignatureReader = class {
9483
+ constructor(runtime, session) {
9484
+ this.runtime = runtime;
9485
+ this.session = session;
9486
+ }
9487
+ readSnapshot() {
9488
+ const model = acquireSignatureModel(this.runtime, this.session);
9489
+ const chainValid = this.runtime.fn.EPDFSig_IsRevisionChainValid(model);
9490
+ const signatures = readSignaturesFromModel(this.runtime, model);
9491
+ return {
9492
+ chainValid,
9493
+ revisions: chainValid ? readRevisions(this.runtime, this.session.requireDocPtr(), signatures) : [],
9494
+ signatures,
9495
+ protection: deriveProtection(signatures)
9496
+ };
9497
+ }
9498
+ /** The protection alone, for the open probe and the form write guard. */
9499
+ readProtection() {
9500
+ const model = acquireSignatureModel(this.runtime, this.session);
9501
+ return deriveProtection(readSignaturesFromModel(this.runtime, model));
9502
+ }
9503
+ /** The DER `/Contents` of a signed field, padding stripped. */
9504
+ readContents(ref) {
9505
+ const model = acquireSignatureModel(this.runtime, this.session);
9506
+ const index = this.requireSignedIndex(model, ref);
9507
+ return readContentsAt(this.runtime, model, index).buffer;
9508
+ }
9509
+ /** Hash a signed field's `/ByteRange` straight from the loaded bytes. */
9510
+ digest(ref, algorithm) {
9511
+ const model = acquireSignatureModel(this.runtime, this.session);
9512
+ const index = this.requireSignedIndex(model, ref);
9513
+ const range = readByteRange(this.runtime, model, index);
9514
+ if (!range) throw new EngineError(EngineErrorCode.NotFound, "signature has no usable /ByteRange");
9515
+ return this.digestRange(range, algorithm);
9516
+ }
9517
+ /** The exact bytes of one revision. */
9518
+ revisionBytes(revisionIndex) {
9519
+ const snapshot = this.readSnapshot();
9520
+ if (!snapshot.chainValid) throw new EngineError(EngineErrorCode.MalformedPdf, "revision chain is not valid");
9521
+ const revision = snapshot.revisions[revisionIndex];
9522
+ if (!revision) throw new EngineError(EngineErrorCode.NotFound, `no revision ${revisionIndex}`);
9523
+ return this.readLoadedBytes(0, revision.end);
9524
+ }
9525
+ /** Resolve a signature-field ref to its identity in the current model. */
9526
+ resolveField(ref) {
9527
+ const { fn } = this.runtime;
9528
+ const model = acquireSignatureModel(this.runtime, this.session);
9529
+ const index = this.indexOf(model, ref);
9530
+ if (index < 0) throw new EngineError(EngineErrorCode.NotFound, ref.kind === "objectNumber" ? `signature field not found: object ${ref.fieldObjectNumber}` : `signature field not found: "${ref.name}"`);
9531
+ const widgetObjNum = fn.EPDFSig_GetWidgetObjNum(model, index);
9532
+ return {
9533
+ index,
9534
+ fieldObjectNumber: fn.EPDFSig_GetFieldObjNum(model, index),
9535
+ signed: fn.EPDFSig_IsSigned(model, index),
9536
+ widget: widgetObjNum > 0 ? {
9537
+ annotObjectNumber: widgetObjNum,
9538
+ pageObjectNumber: fn.EPDFSig_GetWidgetPageObjNum(model, index)
9539
+ } : null
9540
+ };
9541
+ }
9542
+ /** One signature by field object number, from the current model. */
9543
+ readSignatureByObjectNumber(fieldObjectNumber) {
9544
+ const found = this.readSnapshot().signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === fieldObjectNumber);
9545
+ if (!found) throw new EngineError(EngineErrorCode.NotFound, `signature field not found: object ${fieldObjectNumber}`);
9546
+ return found;
9547
+ }
9548
+ /** The complete loaded bytes (a plain file, or a layer's base + delta). */
9549
+ loadedBytes() {
9550
+ const size = Number(this.runtime.fn.EPDFDoc_GetLoadedBytesSize(this.session.requireDocPtr()));
9551
+ return this.readLoadedBytes(0, size);
9552
+ }
9553
+ /**
9554
+ * SHA-256 and length of the base the session is on. A layer session
9555
+ * reports its base's hash (supplied by the host or computed once by the
9556
+ * runtime); a plain session hashes its loaded bytes once per load.
9557
+ */
9558
+ version() {
9559
+ const { fn, mem } = this.runtime;
9560
+ const docPtr = this.session.requireDocPtr();
9561
+ const byteLength = Number(fn.EPDFDoc_GetBaseBytesSize(docPtr));
9562
+ if (byteLength <= 0) throw new EngineError(EngineErrorCode.DocNotOpen, "document has no loaded bytes");
9563
+ if (this.session.kind === "layer") return {
9564
+ sha256: withScratch(mem, 32, (out) => {
9565
+ if (!fn.EPDFLayer_GetBaseSha256(docPtr, out)) throw new EngineError(EngineErrorCode.Unknown, "failed to read the base hash");
9566
+ return toHex(mem.readBytes(out, 32));
9567
+ }),
9568
+ byteLength
9569
+ };
9570
+ const cached = this.session.cachedPlainSha256();
9571
+ if (cached) return {
9572
+ sha256: cached,
9573
+ byteLength
9574
+ };
9575
+ const sha256 = toHex(new Uint8Array(this.digestRange([
9576
+ 0,
9577
+ byteLength,
9578
+ byteLength,
9579
+ 0
9580
+ ], "sha256")));
9581
+ this.session.rememberPlainSha256(sha256);
9582
+ return {
9583
+ sha256,
9584
+ byteLength
9585
+ };
9586
+ }
9587
+ digestRange(range, algorithm) {
9588
+ const { fn, mem } = this.runtime;
9589
+ const docPtr = this.session.requireDocPtr();
9590
+ const outLength = DIGEST_LENGTH[algorithm];
9591
+ return withScratchN(mem, [
9592
+ 32,
9593
+ outLength,
9594
+ 8
9595
+ ], ([rangePtr, outPtr, lenPtr]) => {
9596
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, range[k], k * 8);
9597
+ pokeU64(mem, lenPtr, outLength);
9598
+ if (!fn.EPDFSig_DigestByteRange(docPtr, rangePtr, DIGEST_CODE[algorithm], outPtr, lenPtr)) throw new EngineError(EngineErrorCode.InvalidArg, "byte range is not within the loaded bytes");
9599
+ const written = Number(mem.peek(lenPtr, "i32"));
9600
+ return copyOut$1(mem.readBytes(outPtr, written));
9601
+ });
9602
+ }
9603
+ /** `[offset, offset + length)` of the loaded bytes (base + loaded delta for a layer), copied out. */
9604
+ readLoadedBytes(offset, length) {
9605
+ const { fn, mem } = this.runtime;
9606
+ const docPtr = this.session.requireDocPtr();
9607
+ if (length === 0) return /* @__PURE__ */ new ArrayBuffer(0);
9608
+ return withScratch(mem, length, (ptr) => {
9609
+ if (fn.EPDFDoc_ReadLoadedBytes(docPtr, BigInt(offset), ptr, length) !== length) throw new EngineError(EngineErrorCode.Unknown, "failed to read the loaded bytes");
9610
+ return copyOut$1(mem.readBytes(ptr, length));
9611
+ });
9612
+ }
9613
+ indexOf(model, ref) {
9614
+ const { fn } = this.runtime;
9615
+ if (ref.kind === "objectNumber") return fn.EPDFSig_GetIndexByFieldObjNum(model, ref.fieldObjectNumber);
9616
+ const count = fn.EPDFSig_Count(model);
9617
+ for (let i = 0; i < count; i++) if (this.readWide((buf, cap) => fn.EPDFSig_GetFieldName(model, i, buf, cap)) === ref.name) return i;
9618
+ return -1;
9619
+ }
9620
+ requireSignedIndex(model, ref) {
9621
+ const { fn } = this.runtime;
9622
+ const index = this.indexOf(model, ref);
9623
+ if (index < 0) throw new EngineError(EngineErrorCode.NotFound, ref.kind === "objectNumber" ? `signature field not found: object ${ref.fieldObjectNumber}` : `signature field not found: "${ref.name}"`);
9624
+ if (!fn.EPDFSig_IsSigned(model, index)) throw new EngineError(EngineErrorCode.NotFound, "signature field is not signed");
9625
+ return index;
9626
+ }
9627
+ readString(model, index, key) {
9628
+ return this.readWide((buf, cap) => this.runtime.fn.EPDFSig_GetString(model, index, key, buf, cap));
9629
+ }
9630
+ readWide(call) {
9631
+ return readUtf16String(this.runtime.mem, call, "");
9632
+ }
9633
+ };
9634
+ function copyOut$1(view) {
9635
+ const buffer = new ArrayBuffer(view.byteLength);
9636
+ new Uint8Array(buffer).set(view);
9637
+ return buffer;
9638
+ }
9639
+ function toHex(bytes) {
9640
+ let out = "";
9641
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
9642
+ return out;
9643
+ }
9644
+ //#endregion
9645
+ //#region src/features/save/DocumentSaver.ts
9646
+ /** The fork's `EPDFSaveStatus`. */
9647
+ const EPDF_SAVE_WRITTEN = 1;
9648
+ const EPDF_SAVE_UNCHANGED_SINCE_LOAD = 2;
9649
+ /**
9650
+ * Owns every "serialize the open document" path for a `DocumentSession`.
9651
+ * Two distinct outputs:
9652
+ *
9653
+ * - Layer artifacts (`saveLayerArtifact*`) are the storage-optimized
9654
+ * `.layer` delta used by server-side persistence. Only valid for a
9655
+ * `layer` session.
9656
+ * - Standalone saves (`saveStandalone*`) export a self-contained PDF
9657
+ * view (base + applied delta) via `FPDF_SaveAsCopy` /
9658
+ * `EPDF_SaveDocumentToOwnedBuffer`, valid for any session kind.
9659
+ *
9660
+ * Lives in `engine-services` so the browser Worker and the Node
9661
+ * `worker_thread` server share the exact same save code path.
9662
+ */
9663
+ var DocumentSaver = class {
9664
+ constructor(runtime, session) {
9665
+ this.runtime = runtime;
9666
+ this.session = session;
9667
+ }
9668
+ /**
9669
+ * The bytes a save would produce, and whether they are the loaded bytes.
9670
+ * Three answers, cheapest first: nothing was mutated since load (the
9671
+ * session's counter); something was mutated but the document is still
9672
+ * the one it was opened with — the fork's save pass found no reachable
9673
+ * object that differs from its loaded version, an annotation added and
9674
+ * removed again being the common case; or a real delta. The one pass
9675
+ * answers the second and produces the third, comparing only the objects
9676
+ * that were touched. A signed file must come back exactly as sealed, so
9677
+ * the loaded bytes are returned verbatim in the first two cases.
9678
+ */
9679
+ snapshot() {
9680
+ if (!this.session.hasUnsavedEdits()) return {
9681
+ bytes: this.loadedBytes(),
9682
+ source: "loaded"
9683
+ };
9684
+ const saved = this.saveStandaloneToBufferEx("incremental");
9685
+ if (saved.unchangedSinceLoad) return {
9686
+ bytes: this.loadedBytes(),
9687
+ source: "loaded"
9688
+ };
9689
+ return {
9690
+ bytes: saved.bytes,
9691
+ source: "working-copy"
9692
+ };
9693
+ }
9694
+ /** The complete loaded bytes: a plain file, or a layer's base plus the delta it was opened with. */
9695
+ loadedBytes() {
9696
+ return new SignatureReader(this.runtime, this.session).loadedBytes();
9697
+ }
9698
+ /**
9699
+ * The loaded bytes streamed to `path` in chunks: a plain file, or a
9700
+ * layer's base plus the delta it was opened with, never the base alone.
9701
+ * Native only. Replaces whatever `path` held.
9702
+ */
9703
+ copyLoadedBytesToFile(path) {
9704
+ const reader = new SignatureReader(this.runtime, this.session);
9705
+ const size = Number(this.runtime.fn.EPDFDoc_GetLoadedBytesSize(this.session.requireDocPtr()));
9706
+ const chunk = 4 * 1024 * 1024;
9707
+ this.runtime.fileWrite.removeFile(path);
9708
+ for (let offset = 0; offset < size; offset += chunk) {
9709
+ const length = Math.min(chunk, size - offset);
9710
+ this.runtime.fileWrite.appendBytes(path, new Uint8Array(reader.readLoadedBytes(offset, length)));
9711
+ }
9712
+ if (size === 0) this.runtime.fileWrite.appendBytes(path, /* @__PURE__ */ new Uint8Array(0));
9713
+ }
9714
+ /**
9715
+ * `snapshot()` onto a file: `path` ends up holding the document a save
9716
+ * would write — the loaded bytes when nothing changed, else the standalone
9717
+ * save — without the document ever being an in-memory buffer. Native only.
9718
+ */
9719
+ snapshotToFile(path) {
9720
+ if (!this.session.hasUnsavedEdits()) {
9721
+ this.copyLoadedBytesToFile(path);
9722
+ return {
9723
+ path,
9724
+ source: "loaded"
9725
+ };
9726
+ }
9727
+ if (this.saveStandaloneToFileEx(path, "incremental").unchangedSinceLoad) {
9728
+ this.copyLoadedBytesToFile(path);
9729
+ return {
9730
+ path,
9731
+ source: "loaded"
9732
+ };
9733
+ }
9734
+ return {
9735
+ path,
9736
+ source: "working-copy"
9737
+ };
9738
+ }
9739
+ /** Whether this runtime and session can put scratch output beside the base file. */
9740
+ canWriteScratchFiles() {
9741
+ return this.runtime.kind === "native" && this.session.source.base?.kind === "file";
9742
+ }
9743
+ saveLayerArtifact() {
9744
+ const saved = this.saveLayerArtifactImpl(false);
9745
+ if (saved.size <= 0) throw layerSaveError(-1);
9746
+ return {
9747
+ bytes: saved.bytes,
9748
+ size: saved.size
9749
+ };
9750
+ }
9751
+ /**
9752
+ * `saveLayerArtifact` that says whether anything reachable changed since
9753
+ * the layer was opened. When nothing did, `bytes` is empty and the
9754
+ * artifact the session was opened with stands. The artifact written
9755
+ * otherwise is cumulative against the base (empty delta when the layer
9756
+ * came to equal its base).
9757
+ */
9758
+ saveLayerArtifactEx() {
9759
+ return this.saveLayerArtifactImpl(true);
9760
+ }
9761
+ /**
9762
+ * `saveLayerArtifactEx` onto a file: nothing reaches `path` when nothing
9763
+ * changed since load. The delta inside is streamed, never held whole.
9764
+ * Native only.
9765
+ */
9766
+ saveLayerArtifactToFileEx(path) {
9767
+ this.requireLayer();
9768
+ return this.saveLayerToFileImpl(path, "artifact");
9769
+ }
9770
+ /** `saveLayerDeltaEx` onto a file: the cumulative delta, or nothing when nothing changed since load. Native only. */
9771
+ saveLayerDeltaToFileEx(path) {
9772
+ this.requireLayer();
9773
+ return this.saveLayerToFileImpl(path, "delta");
9774
+ }
9775
+ saveLayerToFileImpl(path, what) {
9776
+ const { mem, fn } = this.runtime;
9777
+ const statusPtr = mem.alloc(4);
9778
+ const changedPtr = mem.alloc(4);
9779
+ this.runtime.fileWrite.removeFile(path);
9780
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9781
+ try {
9782
+ mem.poke(statusPtr, "i32", -1);
9783
+ mem.poke(changedPtr, "i32", 1);
9784
+ const ok = what === "artifact" ? fn.EPDFLayer_SaveLayerArtifactEx(this.session.requireDocPtr(), writer.ptr, statusPtr, changedPtr) : fn.EPDFLayer_SaveDeltaEx(this.session.requireDocPtr(), writer.ptr, statusPtr, changedPtr);
9785
+ const status = Number(mem.peek(statusPtr, "i32"));
9786
+ if (!ok || status !== 0) throw layerSaveError(status);
9787
+ return {
9788
+ path,
9789
+ changedSinceLoad: Number(mem.peek(changedPtr, "i32")) !== 0
9790
+ };
9791
+ } finally {
9792
+ writer.close();
9793
+ mem.free(changedPtr);
9794
+ mem.free(statusPtr);
9795
+ }
9796
+ }
9797
+ saveLayerArtifactImpl(reporting) {
9798
+ this.requireLayer();
9799
+ const { mem, fn } = this.runtime;
9800
+ const sizePtr = mem.alloc(8);
9801
+ const statusPtr = mem.alloc(4);
9802
+ const changedPtr = mem.alloc(4);
9803
+ let artifactPtr = null;
9804
+ try {
9805
+ mem.poke(sizePtr, "i32", 0);
9806
+ mem.poke(statusPtr, "i32", -1);
9807
+ mem.poke(changedPtr, "i32", 1);
9808
+ artifactPtr = reporting ? fn.EPDFLayer_SaveLayerArtifactToOwnedBufferEx(this.session.requireDocPtr(), sizePtr, statusPtr, changedPtr) : fn.EPDFLayer_SaveLayerArtifactToOwnedBuffer(this.session.requireDocPtr(), sizePtr, statusPtr);
9809
+ const status = Number(mem.peek(statusPtr, "i32"));
9810
+ const size = Number(mem.peek(sizePtr, "i32"));
9811
+ const changedSinceLoad = reporting ? Number(mem.peek(changedPtr, "i32")) !== 0 : true;
9812
+ if (status !== 0) throw layerSaveError(status);
9813
+ if (reporting && !changedSinceLoad) return {
9814
+ bytes: /* @__PURE__ */ new ArrayBuffer(0),
9815
+ size: 0,
9816
+ changedSinceLoad: false
9817
+ };
9818
+ if (!artifactPtr || size <= 0) throw layerSaveError(status);
9819
+ const bytes = mem.readBytes(artifactPtr, size);
9820
+ const buffer = new ArrayBuffer(bytes.byteLength);
9821
+ new Uint8Array(buffer).set(bytes);
9822
+ return {
9823
+ bytes: buffer,
9824
+ size,
9825
+ changedSinceLoad
9826
+ };
9827
+ } finally {
9828
+ if (artifactPtr) fn.EPDF_FreeBuffer(artifactPtr);
9829
+ mem.free(changedPtr);
9830
+ mem.free(statusPtr);
9831
+ mem.free(sizePtr);
9832
+ }
9833
+ }
9834
+ /**
9835
+ * The raw cumulative delta a layer save writes (every promoted object,
9836
+ * offsets notional from the base's append offset), without the artifact
9837
+ * header: what `EPDFDoc_OpenBaseOverlay` composes with the base. Empty
9838
+ * when nothing was promoted.
9839
+ */
9840
+ saveLayerDelta() {
9841
+ const saved = this.saveLayerDeltaImpl(false);
9842
+ return {
9843
+ bytes: saved.bytes,
9844
+ size: saved.size
9845
+ };
9846
+ }
9847
+ /**
9848
+ * `saveLayerDelta` that says whether anything reachable changed since the
9849
+ * layer was opened; empty bytes when nothing did (keep what you have), or
9850
+ * when the layer came to equal its base (then `changedSinceLoad` is true
9851
+ * and the document IS the base).
9852
+ */
9853
+ saveLayerDeltaEx() {
9854
+ return this.saveLayerDeltaImpl(true);
9855
+ }
9856
+ saveLayerDeltaImpl(reporting) {
9857
+ this.requireLayer();
9858
+ const { mem, fn } = this.runtime;
9859
+ const sizePtr = mem.alloc(8);
9860
+ const statusPtr = mem.alloc(4);
9861
+ const changedPtr = mem.alloc(4);
9862
+ let deltaPtr = null;
9863
+ try {
9864
+ mem.poke(sizePtr, "i32", 0);
9865
+ mem.poke(statusPtr, "i32", -1);
9866
+ mem.poke(changedPtr, "i32", 1);
9867
+ deltaPtr = reporting ? fn.EPDFLayer_SaveDeltaToOwnedBufferEx(this.session.requireDocPtr(), sizePtr, statusPtr, changedPtr) : fn.EPDFLayer_SaveDeltaToOwnedBuffer(this.session.requireDocPtr(), sizePtr, statusPtr);
9868
+ const status = Number(mem.peek(statusPtr, "i32"));
9869
+ const size = Number(mem.peek(sizePtr, "i32"));
9870
+ const changedSinceLoad = reporting ? Number(mem.peek(changedPtr, "i32")) !== 0 : true;
9871
+ if (status !== 0) throw layerSaveError(status);
9872
+ if (!deltaPtr || size <= 0) return {
9873
+ bytes: /* @__PURE__ */ new ArrayBuffer(0),
9874
+ size: 0,
9875
+ changedSinceLoad
9876
+ };
9877
+ const bytes = mem.readBytes(deltaPtr, size);
9878
+ const buffer = new ArrayBuffer(bytes.byteLength);
9879
+ new Uint8Array(buffer).set(bytes);
9880
+ return {
9881
+ bytes: buffer,
9882
+ size,
9883
+ changedSinceLoad
9884
+ };
9885
+ } finally {
9886
+ if (deltaPtr) fn.EPDF_FreeBuffer(deltaPtr);
9887
+ mem.free(changedPtr);
9888
+ mem.free(statusPtr);
9889
+ mem.free(sizePtr);
9890
+ }
9891
+ }
9892
+ saveLayerArtifactToFile(path) {
9893
+ this.requireLayer();
9894
+ const { mem, fn } = this.runtime;
9895
+ const statusPtr = mem.alloc(4);
9896
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9897
+ try {
9898
+ mem.poke(statusPtr, "i32", -1);
9899
+ const ok = fn.EPDFLayer_SaveLayerArtifact(this.session.requireDocPtr(), writer.ptr, statusPtr);
9900
+ const status = Number(mem.peek(statusPtr, "i32"));
9901
+ if (!ok || status !== 0) throw layerSaveError(status);
9902
+ return { path };
9903
+ } finally {
9904
+ writer.close();
9905
+ mem.free(statusPtr);
9906
+ }
9907
+ }
9908
+ saveStandaloneToBuffer(mode) {
9909
+ const { mem, fn } = this.runtime;
9910
+ const sizePtr = mem.alloc(8);
8375
9911
  let pdfPtr = null;
8376
9912
  try {
8377
9913
  mem.poke(sizePtr, "i32", 0);
8378
9914
  pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(this.session.requireDocPtr(), pdfSaveModeFlags(mode), sizePtr);
8379
9915
  const size = Number(mem.peek(sizePtr, "i32"));
8380
9916
  if (!pdfPtr || size <= 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
8381
- const bytes = mem.readBytes(pdfPtr, size);
8382
- const buffer = new ArrayBuffer(bytes.byteLength);
8383
- new Uint8Array(buffer).set(bytes);
8384
9917
  return {
8385
- bytes: buffer,
8386
- size
9918
+ bytes: readSavedBuffer(mem, pdfPtr, size),
9919
+ size
9920
+ };
9921
+ } finally {
9922
+ if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
9923
+ mem.free(sizePtr);
9924
+ }
9925
+ }
9926
+ /**
9927
+ * `saveStandaloneToBuffer` that says whether the save wrote anything. An
9928
+ * incremental save of a layer never rewrites an object equal to its base
9929
+ * twin, and when no reachable object differs from the document the layer
9930
+ * was opened with it writes nothing at all: `unchangedSinceLoad`, empty
9931
+ * bytes — the loaded bytes are the document.
9932
+ */
9933
+ saveStandaloneToBufferEx(mode) {
9934
+ const { mem, fn } = this.runtime;
9935
+ const sizePtr = mem.alloc(8);
9936
+ const statusPtr = mem.alloc(4);
9937
+ let pdfPtr = null;
9938
+ try {
9939
+ mem.poke(sizePtr, "i32", 0);
9940
+ mem.poke(statusPtr, "i32", 0);
9941
+ pdfPtr = fn.EPDF_SaveDocumentToOwnedBufferEx(this.session.requireDocPtr(), pdfSaveModeFlags(mode), 0, sizePtr, statusPtr);
9942
+ const status = Number(mem.peek(statusPtr, "i32"));
9943
+ if (status === EPDF_SAVE_UNCHANGED_SINCE_LOAD) return {
9944
+ bytes: /* @__PURE__ */ new ArrayBuffer(0),
9945
+ size: 0,
9946
+ unchangedSinceLoad: true
9947
+ };
9948
+ const size = Number(mem.peek(sizePtr, "i32"));
9949
+ if (!pdfPtr || size <= 0 || status !== EPDF_SAVE_WRITTEN) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9950
+ return {
9951
+ bytes: readSavedBuffer(mem, pdfPtr, size),
9952
+ size,
9953
+ unchangedSinceLoad: false
9954
+ };
9955
+ } finally {
9956
+ if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
9957
+ mem.free(statusPtr);
9958
+ mem.free(sizePtr);
9959
+ }
9960
+ }
9961
+ saveStandaloneToFile(path, mode) {
9962
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9963
+ try {
9964
+ if (!this.runtime.fn.FPDF_SaveAsCopy(this.session.requireDocPtr(), writer.ptr, pdfSaveModeFlags(mode))) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9965
+ return { path };
9966
+ } finally {
9967
+ writer.close();
9968
+ }
9969
+ }
9970
+ /**
9971
+ * `saveStandaloneToFile` with the report of `saveStandaloneToBufferEx`.
9972
+ * On `unchangedSinceLoad` no byte reached the file: the caller streams
9973
+ * the loaded bytes instead.
9974
+ */
9975
+ saveStandaloneToFileEx(path, mode) {
9976
+ const { mem, fn } = this.runtime;
9977
+ const writer = this.runtime.fileWrite.toNodeFile(path);
9978
+ const statusPtr = mem.alloc(4);
9979
+ try {
9980
+ mem.poke(statusPtr, "i32", 0);
9981
+ const ok = fn.EPDF_SaveAsCopyEx(this.session.requireDocPtr(), writer.ptr, pdfSaveModeFlags(mode), statusPtr);
9982
+ const status = Number(mem.peek(statusPtr, "i32"));
9983
+ if (!ok || status === 0) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
9984
+ return {
9985
+ path,
9986
+ unchangedSinceLoad: status === EPDF_SAVE_UNCHANGED_SINCE_LOAD
9987
+ };
9988
+ } finally {
9989
+ mem.free(statusPtr);
9990
+ writer.close();
9991
+ }
9992
+ }
9993
+ requireLayer() {
9994
+ if (this.session.kind !== "layer") throw new EngineError(EngineErrorCode.InvalidArg, "document session is not a layer");
9995
+ }
9996
+ };
9997
+ //#endregion
9998
+ //#region src/features/signature/internal/candidateStore.ts
9999
+ function copyOut(view) {
10000
+ const out = new Uint8Array(view.byteLength);
10001
+ out.set(view);
10002
+ return out;
10003
+ }
10004
+ /** Marshal `EPDFSig_SealSpan` over a span held in the runtime heap. */
10005
+ function sealSpanInHeap(runtime, spanPtr, spanLength, objectOffset, fileLength) {
10006
+ const { fn, mem } = runtime;
10007
+ return withScratchN(mem, [
10008
+ 32,
10009
+ 8,
10010
+ 8
10011
+ ], ([rangePtr, coPtr, chPtr]) => {
10012
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, 0, k * 8);
10013
+ pokeU64(mem, coPtr, 0);
10014
+ pokeU64(mem, chPtr, 0);
10015
+ if (!fn.EPDFSig_SealSpan(spanPtr, BigInt(spanLength), BigInt(objectOffset), BigInt(fileLength), rangePtr, coPtr, chPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to seal the signing candidate");
10016
+ return {
10017
+ byteRange: [
10018
+ peekU64(mem, rangePtr, 0),
10019
+ peekU64(mem, rangePtr, 8),
10020
+ peekU64(mem, rangePtr, 16),
10021
+ peekU64(mem, rangePtr, 24)
10022
+ ],
10023
+ contentsOffset: peekU64(mem, coPtr),
10024
+ contentsHexLength: peekU64(mem, chPtr)
10025
+ };
10026
+ });
10027
+ }
10028
+ /** Marshal `EPDFSig_WriteContents` over a buffer in the heap whose first byte is file offset `bufferOffset`. */
10029
+ function writeContentsInHeap(runtime, bufPtr, bufferLength, bufferOffset, sealed, cms) {
10030
+ const { fn, mem } = runtime;
10031
+ const cmsPtr = mem.alloc(cms.byteLength);
10032
+ try {
10033
+ mem.writeBytes(cmsPtr, cms);
10034
+ if (!fn.EPDFSig_WriteContents(bufPtr, BigInt(bufferLength), BigInt(sealed.contentsOffset - bufferOffset), BigInt(sealed.contentsHexLength), cmsPtr, cms.byteLength)) throw new EngineError(EngineErrorCode.SignatureRefused, "the CMS is not one DER object that fits the reserved /Contents");
10035
+ } finally {
10036
+ mem.free(cmsPtr);
10037
+ }
10038
+ }
10039
+ /**
10040
+ * Hex-encode `cms` into the /Contents hole of a candidate FILE, in place.
10041
+ * Only the hole (`<`, the hex digits, `>`) is read and written back; the
10042
+ * delimiters are checked so a wrong offset patches nothing.
10043
+ */
10044
+ function writeContentsIntoFile(runtime, path, hole, cms) {
10045
+ const { mem } = runtime;
10046
+ const span = runtime.fileAccess.readRange(path, hole.holeOffset, hole.hexLength + 2);
10047
+ if (span.byteLength !== hole.hexLength + 2 || span[0] !== 60 || span[span.byteLength - 1] !== 62) throw new EngineError(EngineErrorCode.InvalidArg, "the byteRange gap is not a <…> /Contents hole of the candidate");
10048
+ const spanPtr = mem.alloc(span.byteLength);
10049
+ try {
10050
+ mem.writeBytes(spanPtr, span);
10051
+ writeContentsInHeap(runtime, spanPtr, span.byteLength, hole.holeOffset, {
10052
+ byteRange: [
10053
+ 0,
10054
+ 0,
10055
+ 0,
10056
+ 0
10057
+ ],
10058
+ contentsOffset: hole.holeOffset + 1,
10059
+ contentsHexLength: hole.hexLength,
10060
+ digest: /* @__PURE__ */ new Uint8Array()
10061
+ }, cms);
10062
+ runtime.fileWrite.writeRange(path, hole.holeOffset, mem.readBytes(spanPtr, span.byteLength));
10063
+ } finally {
10064
+ mem.free(spanPtr);
10065
+ }
10066
+ }
10067
+ var MemoryCandidateStore = class {
10068
+ constructor(runtime, baseDocuments) {
10069
+ this.runtime = runtime;
10070
+ this.baseDocuments = baseDocuments;
10071
+ }
10072
+ save(candidateDocPtr, sigObjNum) {
10073
+ const { mem, fn } = this.runtime;
10074
+ return withScratchN(mem, [
10075
+ 8,
10076
+ 8,
10077
+ 8
10078
+ ], ([sizePtr, offPtr, lenPtr]) => {
10079
+ pokeU64(mem, sizePtr, 0);
10080
+ pokeU64(mem, offPtr, 0);
10081
+ pokeU64(mem, lenPtr, 0);
10082
+ const bufPtr = fn.EPDFSig_SaveCandidateToOwnedBuffer(candidateDocPtr, sigObjNum, sizePtr, offPtr, lenPtr);
10083
+ if (!bufPtr) throw new EngineError(EngineErrorCode.Unknown, "failed to save the signing candidate");
10084
+ try {
10085
+ const size = peekU64(mem, sizePtr);
10086
+ return {
10087
+ kind: "memory",
10088
+ bytes: copyOut(mem.readBytes(bufPtr, size)),
10089
+ size,
10090
+ objectOffset: peekU64(mem, offPtr),
10091
+ objectLength: peekU64(mem, lenPtr)
10092
+ };
10093
+ } finally {
10094
+ fn.EPDF_FreeBuffer(bufPtr);
10095
+ }
10096
+ });
10097
+ }
10098
+ seal(saved, algorithm) {
10099
+ if (saved.kind !== "memory") throw new EngineError(EngineErrorCode.InvalidArg, "not a memory candidate");
10100
+ const { mem, fn } = this.runtime;
10101
+ const bufPtr = mem.alloc(saved.bytes.byteLength);
10102
+ try {
10103
+ mem.writeBytes(bufPtr, saved.bytes);
10104
+ return withScratchN(mem, [
10105
+ 32,
10106
+ 8,
10107
+ 8,
10108
+ 64,
10109
+ 8
10110
+ ], ([rangePtr, coPtr, chPtr, digestPtr, lenPtr]) => {
10111
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, 0, k * 8);
10112
+ pokeU64(mem, coPtr, 0);
10113
+ pokeU64(mem, chPtr, 0);
10114
+ pokeU64(mem, lenPtr, 64);
10115
+ if (!fn.EPDFSig_Seal(bufPtr, BigInt(saved.bytes.byteLength), BigInt(saved.objectOffset), BigInt(saved.objectLength), DIGEST_CODE[algorithm], rangePtr, coPtr, chPtr, digestPtr, lenPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to seal the signing candidate");
10116
+ saved.bytes = copyOut(mem.readBytes(bufPtr, saved.bytes.byteLength));
10117
+ const digestLength = Number(mem.peek(lenPtr, "i32"));
10118
+ return {
10119
+ byteRange: [
10120
+ peekU64(mem, rangePtr, 0),
10121
+ peekU64(mem, rangePtr, 8),
10122
+ peekU64(mem, rangePtr, 16),
10123
+ peekU64(mem, rangePtr, 24)
10124
+ ],
10125
+ contentsOffset: peekU64(mem, coPtr),
10126
+ contentsHexLength: peekU64(mem, chPtr),
10127
+ digest: copyOut(mem.readBytes(digestPtr, digestLength))
10128
+ };
10129
+ });
10130
+ } finally {
10131
+ mem.free(bufPtr);
10132
+ }
10133
+ }
10134
+ writeContents(saved, sealed, cms) {
10135
+ if (saved.kind !== "memory") throw new EngineError(EngineErrorCode.InvalidArg, "not a memory candidate");
10136
+ const { mem } = this.runtime;
10137
+ const bufPtr = mem.alloc(saved.bytes.byteLength);
10138
+ try {
10139
+ mem.writeBytes(bufPtr, saved.bytes);
10140
+ writeContentsInHeap(this.runtime, bufPtr, saved.bytes.byteLength, 0, sealed, cms);
10141
+ saved.bytes = copyOut(mem.readBytes(bufPtr, saved.bytes.byteLength));
10142
+ } finally {
10143
+ mem.free(bufPtr);
10144
+ }
10145
+ }
10146
+ openSealed(saved, password) {
10147
+ if (saved.kind !== "memory") throw new EngineError(EngineErrorCode.InvalidArg, "not a memory candidate");
10148
+ const base = this.baseDocuments.acquireMemoryBase({
10149
+ key: `signed:${generateUuid()}`,
10150
+ bytes: saved.bytes,
10151
+ password
10152
+ });
10153
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
10154
+ }
10155
+ discard() {}
10156
+ };
10157
+ /**
10158
+ * A candidate written next to the session's base file, through the
10159
+ * runtime's local-file surface: the base streams through the writer, the
10160
+ * seal patches the object span in place, the digest streams the file, and
10161
+ * the sealed file becomes a file base. Only the object span and the CMS
10162
+ * are ever held in memory. Node runtimes only.
10163
+ */
10164
+ var FileCandidateStore = class {
10165
+ constructor(runtime, baseDocuments, basePath, candidatePath = defaultCandidatePath) {
10166
+ this.runtime = runtime;
10167
+ this.baseDocuments = baseDocuments;
10168
+ this.basePath = basePath;
10169
+ this.candidatePath = candidatePath;
10170
+ }
10171
+ save(candidateDocPtr, sigObjNum, signingId) {
10172
+ const { mem, fn } = this.runtime;
10173
+ const path = this.candidatePath(this.basePath, signingId);
10174
+ this.runtime.fileWrite.removeFile(path);
10175
+ const writer = this.runtime.fileWrite.toNodeFile(path);
10176
+ try {
10177
+ return withScratchN(mem, [
10178
+ 8,
10179
+ 8,
10180
+ 8
10181
+ ], ([sizePtr, offPtr, lenPtr]) => {
10182
+ pokeU64(mem, sizePtr, 0);
10183
+ pokeU64(mem, offPtr, 0);
10184
+ pokeU64(mem, lenPtr, 0);
10185
+ if (!fn.EPDFSig_SaveCandidate(candidateDocPtr, sigObjNum, writer.ptr, sizePtr, offPtr, lenPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to save the signing candidate");
10186
+ return {
10187
+ kind: "file",
10188
+ path,
10189
+ size: peekU64(mem, sizePtr),
10190
+ objectOffset: peekU64(mem, offPtr),
10191
+ objectLength: peekU64(mem, lenPtr)
10192
+ };
10193
+ });
10194
+ } catch (error) {
10195
+ writer.close();
10196
+ this.runtime.fileWrite.removeFile(path);
10197
+ throw error;
10198
+ } finally {
10199
+ writer.close();
10200
+ }
10201
+ }
10202
+ seal(saved, algorithm) {
10203
+ if (saved.kind !== "file") throw new EngineError(EngineErrorCode.InvalidArg, "not a file candidate");
10204
+ const { mem, fn } = this.runtime;
10205
+ const span = this.runtime.fileAccess.readRange(saved.path, saved.objectOffset, saved.objectLength);
10206
+ if (span.byteLength !== saved.objectLength) throw new EngineError(EngineErrorCode.Unknown, "the signing candidate file is shorter than its object span");
10207
+ const spanPtr = mem.alloc(span.byteLength);
10208
+ let plan;
10209
+ try {
10210
+ mem.writeBytes(spanPtr, span);
10211
+ plan = sealSpanInHeap(this.runtime, spanPtr, span.byteLength, saved.objectOffset, saved.size);
10212
+ this.runtime.fileWrite.writeRange(saved.path, saved.objectOffset, mem.readBytes(spanPtr, span.byteLength));
10213
+ } finally {
10214
+ mem.free(spanPtr);
10215
+ }
10216
+ const access = this.runtime.fileAccess.fromNodeFile(saved.path);
10217
+ try {
10218
+ const digest = withScratchN(mem, [
10219
+ 32,
10220
+ 64,
10221
+ 8
10222
+ ], ([rangePtr, digestPtr, lenPtr]) => {
10223
+ for (let k = 0; k < 4; k++) pokeU64(mem, rangePtr, plan.byteRange[k], k * 8);
10224
+ pokeU64(mem, lenPtr, 64);
10225
+ if (!fn.EPDFSig_DigestFileRange(access.ptr, rangePtr, DIGEST_CODE[algorithm], digestPtr, lenPtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to digest the signing candidate file");
10226
+ return copyOut(mem.readBytes(digestPtr, Number(mem.peek(lenPtr, "i32"))));
10227
+ });
10228
+ return {
10229
+ ...plan,
10230
+ digest
10231
+ };
10232
+ } finally {
10233
+ access.close();
10234
+ }
10235
+ }
10236
+ writeContents(saved, sealed, cms) {
10237
+ if (saved.kind !== "file") throw new EngineError(EngineErrorCode.InvalidArg, "not a file candidate");
10238
+ const { mem } = this.runtime;
10239
+ const span = this.runtime.fileAccess.readRange(saved.path, saved.objectOffset, saved.objectLength);
10240
+ const spanPtr = mem.alloc(span.byteLength);
10241
+ try {
10242
+ mem.writeBytes(spanPtr, span);
10243
+ writeContentsInHeap(this.runtime, spanPtr, span.byteLength, saved.objectOffset, sealed, cms);
10244
+ this.runtime.fileWrite.writeRange(saved.path, saved.objectOffset, mem.readBytes(spanPtr, span.byteLength));
10245
+ } finally {
10246
+ mem.free(spanPtr);
10247
+ }
10248
+ }
10249
+ openSealed(saved, password) {
10250
+ if (saved.kind !== "file") throw new EngineError(EngineErrorCode.InvalidArg, "not a file candidate");
10251
+ const base = this.baseDocuments.acquireFileBase({
10252
+ key: `signed:${saved.path}`,
10253
+ path: saved.path,
10254
+ password
10255
+ });
10256
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
10257
+ }
10258
+ discard(saved) {
10259
+ if (saved.kind === "file") this.runtime.fileWrite.removeFile(saved.path);
10260
+ }
10261
+ };
10262
+ /** Beside the base file, named by the signing; the host may override this (server storage layout). */
10263
+ function defaultCandidatePath(basePath, signingId) {
10264
+ return `${basePath}.signing-${signingId}.pdf`;
10265
+ }
10266
+ /**
10267
+ * A scratch file beside the base: `<base>.<tag>-<id>.<ext>`. Unique per
10268
+ * id, so a file a document holds open is never rewritten; the caller that
10269
+ * names it registers its removal before anything writes it.
10270
+ */
10271
+ function scratchPath(basePath, tag, id, ext) {
10272
+ return `${basePath}.${tag}-${id}.${ext}`;
10273
+ }
10274
+ //#endregion
10275
+ //#region src/features/signature/internal/appearance.ts
10276
+ /** `EPDF_STAMP_FIT_CONTAIN`: uniform scale, letterboxed and centred in the rect. */
10277
+ const FIT_CONTAIN = 0;
10278
+ /**
10279
+ * Draw one page of a PDF into a widget annotation's normal appearance —
10280
+ * what a signature's mark is, whether the field is being signed
10281
+ * (`SignatureMutator`) or only filled visually (`FormMutator`).
10282
+ *
10283
+ * Two fork calls, the same pair the stamp path makes: `SetAppearanceFromPage`
10284
+ * clones the page into the document as a wrapped Form XObject (page content
10285
+ * and resources in a child form, the outer stream a placement matrix), then
10286
+ * `UpdateAppearanceToRect` writes that placement — the page fitted uniformly
10287
+ * into the widget's rect. Without the second call the page sits in its own
10288
+ * box and the viewer's BBox→/Rect mapping stretches it to the rect's aspect.
10289
+ */
10290
+ function bakeWidgetAppearance(runtime, docPtr, widget, pdf, pageIndex) {
10291
+ const { mem, fn } = runtime;
10292
+ const stack = new CloseStack();
10293
+ try {
10294
+ const pagePtr = fn.EPDFDoc_LoadPageByObjectNumber(docPtr, widget.pageObjectNumber);
10295
+ if (!pagePtr) throw new EngineError(EngineErrorCode.NotFound, "the widget page could not be loaded");
10296
+ stack.push(() => fn.FPDF_ClosePage(pagePtr));
10297
+ const annotPtr = fn.EPDFPage_GetAnnotByObjectNumber(pagePtr, widget.annotObjectNumber);
10298
+ if (!annotPtr) throw new EngineError(EngineErrorCode.NotFound, "the signature widget could not be loaded");
10299
+ stack.push(() => fn.FPDFPage_CloseAnnot(annotPtr));
10300
+ const dataPtr = mem.alloc(pdf.byteLength);
10301
+ stack.push(() => mem.free(dataPtr));
10302
+ mem.writeBytes(dataPtr, pdf);
10303
+ const artworkPtr = fn.FPDF_LoadMemDocument64(dataPtr, pdf.byteLength, "");
10304
+ if (!artworkPtr) throw new EngineError(EngineErrorCode.MalformedPdf, "the appearance PDF could not be opened");
10305
+ stack.push(() => fn.FPDF_CloseDocument(artworkPtr));
10306
+ if (!fn.EPDFAnnot_SetAppearanceFromPage(annotPtr, artworkPtr, pageIndex)) throw new EngineError(EngineErrorCode.InvalidArg, "the appearance page could not be drawn into the widget");
10307
+ if (!fn.EPDFAnnot_UpdateAppearanceToRect(annotPtr, FIT_CONTAIN)) throw new EngineError(EngineErrorCode.Unknown, "the appearance page could not be fitted into the widget");
10308
+ } finally {
10309
+ stack.close();
10310
+ }
10311
+ }
10312
+ //#endregion
10313
+ //#region src/features/signature/internal/sealCheck.ts
10314
+ /**
10315
+ * The one check both signing paths make before sealed bytes become a
10316
+ * version — the in-session complete and the server's session-less
10317
+ * finalize. Reading the result back through the ordinary signature model
10318
+ * proves the bytes, not the writer: the field is signed, its signature
10319
+ * covers the whole LAST revision (the candidate's own, never an earlier
10320
+ * one), its /ByteRange is the one the digest was computed over, and the
10321
+ * /Contents decode to exactly the CMS installed. Anything else is refused
10322
+ * and the bytes are never installed.
10323
+ */
10324
+ function assertSealedSignature(runtime, docPtr, model, expected) {
10325
+ const refuse = (why) => new EngineError(EngineErrorCode.SignatureRefused, `the sealed bytes ${why}`);
10326
+ const signatures = readSignaturesFromModel(runtime, model);
10327
+ const signature = signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === expected.fieldObjectNumber);
10328
+ if (!signature) throw refuse("lost the signature field");
10329
+ if (!signature.signed || signature.coverage !== "whole-revision" || !signature.byteRange) throw refuse("do not carry a whole-revision signature on the field");
10330
+ if (!sameRange(signature.byteRange, expected.byteRange)) throw refuse(`carry /ByteRange [${signature.byteRange.join(" ")}] where [${expected.byteRange.join(" ")}] was sealed`);
10331
+ if (!runtime.fn.EPDFSig_IsRevisionChainValid(model)) throw refuse("have a broken cross-reference chain");
10332
+ const revisions = readRevisions(runtime, docPtr, signatures);
10333
+ if (signature.revisionIndex !== revisions.length - 1) throw refuse("seal an earlier revision than the candidate");
10334
+ if (!bytesEqual(readContentsAt(runtime, model, signature.index), expected.cms)) throw refuse("carry /Contents that are not the CMS installed");
10335
+ return signature;
10336
+ }
10337
+ function sameRange(a, b) {
10338
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
10339
+ }
10340
+ function bytesEqual(a, b) {
10341
+ if (a.byteLength !== b.byteLength) return false;
10342
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
10343
+ return true;
10344
+ }
10345
+ //#endregion
10346
+ //#region src/features/forms/internal/wideStringArray.ts
10347
+ /**
10348
+ * Marshal a `const FPDF_WIDESTRING*` array: encode every string into the
10349
+ * runtime heap, write the pointer table with the runtime's pointer width
10350
+ * (bigint pointers cannot be poked into the wasm 32-bit heap), run |body|,
10351
+ * free everything.
10352
+ */
10353
+ function withWideStringArray(runtime, values, body) {
10354
+ const { mem } = runtime;
10355
+ const stringPtrs = values.map((value) => mem.writeU16String(value));
10356
+ const isWasm = runtime.kind === "wasm";
10357
+ const ptrSize = isWasm ? 4 : 8;
10358
+ try {
10359
+ return withScratch(mem, Math.max(1, stringPtrs.length * ptrSize), (arrayPtr) => {
10360
+ stringPtrs.forEach((ptr, i) => isWasm ? mem.poke(arrayPtr, "i32", Number(ptr), i * 4) : mem.poke(arrayPtr, "i64", ptr, i * 8));
10361
+ return body(arrayPtr, stringPtrs.length);
10362
+ });
10363
+ } finally {
10364
+ for (let i = stringPtrs.length - 1; i >= 0; i--) mem.free(stringPtrs[i]);
10365
+ }
10366
+ }
10367
+ //#endregion
10368
+ //#region src/features/signature/SignatureMutator.ts
10369
+ const SUBFILTER_CODE = {
10370
+ "adbe.pkcs7.detached": 0,
10371
+ "ETSI.CAdES.detached": 1,
10372
+ "ETSI.RFC3161": 2
10373
+ };
10374
+ const FIELD_ACTION_CODE = {
10375
+ all: 1,
10376
+ include: 2,
10377
+ exclude: 3
10378
+ };
10379
+ const DEFAULT_CONTENTS_SIZE = 8192;
10380
+ const MIN_CONTENTS_SIZE = 256;
10381
+ const MAX_CONTENTS_SIZE = 4 * 1024 * 1024;
10382
+ const PREPARE_ILP32 = {
10383
+ bytes: 52,
10384
+ ptrBytes: 4,
10385
+ subfilter: 0,
10386
+ digest: 4,
10387
+ contentsSize: 8,
10388
+ name: 12,
10389
+ reason: 16,
10390
+ location: 20,
10391
+ contactInfo: 24,
10392
+ signingTime: 28,
10393
+ docmdpPermission: 32,
10394
+ fieldmdpAction: 36,
10395
+ fieldmdpFields: 40,
10396
+ fieldmdpFieldCount: 44,
10397
+ lockPermission: 48
10398
+ };
10399
+ const PREPARE_LP64 = {
10400
+ bytes: 80,
10401
+ ptrBytes: 8,
10402
+ subfilter: 0,
10403
+ digest: 4,
10404
+ contentsSize: 8,
10405
+ name: 16,
10406
+ reason: 24,
10407
+ location: 32,
10408
+ contactInfo: 40,
10409
+ signingTime: 48,
10410
+ docmdpPermission: 56,
10411
+ fieldmdpAction: 60,
10412
+ fieldmdpFields: 64,
10413
+ fieldmdpFieldCount: 72,
10414
+ lockPermission: 76
10415
+ };
10416
+ /**
10417
+ * The two-phase signing protocol on a session, local and native alike.
10418
+ *
10419
+ * `prepare` never touches the live document: it builds a CANDIDATE — a
10420
+ * fresh layer over the session's own immutable base (see `openCandidate`)
10421
+ * — writes the signature there, saves it through a `CandidateStore`
10422
+ * (memory locally, a file beside the base on file-backed sessions), seals
10423
+ * it, and parks the saved candidate on the session. `complete` writes the
10424
+ * CMS into it, opens it as a new base with a fresh layer, proves the new
10425
+ * signature seals a whole revision, and installs it as the session's
10426
+ * document. `abort` discards the candidate.
10427
+ */
10428
+ var SignatureMutator = class {
10429
+ constructor(runtime, session, baseDocuments, candidatePath) {
10430
+ this.runtime = runtime;
10431
+ this.session = session;
10432
+ this.baseDocuments = baseDocuments;
10433
+ this.candidatePath = candidatePath;
10434
+ }
10435
+ prepare(input) {
10436
+ if (this.session.pendingSigning) throw new EngineError(EngineErrorCode.SigningPending, `a signing is pending (${this.session.pendingSigning.prepared.signingId}); complete or abort it first`);
10437
+ const reader = new SignatureReader(this.runtime, this.session);
10438
+ const field = reader.resolveField(input.field);
10439
+ if (field.signed) throw new EngineError(EngineErrorCode.SignatureRefused, "the signature field is already signed");
10440
+ if (input.appearance && !field.widget) throw new EngineError(EngineErrorCode.SignatureRefused, "the signature field has no widget to carry an appearance");
10441
+ const algorithm = input.digest ?? "sha256";
10442
+ if (!(algorithm in DIGEST_CODE) || algorithm === "sha1") throw new EngineError(EngineErrorCode.SignatureRefused, `unsupported digest '${algorithm}'`);
10443
+ const subFilter = (input.kind ?? "signature") === "timestamp" ? "ETSI.RFC3161" : input.subFilter ?? "ETSI.CAdES.detached";
10444
+ if (!(subFilter in SUBFILTER_CODE)) throw new EngineError(EngineErrorCode.SignatureRefused, `unsupported subFilter '${subFilter}'`);
10445
+ const contentsSize = input.contentsSize ?? DEFAULT_CONTENTS_SIZE;
10446
+ if (!Number.isInteger(contentsSize) || contentsSize < MIN_CONTENTS_SIZE || contentsSize > MAX_CONTENTS_SIZE) throw new EngineError(EngineErrorCode.SignatureRefused, `contentsSize must be an integer in [${MIN_CONTENTS_SIZE}, ${MAX_CONTENTS_SIZE}]`);
10447
+ const expectedVersion = this.session.versionRef(reader.version().sha256);
10448
+ const signingId = generateUuid();
10449
+ const stack = new CloseStack();
10450
+ try {
10451
+ const candidate = this.openCandidate(signingId, stack);
10452
+ stack.push(() => candidate.close());
10453
+ const attribution = input.attribution ?? input.signer;
10454
+ const valueObjNum = this.callPrepare(candidate.docPtr, field.fieldObjectNumber, {
10455
+ subfilter: SUBFILTER_CODE[subFilter],
10456
+ digest: DIGEST_CODE[algorithm],
10457
+ contentsSize,
10458
+ name: attribution?.name ?? null,
10459
+ reason: attribution?.reason ?? null,
10460
+ location: attribution?.location ?? null,
10461
+ contactInfo: attribution?.contactInfo ?? null,
10462
+ signingTime: input.signingTime ?? null,
10463
+ docmdpPermission: input.certify?.permission ?? 0,
10464
+ fieldmdpAction: input.lock ? FIELD_ACTION_CODE[input.lock.action] : 0,
10465
+ fieldmdpFields: input.lock?.action === "all" ? [] : input.lock?.fields ?? [],
10466
+ lockPermission: input.lock?.permission ?? 0
10467
+ });
10468
+ if (valueObjNum === 0) throw new EngineError(EngineErrorCode.SignatureRefused, "the engine refused to author the signature (the field is signed, read-only or locked, a certification is not allowed here, the seed value requires what is not implemented, or the request is inconsistent)");
10469
+ if (input.appearance && field.widget) bakeWidgetAppearance(this.runtime, candidate.docPtr, field.widget, input.appearance.pdf, input.appearance.pageIndex ?? 0);
10470
+ const store = this.storeFor();
10471
+ const saved = store.save(candidate.docPtr, valueObjNum, signingId);
10472
+ let sealed;
10473
+ try {
10474
+ sealed = store.seal(saved, algorithm);
10475
+ } catch (error) {
10476
+ store.discard(saved);
10477
+ throw error;
10478
+ }
10479
+ const prepared = {
10480
+ signingId,
10481
+ digest: sealed.digest,
10482
+ algorithm,
10483
+ byteRange: sealed.byteRange,
10484
+ contentsSize,
10485
+ subFilter,
10486
+ expectedVersion,
10487
+ expiresAt: null
10488
+ };
10489
+ this.session.pendingSigning = {
10490
+ prepared,
10491
+ fieldObjectNumber: field.fieldObjectNumber,
10492
+ saved,
10493
+ contentsOffset: sealed.contentsOffset,
10494
+ contentsHexLength: sealed.contentsHexLength
10495
+ };
10496
+ return prepared;
10497
+ } finally {
10498
+ stack.close();
10499
+ }
10500
+ }
10501
+ complete(input) {
10502
+ const pending = this.session.pendingSigning;
10503
+ if (!pending || pending.prepared.signingId !== input.signingId) {
10504
+ const last = this.session.lastCompletion;
10505
+ if (last && last.signingId === input.signingId) {
10506
+ if (!bytesEqual(last.cms, input.cms)) throw new EngineError(EngineErrorCode.SignatureRefused, "this signing already completed with a different CMS");
10507
+ return {
10508
+ ...last.result,
10509
+ status: "already-completed"
10510
+ };
10511
+ }
10512
+ throw new EngineError(EngineErrorCode.NotFound, `no pending signing '${input.signingId}'`);
10513
+ }
10514
+ if (!sameVersion(pending.prepared.expectedVersion, input.expectedVersion)) throw new EngineError(EngineErrorCode.SigningVersionMismatch, "expectedVersion is not the version the candidate was prepared on");
10515
+ if (input.cms.byteLength === 0 || input.cms[0] !== 48) throw new EngineError(EngineErrorCode.SignatureRefused, "the CMS is not a DER SEQUENCE");
10516
+ if (input.cms.byteLength > pending.prepared.contentsSize) throw new EngineError(EngineErrorCode.SignatureRefused, `the CMS (${input.cms.byteLength} bytes) does not fit the reserved ${pending.prepared.contentsSize} bytes`);
10517
+ const store = this.storeFor();
10518
+ const sealed = {
10519
+ byteRange: pending.prepared.byteRange,
10520
+ contentsOffset: pending.contentsOffset,
10521
+ contentsHexLength: pending.contentsHexLength,
10522
+ digest: pending.prepared.digest
10523
+ };
10524
+ store.writeContents(pending.saved, sealed, input.cms);
10525
+ const handle = store.openSealed(pending.saved, this.session.password);
10526
+ this.verifySealed(handle, {
10527
+ fieldObjectNumber: pending.fieldObjectNumber,
10528
+ byteRange: pending.prepared.byteRange,
10529
+ cms: input.cms
10530
+ });
10531
+ this.session.install(handle);
10532
+ disposeFormModel(this.runtime, this.session);
10533
+ disposeSignatureModel(this.runtime, this.session);
10534
+ const reader = new SignatureReader(this.runtime, this.session);
10535
+ const snapshot = reader.readSnapshot();
10536
+ const signature = snapshot.signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === pending.fieldObjectNumber);
10537
+ if (!signature) throw new EngineError(EngineErrorCode.Unknown, "the installed document lost the signature field");
10538
+ const result = {
10539
+ status: "completed",
10540
+ signature,
10541
+ version: reader.version(),
10542
+ previous: pending.prepared.expectedVersion,
10543
+ protection: snapshot.protection,
10544
+ meta: {
10545
+ affectedPages: this.session.allRecords().map((r) => this.session.pageState(r.pageObjectNumber)),
10546
+ cacheDelta: null
10547
+ }
10548
+ };
10549
+ this.session.lastCompletion = {
10550
+ signingId: input.signingId,
10551
+ cms: input.cms.slice(),
10552
+ result
10553
+ };
10554
+ return result;
10555
+ }
10556
+ abort(signingId) {
10557
+ const pending = this.session.pendingSigning;
10558
+ if (pending && pending.prepared.signingId === signingId) {
10559
+ this.session.pendingSigning = null;
10560
+ this.storeFor().discard(pending.saved);
10561
+ return { status: "aborted" };
10562
+ }
10563
+ if (this.session.lastCompletion?.signingId === signingId) return { status: "already-completed" };
10564
+ return { status: "unknown" };
10565
+ }
10566
+ callPrepare(docPtr, fieldObjectNumber, opts) {
10567
+ const { mem, fn } = this.runtime;
10568
+ const layout = this.runtime.kind === "wasm" ? PREPARE_ILP32 : PREPARE_LP64;
10569
+ const owned = [];
10570
+ const wide = (value) => {
10571
+ if (value === null) return NULL_PTR;
10572
+ const ptr = mem.writeU16String(value);
10573
+ owned.push(ptr);
10574
+ return ptr;
10575
+ };
10576
+ const utf8 = (value) => {
10577
+ if (value === null) return NULL_PTR;
10578
+ const ptr = mem.writeU8String(value);
10579
+ owned.push(ptr);
10580
+ return ptr;
10581
+ };
10582
+ try {
10583
+ return withWideStringArray(this.runtime, opts.fieldmdpFields, (fieldsPtr, fieldCount) => withScratch(mem, layout.bytes, (structPtr) => {
10584
+ for (let off = 0; off < layout.bytes; off += 4) mem.poke(structPtr, "i32", 0, off);
10585
+ const pokePtr = (offset, ptr) => {
10586
+ if (layout.ptrBytes === 4) mem.poke(structPtr, "i32", Number(ptr), offset);
10587
+ else mem.poke(structPtr, "i64", ptr, offset);
10588
+ };
10589
+ mem.poke(structPtr, "i32", opts.subfilter, layout.subfilter);
10590
+ mem.poke(structPtr, "i32", opts.digest, layout.digest);
10591
+ mem.poke(structPtr, "i32", opts.contentsSize, layout.contentsSize);
10592
+ pokePtr(layout.name, wide(opts.name));
10593
+ pokePtr(layout.reason, wide(opts.reason));
10594
+ pokePtr(layout.location, wide(opts.location));
10595
+ pokePtr(layout.contactInfo, wide(opts.contactInfo));
10596
+ pokePtr(layout.signingTime, utf8(opts.signingTime));
10597
+ mem.poke(structPtr, "i32", opts.docmdpPermission, layout.docmdpPermission);
10598
+ mem.poke(structPtr, "i32", opts.fieldmdpAction, layout.fieldmdpAction);
10599
+ pokePtr(layout.fieldmdpFields, fieldCount > 0 ? fieldsPtr : NULL_PTR);
10600
+ mem.poke(structPtr, "i32", fieldCount, layout.fieldmdpFieldCount);
10601
+ mem.poke(structPtr, "i32", opts.lockPermission, layout.lockPermission);
10602
+ return fn.EPDFSig_Prepare(docPtr, fieldObjectNumber, structPtr);
10603
+ }));
10604
+ } finally {
10605
+ for (let i = owned.length - 1; i >= 0; i--) mem.free(owned[i]);
10606
+ }
10607
+ }
10608
+ /** Draw a page of `pdf` into the widget's appearance stream on the candidate. */
10609
+ /**
10610
+ * File-backed sessions (a file base in the registry: the server) persist
10611
+ * the candidate as a file beside the base; everything else keeps it in
10612
+ * memory. The choice follows the SESSION's base, not the candidate's.
10613
+ */
10614
+ storeFor() {
10615
+ const base = this.session.source.base;
10616
+ if (base?.kind === "file") return new FileCandidateStore(this.runtime, this.baseDocuments, base.path, this.candidatePath);
10617
+ return new MemoryCandidateStore(this.runtime, this.baseDocuments);
10618
+ }
10619
+ /**
10620
+ * Prove the sealed bytes carry a whole-revision signature on the field
10621
+ * before anything is installed; closes the handle on failure.
10622
+ */
10623
+ /**
10624
+ * Read the sealed candidate back before it is installed: the checks in
10625
+ * `assertSealedSignature` are made on a model loaded from the sealed
10626
+ * bytes themselves, so a candidate the writer got wrong never becomes
10627
+ * the session's bytes.
10628
+ */
10629
+ verifySealed(handle, expected) {
10630
+ const { fn } = this.runtime;
10631
+ try {
10632
+ const model = fn.EPDFSig_LoadModel(handle.docPtr);
10633
+ if (!model) throw new EngineError(EngineErrorCode.Unknown, "the sealed bytes have no signature model");
10634
+ try {
10635
+ assertSealedSignature(this.runtime, handle.docPtr, model, expected);
10636
+ } finally {
10637
+ fn.EPDFSig_CloseModel(model);
10638
+ }
10639
+ if (this.session.kind !== "layer") this.session.persistLayerArtifact = false;
10640
+ } catch (error) {
10641
+ handle.close();
10642
+ throw error;
10643
+ }
10644
+ }
10645
+ /**
10646
+ * The candidate the signature is authored on. A layer session reopens a
10647
+ * fresh layer over ITS OWN immutable base (a registry retain, no copy of
10648
+ * the file), fed the layer it was opened with — or, with unsaved edits,
10649
+ * the artifact a save would write, which is cumulative: the edits and
10650
+ * the signature then share one revision, as Acrobat saves a
10651
+ * fill-and-sign. The fallback freezes the complete loaded bytes as a new
10652
+ * memory base: for a plain session, and for a layer whose loaded delta
10653
+ * already holds signed bytes (a layer save rewrites the delta and would
10654
+ * drop them; the fork refuses to prepare such a candidate).
10655
+ */
10656
+ openCandidate(signingId, stack) {
10657
+ const password = this.session.password;
10658
+ const source = this.session.source;
10659
+ const saver = new DocumentSaver(this.runtime, this.session);
10660
+ const scratch = saver.canWriteScratchFiles() && source.base?.kind === "file" ? source.base.path : null;
10661
+ if (this.session.kind === "layer" && source.base && !this.loadedDeltaHoldsSignedBytes()) {
10662
+ const base = this.baseDocuments.retainByKey(source.base.key);
10663
+ if (base) {
10664
+ let layer = source.layer ?? { kind: "fresh" };
10665
+ if (this.session.hasUnsavedEdits()) if (scratch) {
10666
+ const path = scratchPath(scratch, "signing", signingId, "layer");
10667
+ stack.push(() => this.runtime.fileWrite.removeFile(path));
10668
+ if (saver.saveLayerArtifactToFileEx(path).changedSinceLoad) layer = {
10669
+ kind: "artifact-file",
10670
+ path
10671
+ };
10672
+ } else {
10673
+ const artifact = saver.saveLayerArtifactEx();
10674
+ if (artifact.changedSinceLoad) layer = {
10675
+ kind: "artifact",
10676
+ bytes: artifact.bytes
10677
+ };
10678
+ }
10679
+ return openLayerDocument(this.runtime, base, layer, password);
10680
+ }
10681
+ }
10682
+ if (scratch) {
10683
+ const path = scratchPath(scratch, "signing", signingId, "base.pdf");
10684
+ stack.push(() => this.runtime.fileWrite.removeFile(path));
10685
+ saver.snapshotToFile(path);
10686
+ const base = this.baseDocuments.acquireFileBase({
10687
+ key: `candidate:${signingId}`,
10688
+ path,
10689
+ password
10690
+ });
10691
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
10692
+ }
10693
+ const candidateBytes = saver.snapshot().bytes;
10694
+ const base = this.baseDocuments.acquireMemoryBase({
10695
+ key: `candidate:${signingId}`,
10696
+ bytes: new Uint8Array(candidateBytes),
10697
+ password
10698
+ });
10699
+ return openLayerDocument(this.runtime, base, { kind: "fresh" }, password);
10700
+ }
10701
+ /** Whether a signature's byte range reaches into the layer's loaded delta (past the base). */
10702
+ loadedDeltaHoldsSignedBytes() {
10703
+ const layer = this.session.source.layer;
10704
+ if (!layer || layer.kind === "fresh") return false;
10705
+ const baseSize = Number(this.runtime.fn.EPDFDoc_GetBaseBytesSize(this.session.requireDocPtr()));
10706
+ return new SignatureReader(this.runtime, this.session).readSnapshot().signatures.some((s) => s.signed && s.byteRange !== null && s.byteRange[2] + s.byteRange[3] > baseSize);
10707
+ }
10708
+ };
10709
+ function sameVersion(a, b) {
10710
+ return a.baseSha256 === b.baseSha256 && a.editsVersion === b.editsVersion;
10711
+ }
10712
+ //#endregion
10713
+ //#region src/features/signature/SignatureAnalyzer.ts
10714
+ const CHANGE_BY_CODE = {
10715
+ 0: "added",
10716
+ 1: "modified",
10717
+ 2: "freed"
10718
+ };
10719
+ const KIND_BY_CODE = {
10720
+ 0: "dictionary",
10721
+ 1: "stream",
10722
+ 2: "array",
10723
+ 3: "scalar",
10724
+ 4: "xref",
10725
+ 5: "objstm",
10726
+ 6: "trailer"
10727
+ };
10728
+ const DIFF_OLD = 0;
10729
+ const DIFF_NEW = 1;
10730
+ /** EPDF_DIFF_READ_OK / FAILED / ABSENT. */
10731
+ const READ_BY_CODE = {
10732
+ 0: "ok",
10733
+ 1: "failed",
10734
+ 2: "absent"
10735
+ };
10736
+ /** The objects an edge may be anchored at: the trailer, the structure of the revision, and every changed object. */
10737
+ function anchorSet(s, changed) {
10738
+ const out = /* @__PURE__ */ new Set([
10739
+ 0,
10740
+ s.root,
10741
+ s.pagesRoot,
10742
+ ...s.pages,
10743
+ ...changed
10744
+ ]);
10745
+ if (s.acroForm) out.add(s.acroForm);
10746
+ for (const f of s.fields) {
10747
+ out.add(f.objectNumber);
10748
+ for (const w of f.widgets) out.add(w);
10749
+ }
10750
+ return out;
10751
+ }
10752
+ /**
10753
+ * Modification analysis over a session: which revisions to compare, the
10754
+ * fork's exact object diff for each adjacent pair, each revision's
10755
+ * structure, and the pure rule engine's verdict per step. Every revision
10756
+ * is opened as its own prefix document from the target's loaded bytes;
10757
+ * `working-copy` first snapshots the session the way a save would.
10758
+ */
10759
+ var SignatureAnalyzer = class {
10760
+ constructor(runtime, session) {
10761
+ this.runtime = runtime;
10762
+ this.session = session;
10763
+ }
10764
+ /**
10765
+ * The signature snapshot of the session's working copy: the unsaved
10766
+ * state snapshotted the way a save would write it, as one more revision
10767
+ * over the loaded bytes. Identical to the loaded-bytes snapshot when
10768
+ * nothing is unsaved.
10769
+ */
10770
+ readWorkingCopySnapshot() {
10771
+ const stack = new CloseStack();
10772
+ try {
10773
+ const copy = this.openWorkingCopy(stack);
10774
+ if (copy.source === "loaded") return new SignatureReader(this.runtime, this.session).readSnapshot();
10775
+ const target = copy.target;
10776
+ return withSignatureModel(this.runtime, target, (model) => {
10777
+ const chainValid = this.runtime.fn.EPDFSig_IsRevisionChainValid(model);
10778
+ const signatures = readSignaturesFromModel(this.runtime, model);
10779
+ return {
10780
+ chainValid,
10781
+ revisions: chainValid ? readRevisions(this.runtime, target, signatures) : [],
10782
+ signatures,
10783
+ protection: deriveProtection(signatures)
10784
+ };
10785
+ });
10786
+ } finally {
10787
+ stack.close();
10788
+ }
10789
+ }
10790
+ analyze(input) {
10791
+ const reader = new SignatureReader(this.runtime, this.session);
10792
+ const snapshot = reader.readSnapshot();
10793
+ const version = reader.version();
10794
+ const mode = input.exploratoryLevel ? "exploratory" : "authoritative";
10795
+ const detail = input.detail ?? "summary";
10796
+ let basisSource = "persisted";
10797
+ const editsVersion = this.session.mutationSeq();
10798
+ let sinceRevision;
10799
+ let sinceSignature = null;
10800
+ if ("signatureIndex" in input.since) {
10801
+ const sig = snapshot.signatures[input.since.signatureIndex];
10802
+ if (!sig) throw new EngineError(EngineErrorCode.NotFound, `no signature ${input.since.signatureIndex}`);
10803
+ if (!sig.signed || sig.revisionIndex === null) throw new EngineError(EngineErrorCode.InvalidArg, `signature ${input.since.signatureIndex} seals no whole revision; nothing to analyze from`);
10804
+ sinceRevision = sig.revisionIndex;
10805
+ sinceSignature = sig.index;
10806
+ } else sinceRevision = input.since.revisionIndex;
10807
+ if (!snapshot.chainValid) {
10808
+ const current = {
10809
+ verdict: "indeterminate",
10810
+ complete: false,
10811
+ findings: [{
10812
+ rule: "revision-chain",
10813
+ verdict: "incomplete",
10814
+ objectNumber: 0,
10815
+ detail: "the cross-reference chain is broken or was rebuilt: no revision can be told from another"
10816
+ }],
10817
+ method: "net-state"
10818
+ };
10819
+ return {
10820
+ mode,
10821
+ policyVersion: SIGNATURE_POLICY_VERSION,
10822
+ basis: {
10823
+ version,
10824
+ editsVersion,
10825
+ source: "persisted"
10826
+ },
10827
+ since: {
10828
+ revisionIndex: sinceRevision,
10829
+ signatureIndex: sinceSignature
10830
+ },
10831
+ until: { revisionIndex: sinceRevision },
10832
+ restrictions: [],
10833
+ current: {
10834
+ ...current,
10835
+ primary: current.findings[0]
10836
+ },
10837
+ later: {
10838
+ revisionCount: 0,
10839
+ undoneObjectNumbers: []
10840
+ },
10841
+ verdict: "indeterminate",
10842
+ steps: []
10843
+ };
10844
+ }
10845
+ if (!Number.isInteger(sinceRevision) || sinceRevision < 0 || sinceRevision >= snapshot.revisions.length) throw new EngineError(EngineErrorCode.InvalidArg, `no revision ${sinceRevision}`);
10846
+ const stack = new CloseStack();
10847
+ try {
10848
+ let target = this.session.requireDocPtr();
10849
+ let revisions = snapshot.revisions;
10850
+ const copy = input.until === "working-copy" ? this.openWorkingCopy(stack) : null;
10851
+ if (copy && copy.source === "working-copy") {
10852
+ basisSource = "working-copy";
10853
+ target = copy.target;
10854
+ const signatures = withSignatureModel(this.runtime, target, (m) => readSignaturesFromModel(this.runtime, m));
10855
+ revisions = readRevisions(this.runtime, target, signatures);
10856
+ if (revisions.length === 0) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy has no valid revision chain");
10857
+ }
10858
+ let untilRevision = revisions.length - 1;
10859
+ if (typeof input.until === "object") {
10860
+ untilRevision = input.until.revisionIndex;
10861
+ if (!Number.isInteger(untilRevision) || untilRevision < sinceRevision || untilRevision >= revisions.length) throw new EngineError(EngineErrorCode.InvalidArg, `no revision ${untilRevision} at or after ${sinceRevision}`);
10862
+ }
10863
+ const pair = new CloseStack();
10864
+ let before;
10865
+ let after;
10866
+ let changes = [];
10867
+ let health;
10868
+ try {
10869
+ const sealed = this.openPrefix(target, revisions, sinceRevision, pair);
10870
+ const judged = untilRevision === revisions.length - 1 ? target : this.openPrefix(target, revisions, untilRevision, pair);
10871
+ before = readStructure(this.runtime, sealed);
10872
+ after = untilRevision === sinceRevision ? before : readStructure(this.runtime, judged);
10873
+ if (untilRevision > sinceRevision) ({changes, health} = this.compare(sealed, judged, before, after));
10874
+ } finally {
10875
+ pair.close();
10876
+ }
10877
+ const judgedSignature = sinceSignature === null ? null : before.signatures[sinceSignature] ?? null;
10878
+ const restrictions = restrictionsFor(before, judgedSignature);
10879
+ const levelOverride = input.exploratoryLevel;
10880
+ const net = evaluateStep({
10881
+ older: sinceRevision,
10882
+ newer: untilRevision,
10883
+ changes,
10884
+ before,
10885
+ after,
10886
+ health,
10887
+ restrictions: {
10888
+ level: restrictions.level,
10889
+ locks: restrictions.locks
10890
+ },
10891
+ levelOverride
10892
+ });
10893
+ const certifies = restrictions.anchors.some((r) => r.source === "docmdp" && r.own);
10894
+ const intermediate = untilRevision - sinceRevision;
10895
+ let steps;
10896
+ if (intermediate <= 1) steps = intermediate === 1 ? [net] : [];
10897
+ else if (certifies || detail === "full") steps = this.walkSteps(target, revisions, sinceRevision, untilRevision, before, {
10898
+ level: restrictions.level,
10899
+ locks: restrictions.locks,
10900
+ levelOverride
10901
+ });
10902
+ else steps = [];
10903
+ const current = certifies ? {
10904
+ ...combine(net, steps),
10905
+ method: "net-state+replay"
10906
+ } : {
10907
+ ...assessmentOf(net),
10908
+ method: "net-state"
10909
+ };
10910
+ return {
10911
+ mode,
10912
+ policyVersion: SIGNATURE_POLICY_VERSION,
10913
+ basis: {
10914
+ version,
10915
+ editsVersion,
10916
+ source: basisSource
10917
+ },
10918
+ since: {
10919
+ revisionIndex: sinceRevision,
10920
+ signatureIndex: sinceSignature
10921
+ },
10922
+ until: { revisionIndex: untilRevision },
10923
+ restrictions: restrictions.anchors,
10924
+ current,
10925
+ later: {
10926
+ revisionCount: intermediate,
10927
+ undoneObjectNumbers: changes.filter(sameEffectiveValue).map((c) => c.objectNumber)
10928
+ },
10929
+ verdict: current.verdict,
10930
+ steps
10931
+ };
10932
+ } finally {
10933
+ stack.close();
10934
+ }
10935
+ }
10936
+ /** Revision `index` of `target`'s bytes as its own document, owned by `stack`. */
10937
+ openPrefix(target, revisions, index, stack) {
10938
+ const prefix = this.runtime.fn.EPDFDoc_OpenRevision(target, BigInt(revisions[index].end));
10939
+ if (prefix === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, `revision ${index} does not open as a document`);
10940
+ stack.push(() => this.runtime.fn.FPDF_CloseDocument(prefix));
10941
+ return prefix;
10942
+ }
10943
+ /**
10944
+ * Every pairwise step between `since` and `until`, two prefix documents
10945
+ * open at a time whatever the revision count. `sealedStructure` is the
10946
+ * structure of `since` (already read); every later structure is read once
10947
+ * and dropped with its document.
10948
+ */
10949
+ walkSteps(target, revisions, since, until, sealedStructure, restrictions) {
10950
+ const { fn } = this.runtime;
10951
+ const steps = [];
10952
+ let older = this.openPrefix(target, revisions, since, new CloseStack());
10953
+ let olderStructure = sealedStructure;
10954
+ try {
10955
+ for (let r = since + 1; r <= until; r++) {
10956
+ const newer = r === revisions.length - 1 ? target : this.openPrefix(target, revisions, r, new CloseStack());
10957
+ try {
10958
+ const newerStructure = readStructure(this.runtime, newer);
10959
+ const { changes, health } = this.compare(older, newer, olderStructure, newerStructure);
10960
+ steps.push(evaluateStep({
10961
+ older: r - 1,
10962
+ newer: r,
10963
+ changes,
10964
+ before: olderStructure,
10965
+ after: newerStructure,
10966
+ health,
10967
+ restrictions: {
10968
+ level: restrictions.level,
10969
+ locks: restrictions.locks
10970
+ },
10971
+ levelOverride: restrictions.levelOverride
10972
+ }));
10973
+ olderStructure = newerStructure;
10974
+ } finally {
10975
+ fn.FPDF_CloseDocument(older);
10976
+ older = newer;
10977
+ }
10978
+ }
10979
+ } finally {
10980
+ if (older !== target) fn.FPDF_CloseDocument(older);
10981
+ }
10982
+ return steps;
10983
+ }
10984
+ /**
10985
+ * The document as a save would write it, opened read-only, and whose
10986
+ * bytes that is. `loaded`: nothing was mutated since load, or the saver's
10987
+ * pass found no reachable object that differs from its loaded version —
10988
+ * the session's own document is the working copy. `working-copy`: a
10989
+ * layer session composes its immutable base with the cumulative delta
10990
+ * the pass emits (`EPDFDoc_OpenBaseOverlay`: no copy of the base, one
10991
+ * cross-reference parse, the loaded delta's revision REPLACED, as an
10992
+ * artifact is), or — when every edit brought the layer back to its base
10993
+ * — a fresh layer over that base; a plain session (unsigned by law 9, so
10994
+ * this is exploratory) materialises a standalone incremental save and
10995
+ * opens it whole.
10996
+ */
10997
+ openWorkingCopy(stack) {
10998
+ const { fn, mem } = this.runtime;
10999
+ const docPtr = this.session.requireDocPtr();
11000
+ if (!this.session.hasUnsavedEdits()) return {
11001
+ target: docPtr,
11002
+ source: "loaded"
11003
+ };
11004
+ const saver = new DocumentSaver(this.runtime, this.session);
11005
+ if (this.session.kind === "layer") {
11006
+ const base = this.session.source.base;
11007
+ if (saver.canWriteScratchFiles() && base?.kind === "file") {
11008
+ const path = scratchPath(base.path, "working-copy", generateUuid(), "delta");
11009
+ stack.push(() => this.runtime.fileWrite.removeFile(path));
11010
+ if (!saver.saveLayerDeltaToFileEx(path).changedSinceLoad) return {
11011
+ target: docPtr,
11012
+ source: "loaded"
11013
+ };
11014
+ if (this.runtime.fileAccess.sizeOf(path) === 0) return this.freshLayerOverBase(stack);
11015
+ const overlay = withUtf8CString(mem, path, (pathPtr) => fn.EPDFDoc_OpenBaseOverlayFromPath(docPtr, pathPtr));
11016
+ if (overlay === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy does not compose with its base");
11017
+ stack.push(() => fn.FPDF_CloseDocument(overlay));
11018
+ return {
11019
+ target: overlay,
11020
+ source: "working-copy"
11021
+ };
11022
+ }
11023
+ const delta = saver.saveLayerDeltaEx();
11024
+ if (!delta.changedSinceLoad) return {
11025
+ target: docPtr,
11026
+ source: "loaded"
11027
+ };
11028
+ if (delta.size === 0) return this.freshLayerOverBase(stack);
11029
+ const bytes = new Uint8Array(delta.bytes);
11030
+ const ptr = mem.alloc(bytes.byteLength);
11031
+ try {
11032
+ mem.writeBytes(ptr, bytes);
11033
+ const overlay = fn.EPDFDoc_OpenBaseOverlay(docPtr, ptr, bytes.byteLength);
11034
+ if (overlay === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy does not compose with its base");
11035
+ stack.push(() => fn.FPDF_CloseDocument(overlay));
11036
+ return {
11037
+ target: overlay,
11038
+ source: "working-copy"
11039
+ };
11040
+ } finally {
11041
+ mem.free(ptr);
11042
+ }
11043
+ }
11044
+ const saved = saver.saveStandaloneToBufferEx("incremental");
11045
+ if (saved.unchangedSinceLoad) return {
11046
+ target: docPtr,
11047
+ source: "loaded"
11048
+ };
11049
+ const opened = openFatMemoryDocument(this.runtime, new Uint8Array(saved.bytes), this.session.password);
11050
+ stack.push(() => opened.close());
11051
+ return {
11052
+ target: opened.docPtr,
11053
+ source: "working-copy"
11054
+ };
11055
+ }
11056
+ /** Changed since load, nothing to write: the layer equals its base, and a fresh layer over it IS that document. */
11057
+ freshLayerOverBase(stack) {
11058
+ const { fn, mem } = this.runtime;
11059
+ const base = fn.EPDFLayer_GetBaseDocument(this.session.requireDocPtr());
11060
+ const statusPtr = mem.alloc(4);
11061
+ try {
11062
+ mem.poke(statusPtr, "i32", -1);
11063
+ const fresh = fn.EPDFLayer_OpenLayer(base, NULL_PTR, this.session.password ?? "", statusPtr);
11064
+ if (fresh === NULL_PTR || Number(mem.peek(statusPtr, "i32")) !== 0) throw new EngineError(EngineErrorCode.MalformedPdf, "the working copy could not be opened over its base");
11065
+ stack.push(() => fn.FPDF_CloseDocument(fresh));
11066
+ return {
11067
+ target: fresh,
11068
+ source: "working-copy"
11069
+ };
11070
+ } finally {
11071
+ mem.free(statusPtr);
11072
+ }
11073
+ }
11074
+ /**
11075
+ * The fork's exact object diff between two revision documents,
11076
+ * marshalled: values in pass one, anchored edges in pass two (the anchors
11077
+ * are the changed objects plus each side's structural objects).
11078
+ */
11079
+ compare(older, newer, before, after) {
11080
+ const { fn, mem } = this.runtime;
11081
+ const diff = fn.EPDFDoc_CompareRevisions(older, newer);
11082
+ if (diff === NULL_PTR) throw new EngineError(EngineErrorCode.MalformedPdf, "the revisions do not share a byte history");
11083
+ try {
11084
+ const count = fn.EPDFObjectDiff_GetCount(diff);
11085
+ const changes = [];
11086
+ withScratchN(mem, [
11087
+ 4,
11088
+ 4,
11089
+ 4,
11090
+ 4,
11091
+ 4,
11092
+ 4
11093
+ ], ([numPtr, changePtr, kindPtr, oldGenPtr, newGenPtr, streamPtr]) => {
11094
+ for (let i = 0; i < count; i++) {
11095
+ for (const p of [
11096
+ numPtr,
11097
+ changePtr,
11098
+ kindPtr,
11099
+ oldGenPtr,
11100
+ newGenPtr,
11101
+ streamPtr
11102
+ ]) mem.poke(p, "i32", 0);
11103
+ if (!fn.EPDFObjectDiff_GetEntry(diff, i, numPtr, changePtr, kindPtr, oldGenPtr, newGenPtr, streamPtr)) throw new EngineError(EngineErrorCode.Unknown, `failed to read diff entry ${i}`);
11104
+ const objectNumber = Number(mem.peek(numPtr, "i32")) >>> 0;
11105
+ const oldGen = Number(mem.peek(oldGenPtr, "i32"));
11106
+ const newGen = Number(mem.peek(newGenPtr, "i32"));
11107
+ const oldValue = this.readValue(diff, i, DIFF_OLD);
11108
+ const newValue = this.readValue(diff, i, DIFF_NEW);
11109
+ let truncated = oldValue.truncated || newValue.truncated;
11110
+ let oldParsed = null;
11111
+ let newParsed = null;
11112
+ try {
11113
+ oldParsed = oldValue.text === null || oldValue.truncated ? null : parsePdfValue(oldValue.text);
11114
+ newParsed = newValue.text === null || newValue.truncated ? null : parsePdfValue(newValue.text);
11115
+ } catch {
11116
+ truncated = true;
11117
+ }
11118
+ const change = CHANGE_BY_CODE[Number(mem.peek(changePtr, "i32"))] ?? "modified";
11119
+ changes.push({
11120
+ objectNumber,
11121
+ change,
11122
+ kind: KIND_BY_CODE[Number(mem.peek(kindPtr, "i32"))] ?? "scalar",
11123
+ present: {
11124
+ old: change !== "added",
11125
+ new: change !== "freed"
11126
+ },
11127
+ generation: {
11128
+ old: oldGen < 0 ? null : oldGen,
11129
+ new: newGen < 0 ? null : newGen
11130
+ },
11131
+ value: {
11132
+ old: oldParsed,
11133
+ new: newParsed,
11134
+ truncated
11135
+ },
11136
+ raw: {
11137
+ old: oldValue.text,
11138
+ new: newValue.text
11139
+ },
11140
+ read: {
11141
+ old: READ_BY_CODE[fn.EPDFObjectDiff_GetReadStatus(diff, i, DIFF_OLD)] ?? "failed",
11142
+ new: READ_BY_CODE[fn.EPDFObjectDiff_GetReadStatus(diff, i, DIFF_NEW)] ?? "failed"
11143
+ },
11144
+ streamDataChanged: Number(mem.peek(streamPtr, "i32")) !== 0,
11145
+ usage: {
11146
+ old: [],
11147
+ new: []
11148
+ }
11149
+ });
11150
+ }
11151
+ });
11152
+ return {
11153
+ changes: this.withAnchoredUsage(diff, changes, before, after),
11154
+ health: {
11155
+ old: this.readHealth(diff, DIFF_OLD),
11156
+ new: this.readHealth(diff, DIFF_NEW)
11157
+ }
11158
+ };
11159
+ } finally {
11160
+ fn.EPDFObjectDiff_Close(diff);
11161
+ }
11162
+ }
11163
+ /** What the fork learned about one revision as a whole (see `RevisionHealth`). */
11164
+ readHealth(diff, which) {
11165
+ const { fn, mem } = this.runtime;
11166
+ return withScratchN(mem, [
11167
+ 4,
11168
+ 4,
11169
+ 4
11170
+ ], ([sparsePtr, barePtr, completePtr]) => {
11171
+ for (const p of [
11172
+ sparsePtr,
11173
+ barePtr,
11174
+ completePtr
11175
+ ]) mem.poke(p, "i32", 0);
11176
+ if (!fn.EPDFObjectDiff_GetRevisionHealth(diff, which, sparsePtr, barePtr, completePtr)) throw new EngineError(EngineErrorCode.Unknown, "failed to read revision health");
11177
+ return {
11178
+ sparseXref: Number(mem.peek(sparsePtr, "i32")) !== 0,
11179
+ bareReferences: Number(mem.peek(barePtr, "i32")) >>> 0,
11180
+ referrersComplete: Number(mem.peek(completePtr, "i32")) !== 0
11181
+ };
11182
+ });
11183
+ }
11184
+ /**
11185
+ * Pass two: every changed object's inbound edges, resolved through
11186
+ * unchanged non-structural objects to the nearest anchor (the fork's
11187
+ * referrer index covers every reachable object, so the walk never leaves
11188
+ * the diff). An object whose edges exceed the resolver's budget is
11189
+ * marked `usageIncomplete` and the evaluator refuses to permit it.
11190
+ */
11191
+ withAnchoredUsage(diff, changes, before, after) {
11192
+ const changed = new Set(changes.map((c) => c.objectNumber));
11193
+ const resolvers = {
11194
+ old: new EdgeResolver((n) => this.readReferrers(diff, DIFF_OLD, n), anchorSet(before, changed)),
11195
+ new: new EdgeResolver((n) => this.readReferrers(diff, DIFF_NEW, n), anchorSet(after, changed))
11196
+ };
11197
+ return changes.map((c) => {
11198
+ const old = c.present.old ? resolvers.old.resolve(c.objectNumber) : [];
11199
+ const now = c.present.new ? resolvers.new.resolve(c.objectNumber) : [];
11200
+ const incomplete = old === USAGE_INCOMPLETE || now === USAGE_INCOMPLETE;
11201
+ return {
11202
+ ...c,
11203
+ usage: {
11204
+ old: old === USAGE_INCOMPLETE ? [] : old,
11205
+ new: now === USAGE_INCOMPLETE ? [] : now
11206
+ },
11207
+ ...incomplete ? { usageIncomplete: true } : {}
8387
11208
  };
8388
- } finally {
8389
- if (pdfPtr) fn.EPDF_FreeBuffer(pdfPtr);
8390
- mem.free(sizePtr);
8391
- }
11209
+ });
8392
11210
  }
8393
- saveStandaloneToFile(path, mode) {
8394
- const writer = this.runtime.fileWrite.toNodeFile(path);
11211
+ readValue(diff, index, which) {
11212
+ const { fn, mem } = this.runtime;
11213
+ return withScratch(mem, 4, (truncPtr) => {
11214
+ mem.poke(truncPtr, "i32", 0);
11215
+ const length = fn.EPDFObjectDiff_GetValue(diff, index, which, NULL_PTR, 0, truncPtr);
11216
+ const truncatedProbe = Number(mem.peek(truncPtr, "i32")) !== 0;
11217
+ if (length <= 0) return {
11218
+ text: null,
11219
+ truncated: truncatedProbe
11220
+ };
11221
+ return withScratch(mem, length, (buf) => {
11222
+ mem.poke(truncPtr, "i32", 0);
11223
+ const written = fn.EPDFObjectDiff_GetValue(diff, index, which, buf, length, truncPtr);
11224
+ const truncated = Number(mem.peek(truncPtr, "i32")) !== 0;
11225
+ if (written <= 0) return {
11226
+ text: null,
11227
+ truncated
11228
+ };
11229
+ const bytes = mem.readBytes(buf, written - 1);
11230
+ let text = "";
11231
+ for (const b of bytes) text += String.fromCharCode(b);
11232
+ return {
11233
+ text,
11234
+ truncated
11235
+ };
11236
+ });
11237
+ });
11238
+ }
11239
+ readReferrers(diff, which, objectNumber) {
11240
+ const { fn, mem } = this.runtime;
11241
+ const count = fn.EPDFObjectDiff_GetReferrerCount(diff, which, objectNumber);
11242
+ const out = [];
11243
+ if (count <= 0) return out;
11244
+ withScratch(mem, 4, (parentPtr) => {
11245
+ for (let k = 0; k < count; k++) {
11246
+ mem.poke(parentPtr, "i32", 0);
11247
+ const length = fn.EPDFObjectDiff_GetReferrer(diff, which, objectNumber, k, parentPtr, NULL_PTR, 0);
11248
+ const label = length <= 0 ? "" : withScratch(mem, length, (buf) => {
11249
+ const written = fn.EPDFObjectDiff_GetReferrer(diff, which, objectNumber, k, parentPtr, buf, length);
11250
+ if (written <= 0) return "";
11251
+ const bytes = mem.readBytes(buf, written - 1);
11252
+ let text = "";
11253
+ for (const b of bytes) text += String.fromCharCode(b);
11254
+ return text;
11255
+ });
11256
+ out.push({
11257
+ parent: Number(mem.peek(parentPtr, "i32")) >>> 0,
11258
+ label
11259
+ });
11260
+ }
11261
+ });
11262
+ return out;
11263
+ }
11264
+ };
11265
+ //#endregion
11266
+ //#region src/features/signature/CandidateFinalizer.ts
11267
+ /**
11268
+ * The session-less half of a durable signing. A server prepares a
11269
+ * candidate on one replica, keeps only its tail, and may complete it on
11270
+ * another: the completing replica rebuilds base ⊕ tail into a private
11271
+ * file and hands it here with what the prepare reported. The CMS is
11272
+ * hex-encoded into the /Contents hole in place — never more than the
11273
+ * hole itself is held in memory — and the file is then opened like any
11274
+ * other base and read back through the signature model, so the version
11275
+ * returned is the file's own hash and the signature is what the file
11276
+ * says, not what the writer meant. Nothing here touches a live session.
11277
+ */
11278
+ var CandidateFinalizer = class {
11279
+ constructor(runtime, baseDocuments) {
11280
+ this.runtime = runtime;
11281
+ this.baseDocuments = baseDocuments;
11282
+ }
11283
+ finalize(input) {
11284
+ const [r0, r1, r2, r3] = input.byteRange;
11285
+ const size = this.runtime.fileAccess.sizeOf(input.path);
11286
+ const isOffset = (n) => Number.isSafeInteger(n) && n >= 0;
11287
+ if (!input.byteRange.every(isOffset) || r0 !== 0 || r1 >= r2 || r2 + r3 !== size) throw new EngineError(EngineErrorCode.InvalidArg, `byteRange [${input.byteRange.join(" ")}] does not span a ${size}-byte candidate`);
11288
+ const hexLength = r2 - r1 - 2;
11289
+ if (!Number.isSafeInteger(input.contentsSize) || hexLength !== 2 * input.contentsSize) throw new EngineError(EngineErrorCode.InvalidArg, `the /Contents hole holds ${hexLength} hex digits, not the ${2 * input.contentsSize} of contentsSize`);
11290
+ if (input.cms.byteLength === 0 || input.cms[0] !== 48) throw new EngineError(EngineErrorCode.SignatureRefused, "the CMS is not a DER SEQUENCE");
11291
+ if (input.cms.byteLength > input.contentsSize) throw new EngineError(EngineErrorCode.SignatureRefused, `the CMS (${input.cms.byteLength} bytes) does not fit the reserved ${input.contentsSize} bytes`);
11292
+ writeContentsIntoFile(this.runtime, input.path, {
11293
+ holeOffset: r1,
11294
+ hexLength
11295
+ }, input.cms);
11296
+ const session = new DocumentSession(this.runtime);
11297
+ const base = this.baseDocuments.acquireFileBase({
11298
+ key: `finalize:${generateUuid()}`,
11299
+ path: input.path,
11300
+ password: input.password
11301
+ });
8395
11302
  try {
8396
- if (!this.runtime.fn.FPDF_SaveAsCopy(this.session.requireDocPtr(), writer.ptr, pdfSaveModeFlags(mode))) throw new EngineError(EngineErrorCode.DocOpenFailed, "failed to save document");
8397
- return { path };
11303
+ session.openFromHandle(openLayerDocument(this.runtime, base, { kind: "fresh" }, input.password));
11304
+ const model = acquireSignatureModel(this.runtime, session);
11305
+ assertSealedSignature(this.runtime, session.requireDocPtr(), model, {
11306
+ fieldObjectNumber: input.fieldObjectNumber,
11307
+ byteRange: input.byteRange,
11308
+ cms: input.cms
11309
+ });
11310
+ const reader = new SignatureReader(this.runtime, session);
11311
+ const snapshot = reader.readSnapshot();
11312
+ return {
11313
+ signature: snapshot.signatures.find((s) => s.field.kind === "objectNumber" && s.field.fieldObjectNumber === input.fieldObjectNumber),
11314
+ protection: snapshot.protection,
11315
+ version: reader.version()
11316
+ };
8398
11317
  } finally {
8399
- writer.close();
11318
+ session.close();
8400
11319
  }
8401
11320
  }
8402
- requireLayer() {
8403
- if (this.session.kind !== "layer") throw new EngineError(EngineErrorCode.InvalidArg, "document session is not a layer");
8404
- }
8405
11321
  };
8406
11322
  //#endregion
8407
11323
  //#region src/features/fonts/FontRegistrar.ts
11324
+ /** `EPDF_FONT_EMBEDDING_*` codes. Restricted (3) and bitmap-only (4) are
11325
+ * refused at registration, so they never reach a handle. */
11326
+ function permissionFromCode(code) {
11327
+ switch (code) {
11328
+ case 1: return "editable";
11329
+ case 2: return "preview-and-print";
11330
+ default: return "installable";
11331
+ }
11332
+ }
11333
+ /** The engine's family comparison: no case, spaces, hyphens, underscores. */
11334
+ function familyKey(family) {
11335
+ return family.toLowerCase().replace(/[\s\-_'"]/g, "");
11336
+ }
8408
11337
  /**
8409
11338
  * Thread-confined runtime font registry binding.
8410
11339
  *
@@ -8428,6 +11357,7 @@ var FontRegistrar = class {
8428
11357
  this.runtime = runtime;
8429
11358
  this.ids = ids;
8430
11359
  _defineProperty(this, "fileHandles", []);
11360
+ _defineProperty(this, "identities", /* @__PURE__ */ new Map());
8431
11361
  }
8432
11362
  /**
8433
11363
  * Register a font from its bytes. Copies into the WASM heap, hands the
@@ -8446,6 +11376,11 @@ var FontRegistrar = class {
8446
11376
  const id = fn.EPDFFont_RegisterMemFont64(familyName, weight, italic, ptr, bytes.byteLength);
8447
11377
  if (id === 0) throw new EngineError(EngineErrorCode.InvalidArg, `font registration failed (unloadable or no glyphs): ${fontKey}`);
8448
11378
  this.ids.set(fontKey, id);
11379
+ this.identities.set(fontKey, this.readIdentity(id, {
11380
+ familyName,
11381
+ weight,
11382
+ italic
11383
+ }));
8449
11384
  } finally {
8450
11385
  mem.free(ptr);
8451
11386
  }
@@ -8471,8 +11406,72 @@ var FontRegistrar = class {
8471
11406
  throw new EngineError(EngineErrorCode.InvalidArg, `font registration failed (unloadable or no glyphs): ${fontKey} (${path})`);
8472
11407
  }
8473
11408
  this.ids.set(fontKey, id);
11409
+ this.identities.set(fontKey, this.readIdentity(id, {
11410
+ familyName,
11411
+ weight,
11412
+ italic
11413
+ }));
8474
11414
  this.fileHandles.push(access);
8475
11415
  }
11416
+ /** The identity and licence the runtime resolved for a key. Throws if unknown. */
11417
+ describe(fontKey) {
11418
+ const identity = this.identities.get(fontKey);
11419
+ if (!identity) throw new EngineError(EngineErrorCode.InvalidArg, `font not registered on this thread: ${fontKey}`);
11420
+ return identity;
11421
+ }
11422
+ /** {@link describe} for callers that treat an unknown key as "not a key". */
11423
+ describeOrUndefined(fontKey) {
11424
+ return this.identities.get(fontKey);
11425
+ }
11426
+ /**
11427
+ * The key of the registered font whose identity matches a face (family
11428
+ * compared the engine's way; the closest weight, italic first), or
11429
+ * undefined when no registered family matches.
11430
+ */
11431
+ keyForFace(family, weight, italic) {
11432
+ const wanted = familyKey(family);
11433
+ let best;
11434
+ for (const [key, identity] of this.identities) {
11435
+ if (familyKey(identity.familyName) !== wanted) continue;
11436
+ const score = Math.abs(identity.weight - weight) + (identity.italic === italic ? 0 : 1e3);
11437
+ if (!best || score < best.score) best = {
11438
+ key,
11439
+ score
11440
+ };
11441
+ }
11442
+ return best?.key;
11443
+ }
11444
+ /** The application asserts a licence permitting editing with the font. */
11445
+ authorizeEditing(fontKey) {
11446
+ const id = this.requireId(fontKey);
11447
+ if (!this.runtime.fn.EPDFFont_AuthorizeEditing(id)) throw new EngineError(EngineErrorCode.InvalidArg, `authorizeEditing failed: ${fontKey}`);
11448
+ const previous = this.describe(fontKey);
11449
+ const identity = this.readIdentity(id, {
11450
+ familyName: previous.familyName,
11451
+ weight: previous.weight,
11452
+ italic: previous.italic ? 1 : 0
11453
+ });
11454
+ this.identities.set(fontKey, identity);
11455
+ return identity;
11456
+ }
11457
+ readIdentity(id, given) {
11458
+ const { fn, mem } = this.runtime;
11459
+ const attempt = (read, fallback) => {
11460
+ try {
11461
+ return read();
11462
+ } catch {
11463
+ return fallback;
11464
+ }
11465
+ };
11466
+ return {
11467
+ familyName: attempt(() => readUtf8String(mem, (buf, cap) => fn.EPDFFont_GetFamilyName(id, buf, cap)) ?? given.familyName, given.familyName),
11468
+ weight: attempt(() => fn.EPDFFont_GetWeight(id), given.weight || 400),
11469
+ italic: attempt(() => fn.EPDFFont_IsItalic(id), given.italic === 1),
11470
+ embeddingPermission: attempt(() => permissionFromCode(fn.EPDFFont_GetEmbeddingPermission(id)), "installable"),
11471
+ editingAuthorized: attempt(() => fn.EPDFFont_IsEditingAuthorized(id), true),
11472
+ instanced: attempt(() => fn.EPDFFont_IsInstanced(id), false)
11473
+ };
11474
+ }
8476
11475
  /**
8477
11476
  * Register host-owned startup fonts on this thread, in order, optionally
8478
11477
  * adding each to the fallback chain. Order matters: it fixes both the native
@@ -8498,6 +11497,7 @@ var FontRegistrar = class {
8498
11497
  clear() {
8499
11498
  this.runtime.fn.EPDFFont_ClearRegisteredFonts();
8500
11499
  this.ids.clear();
11500
+ this.identities.clear();
8501
11501
  for (const access of this.fileHandles) access.close();
8502
11502
  this.fileHandles.length = 0;
8503
11503
  }
@@ -8650,35 +11650,14 @@ function flagMasks(flags) {
8650
11650
  };
8651
11651
  }
8652
11652
  //#endregion
8653
- //#region src/features/forms/internal/wideStringArray.ts
8654
- /**
8655
- * Marshal a `const FPDF_WIDESTRING*` array: encode every string into the
8656
- * runtime heap, write the pointer table with the runtime's pointer width
8657
- * (bigint pointers cannot be poked into the wasm 32-bit heap), run |body|,
8658
- * free everything.
8659
- */
8660
- function withWideStringArray(runtime, values, body) {
8661
- const { mem } = runtime;
8662
- const stringPtrs = values.map((value) => mem.writeU16String(value));
8663
- const isWasm = runtime.kind === "wasm";
8664
- const ptrSize = isWasm ? 4 : 8;
8665
- try {
8666
- return withScratch(mem, Math.max(1, stringPtrs.length * ptrSize), (arrayPtr) => {
8667
- stringPtrs.forEach((ptr, i) => isWasm ? mem.poke(arrayPtr, "i32", Number(ptr), i * 4) : mem.poke(arrayPtr, "i64", ptr, i * 8));
8668
- return body(arrayPtr, stringPtrs.length);
8669
- });
8670
- } finally {
8671
- for (let i = stringPtrs.length - 1; i >= 0; i--) mem.free(stringPtrs[i]);
8672
- }
8673
- }
8674
- //#endregion
8675
11653
  //#region src/features/forms/FormMutator.ts
8676
11654
  const FAMILY_CODE = {
8677
11655
  checkbox: 2,
8678
11656
  radio: 3,
8679
11657
  text: 4,
8680
11658
  combobox: 5,
8681
- listbox: 6
11659
+ listbox: 6,
11660
+ signature: 7
8682
11661
  };
8683
11662
  /** Widgets a single value write can touch; far above any real form. */
8684
11663
  const CHANGED_WIDGETS_CAPACITY$1 = 256;
@@ -8711,7 +11690,7 @@ var FormMutator = class {
8711
11690
  throwIfAborted(signal);
8712
11691
  const model = acquireFormModel(this.runtime, this.session);
8713
11692
  const resolved = resolveFieldRef(this.runtime, model, ref);
8714
- this.assertWritable(resolved.fieldObjectNumber);
11693
+ this.assertWritable(resolved);
8715
11694
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8716
11695
  if (!FAMILY_BY_VALUE_TYPE[value.type].includes(before.family)) throw new EngineError(EngineErrorCode.InvalidArg, `value type '${value.type}' does not apply to a '${before.family}' field`);
8717
11696
  const changed = this.applyWrite(resolved.fieldObjectNumber, value);
@@ -8721,7 +11700,7 @@ var FormMutator = class {
8721
11700
  throwIfAborted(signal);
8722
11701
  const model = acquireFormModel(this.runtime, this.session);
8723
11702
  const resolved = resolveFieldRef(this.runtime, model, ref);
8724
- this.assertWritable(resolved.fieldObjectNumber);
11703
+ this.assertWritable(resolved);
8725
11704
  const changed = this.withChangedWidgets((buf, cap, countPtr) => this.runtime.fn.EPDFForm_ResetField(this.session.requireDocPtr(), resolved.fieldObjectNumber, buf, cap, countPtr));
8726
11705
  if (changed === null) throw new EngineError(EngineErrorCode.InvalidArg, "form field cannot be reset");
8727
11706
  return this.readBack(resolved.fieldObjectNumber, changed);
@@ -8814,13 +11793,39 @@ var FormMutator = class {
8814
11793
  this.session.noteMutation();
8815
11794
  return { field: this.readBackField(fieldObjectNumber) };
8816
11795
  }
11796
+ /**
11797
+ * Draw a PDF page into every widget of an UNSIGNED signature field: the
11798
+ * visual "sign" of a viewer without a signer. The field's value stays
11799
+ * empty and nothing is sealed; a signed field is refused (its appearance
11800
+ * is part of what the signature covers). Pages whose widgets changed are
11801
+ * reported so their renders re-pin.
11802
+ */
11803
+ setSignatureAppearance(ref, pdf, pageIndex, signal) {
11804
+ throwIfAborted(signal);
11805
+ const docPtr = this.session.requireDocPtr();
11806
+ const model = acquireFormModel(this.runtime, this.session);
11807
+ const resolved = resolveFieldRef(this.runtime, model, ref);
11808
+ this.assertWritable(resolved);
11809
+ const before = readFieldAt(this.runtime, model, resolved.fieldIndex, docPtr);
11810
+ if (before.family !== "signature") throw new EngineError(EngineErrorCode.InvalidArg, `'${before.name}' is a ${before.family} field, not a signature field`);
11811
+ if (withSignatureModel(this.runtime, docPtr, (signatures) => readSignaturesFromModel(this.runtime, signatures).some((s) => s.signed && s.field.kind === "objectNumber" && s.field.fieldObjectNumber === resolved.fieldObjectNumber))) throw new EngineError(EngineErrorCode.InvalidArg, `'${before.name}' is signed; its appearance is sealed with the signature`);
11812
+ if (before.widgets.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, `'${before.name}' has no widget to draw into`);
11813
+ for (const widget of before.widgets) bakeWidgetAppearance(this.runtime, docPtr, widget, pdf, pageIndex);
11814
+ this.session.noteMutation();
11815
+ const pages = [...new Set(before.widgets.map((w) => w.pageObjectNumber))];
11816
+ for (const pon of pages) this.session.bumpRevision(pon);
11817
+ return {
11818
+ field: this.readBackField(resolved.fieldObjectNumber),
11819
+ pages
11820
+ };
11821
+ }
8817
11822
  updateField(ref, patch, signal) {
8818
11823
  throwIfAborted(signal);
8819
11824
  const { fn } = this.runtime;
8820
11825
  const docPtr = this.session.requireDocPtr();
8821
11826
  const model = acquireFormModel(this.runtime, this.session);
8822
11827
  const resolved = resolveFieldRef(this.runtime, model, ref);
8823
- this.assertWritable(resolved.fieldObjectNumber);
11828
+ this.assertWritable(resolved);
8824
11829
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8825
11830
  if (before.family !== patch.family) throw new EngineError(EngineErrorCode.InvalidArg, `patch family '${patch.family}' does not match field family '${before.family}'`);
8826
11831
  const fieldObjectNumber = resolved.fieldObjectNumber;
@@ -8851,7 +11856,7 @@ var FormMutator = class {
8851
11856
  const { fn, mem } = this.runtime;
8852
11857
  const model = acquireFormModel(this.runtime, this.session);
8853
11858
  const resolved = resolveFieldRef(this.runtime, model, ref);
8854
- this.assertWritable(resolved.fieldObjectNumber);
11859
+ this.assertWritable(resolved);
8855
11860
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8856
11861
  if (!withScratchN(mem, [256 * 4, 4], ([buf, countPtr]) => {
8857
11862
  mem.poke(countPtr, "i32", 0);
@@ -8871,7 +11876,7 @@ var FormMutator = class {
8871
11876
  const { fn } = this.runtime;
8872
11877
  const model = acquireFormModel(this.runtime, this.session);
8873
11878
  const resolved = resolveFieldRef(this.runtime, model, ref);
8874
- this.assertWritable(resolved.fieldObjectNumber);
11879
+ this.assertWritable(resolved);
8875
11880
  const before = readFieldAt(this.runtime, model, resolved.fieldIndex, this.session.requireDocPtr());
8876
11881
  const toggle = before.family === "checkbox" || before.family === "radio";
8877
11882
  const state = toggle ? onState ?? (before.family === "checkbox" ? "Yes" : "") : "";
@@ -8885,7 +11890,7 @@ var FormMutator = class {
8885
11890
  const { fn } = this.runtime;
8886
11891
  const model = acquireFormModel(this.runtime, this.session);
8887
11892
  const resolved = resolveFieldRef(this.runtime, model, ref);
8888
- this.assertWritable(resolved.fieldObjectNumber);
11893
+ this.assertWritable(resolved);
8889
11894
  if (!fn.EPDFForm_DetachWidget(this.session.requireDocPtr(), resolved.fieldObjectNumber, widget.annotObjectNumber)) throw new EngineError(EngineErrorCode.InvalidArg, "widget is not attached to this field");
8890
11895
  this.session.noteMutation();
8891
11896
  return { field: this.readBackField(resolved.fieldObjectNumber) };
@@ -8920,8 +11925,30 @@ var FormMutator = class {
8920
11925
  if (fieldIndex < 0) throw new EngineError(EngineErrorCode.Unknown, "form field vanished after write");
8921
11926
  return readFieldAt(this.runtime, fresh, fieldIndex, this.session.requireDocPtr());
8922
11927
  }
8923
- assertWritable(fieldObjectNumber) {
8924
- if (fieldObjectNumber === 0) throw new EngineError(EngineErrorCode.InvalidArg, "form field is stored as a direct object and cannot be written");
11928
+ assertWritable(resolved) {
11929
+ if (resolved.fieldObjectNumber === 0) throw new EngineError(EngineErrorCode.InvalidArg, "form field is stored as a direct object and cannot be written");
11930
+ this.assertNotLockedBySignature(resolved);
11931
+ }
11932
+ /**
11933
+ * A field an earlier signature froze (its FieldMDP, or the /Lock of a
11934
+ * signed field) refuses every write: document-derived authority, the
11935
+ * same way encryption bits are. Off under `signedDocumentPolicy:
11936
+ * 'permit'`. A document whose signature model cannot be built is not
11937
+ * known to be locked.
11938
+ */
11939
+ assertNotLockedBySignature(resolved) {
11940
+ if (this.session.signedDocumentPolicy !== "protect") return;
11941
+ let protection;
11942
+ try {
11943
+ protection = new SignatureReader(this.runtime, this.session).readProtection();
11944
+ } catch {
11945
+ return;
11946
+ }
11947
+ if (protection.fieldLocks.length === 0) return;
11948
+ const model = acquireFormModel(this.runtime, this.session);
11949
+ const name = readUtf16String(this.runtime.mem, (buf, cap) => this.runtime.fn.EPDFForm_GetFieldName(model, resolved.fieldIndex, buf, cap)) ?? "";
11950
+ const lock = fieldLockFor(protection, name);
11951
+ if (lock) throw new EngineError(EngineErrorCode.ProtectedDocument, `form field "${name}" is locked by signature ${lock.signatureIndex} (${lock.source === "fieldmdp" ? "FieldMDP" : "/Lock"})`);
8925
11952
  }
8926
11953
  /** Dispatch the typed native write. Returns the changed widget objnums. */
8927
11954
  applyWrite(fieldObjectNumber, value) {
@@ -9333,6 +12360,7 @@ function searchQueryKey(query, mode) {
9333
12360
  query.matchCase ? 1 : 0,
9334
12361
  query.matchDiacritics ? 1 : 0,
9335
12362
  query.wholeWord ? 1 : 0,
12363
+ query.ignoreWhitespace ? 1 : 0,
9336
12364
  mode
9337
12365
  ]);
9338
12366
  }
@@ -9433,7 +12461,7 @@ var SearchReader = class {
9433
12461
  const text = corpus.snapshot.text;
9434
12462
  let ranges;
9435
12463
  if (query.regex) ranges = matchRegex(text, query);
9436
- else if (query.matchCase || query.matchDiacritics) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
12464
+ else if (query.matchCase || query.matchDiacritics || query.ignoreWhitespace) ranges = matchLiteral(foldText(text, foldOptionsFor(query)), query);
9437
12465
  else ranges = matchLiteral(corpus.folded, query);
9438
12466
  if (ranges.length > 0) {
9439
12467
  const layout = buildPageTextLayout(new PageGeometryReader(this.runtime, this.session).read(pon, signal));
@@ -9531,6 +12559,100 @@ function requireAction(action, owner, index) {
9531
12559
  throw new EngineError(EngineErrorCode.MalformedPdf, `failed to read ${owner}`, { details: { index } });
9532
12560
  }
9533
12561
  //#endregion
12562
+ //#region src/features/measure/internal/readViewports.ts
12563
+ const CALIBRATION_NAME = "EmbedPDF";
12564
+ function readViewports(fn, mem, page) {
12565
+ const viewports = [];
12566
+ for (let i = 0; i < fn.EPDFPage_CountViewports(page); i++) {
12567
+ const viewport = fn.EPDFPage_GetViewport(page, i);
12568
+ if (!viewport) continue;
12569
+ const bbox = withScratch(mem, 16, (p) => fn.EPDFViewport_GetBBox(viewport, p) ? normalizePdfRect(readRectF(mem, p)) : void 0);
12570
+ if (!bbox) continue;
12571
+ const name = readUtf16String(mem, (p, n) => fn.EPDFViewport_GetName(viewport, p, n));
12572
+ const measure = readMeasure(fn, mem, fn.EPDFViewport_GetMeasure(viewport));
12573
+ viewports.push({
12574
+ bbox,
12575
+ owned: name === "EmbedPDF" && (!measure || measure.subtype === "RL"),
12576
+ ...name !== null ? { name } : {},
12577
+ ...measure ? { measure } : {}
12578
+ });
12579
+ }
12580
+ return viewports;
12581
+ }
12582
+ //#endregion
12583
+ //#region src/features/measure/MeasureReader.ts
12584
+ var MeasureReader = class {
12585
+ constructor(runtime, session) {
12586
+ this.runtime = runtime;
12587
+ this.session = session;
12588
+ }
12589
+ viewports(pon, signal) {
12590
+ throwIfAborted(signal);
12591
+ const pool = this.session.pagePool(), page = pool.acquire(pon);
12592
+ try {
12593
+ return readViewports(this.runtime.fn, this.runtime.mem, page);
12594
+ } finally {
12595
+ pool.release(pon);
12596
+ }
12597
+ }
12598
+ };
12599
+ //#endregion
12600
+ //#region src/features/measure/MeasureMutator.ts
12601
+ var MeasureMutator = class {
12602
+ constructor(runtime, session) {
12603
+ this.runtime = runtime;
12604
+ this.session = session;
12605
+ }
12606
+ setScale(pon, measure, signal) {
12607
+ throwIfAborted(signal);
12608
+ if (measure !== null) try {
12609
+ assertWritableMeasure(measure);
12610
+ } catch (e) {
12611
+ throw new EngineError(EngineErrorCode.InvalidArg, e.message);
12612
+ }
12613
+ const { fn, mem } = this.runtime, pool = this.session.pagePool(), page = pool.acquire(pon);
12614
+ try {
12615
+ const owned = [];
12616
+ for (let i = 0; i < fn.EPDFPage_CountViewports(page); i++) {
12617
+ const vp = fn.EPDFPage_GetViewport(page, i);
12618
+ if (!vp || readUtf16String(mem, (p, n) => fn.EPDFViewport_GetName(vp, p, n)) !== "EmbedPDF") continue;
12619
+ const m = fn.EPDFViewport_GetMeasure(vp);
12620
+ if (!m || fn.EPDFMeasure_GetSubtype(m) === 1) owned.push(i);
12621
+ }
12622
+ const bbox = withScratch(mem, 16, (p) => {
12623
+ const index = this.session.recordByObjectNumber(pon).pageIndex;
12624
+ const doc = this.session.requireDocPtr();
12625
+ if (!fn.EPDF_GetPageBoxByIndex(doc, index, 1, p) && !fn.EPDF_GetPageBoxByIndex(doc, index, 0, p)) throw new EngineError(EngineErrorCode.InvalidArg, "Page has no valid calibration box");
12626
+ return normalizePdfRect(readRectF(mem, p));
12627
+ });
12628
+ if (![
12629
+ bbox.left,
12630
+ bbox.right,
12631
+ bbox.bottom,
12632
+ bbox.top
12633
+ ].every(Number.isFinite)) throw new EngineError(EngineErrorCode.InvalidArg, "Invalid calibration box");
12634
+ throwIfAborted(signal);
12635
+ if (measure === null) {
12636
+ for (const i of owned.reverse()) requireMeasureWrite(fn.EPDFPage_RemoveViewport(page, i));
12637
+ return;
12638
+ }
12639
+ withScratch(mem, 16, (p) => {
12640
+ mem.poke(p, "f32", bbox.left);
12641
+ mem.poke(p, "f32", bbox.top, 4);
12642
+ mem.poke(p, "f32", bbox.right, 8);
12643
+ mem.poke(p, "f32", bbox.bottom, 12);
12644
+ const vp = owned.length ? fn.EPDFPage_GetViewport(page, owned[owned.length - 1]) : fn.EPDFPage_AddViewport(page, p);
12645
+ requireMeasureWrite(vp);
12646
+ requireMeasureWrite(fn.EPDFViewport_SetBBox(vp, p));
12647
+ requireMeasureWrite(writeUtf16String(mem, CALIBRATION_NAME, (name) => fn.EPDFViewport_SetName(vp, name)));
12648
+ writeMeasure(fn, mem, fn.EPDFViewport_AddMeasure(vp), measure);
12649
+ });
12650
+ } finally {
12651
+ pool.release(pon);
12652
+ }
12653
+ }
12654
+ };
12655
+ //#endregion
9534
12656
  //#region src/features/pieceinfo/PieceInfoAccessor.ts
9535
12657
  const OBJ_BOOLEAN = 1;
9536
12658
  const OBJ_NUMBER = 2;
@@ -9768,6 +12890,7 @@ var WorkerHost = class {
9768
12890
  this.aborts.set(msg.jobId, ctrl);
9769
12891
  let resultPack;
9770
12892
  try {
12893
+ this.assertNoPendingSigning(msg);
9771
12894
  switch (msg.kind) {
9772
12895
  case "open.fatMem":
9773
12896
  case "open.layerMemBase":
@@ -9807,6 +12930,36 @@ var WorkerHost = class {
9807
12930
  case "annotations.move":
9808
12931
  resultPack = this.handleAnnotationsMove(msg, ctrl.signal);
9809
12932
  break;
12933
+ case "signatures.list":
12934
+ resultPack = this.handleSignaturesList(msg);
12935
+ break;
12936
+ case "signatures.contents":
12937
+ resultPack = this.handleSignaturesContents(msg);
12938
+ break;
12939
+ case "signatures.digest":
12940
+ resultPack = this.handleSignaturesDigest(msg);
12941
+ break;
12942
+ case "signatures.revisionBytes":
12943
+ resultPack = this.handleSignaturesRevisionBytes(msg);
12944
+ break;
12945
+ case "document.version":
12946
+ resultPack = this.handleDocumentVersion(msg);
12947
+ break;
12948
+ case "signatures.prepare":
12949
+ resultPack = this.handleSignaturesPrepare(msg);
12950
+ break;
12951
+ case "signatures.complete":
12952
+ resultPack = this.handleSignaturesComplete(msg);
12953
+ break;
12954
+ case "signatures.abort":
12955
+ resultPack = this.handleSignaturesAbort(msg);
12956
+ break;
12957
+ case "signatures.analyze":
12958
+ resultPack = this.handleSignaturesAnalyze(msg);
12959
+ break;
12960
+ case "signatures.finalizeCandidate":
12961
+ resultPack = this.handleSignaturesFinalizeCandidate(msg);
12962
+ break;
9810
12963
  case "forms.list":
9811
12964
  resultPack = this.handleFormsList(msg, ctrl.signal);
9812
12965
  break;
@@ -9834,6 +12987,9 @@ var WorkerHost = class {
9834
12987
  case "forms.updateField":
9835
12988
  resultPack = this.handleFormsUpdateField(msg, ctrl.signal);
9836
12989
  break;
12990
+ case "forms.setSignatureAppearance":
12991
+ resultPack = this.handleFormsSetSignatureAppearance(msg, ctrl.signal);
12992
+ break;
9837
12993
  case "forms.deleteField":
9838
12994
  resultPack = this.handleFormsDeleteField(msg, ctrl.signal);
9839
12995
  break;
@@ -9897,6 +13053,12 @@ var WorkerHost = class {
9897
13053
  case "annotations.readFile":
9898
13054
  resultPack = this.handleAnnotationsReadFile(msg, ctrl.signal);
9899
13055
  break;
13056
+ case "measure.viewports":
13057
+ resultPack = this.handleMeasureViewports(msg, ctrl.signal);
13058
+ break;
13059
+ case "measure.setScale":
13060
+ resultPack = this.handleMeasureSetScale(msg, ctrl.signal);
13061
+ break;
9900
13062
  case "pieceInfo.read":
9901
13063
  resultPack = this.handlePieceInfoRead(msg, ctrl.signal);
9902
13064
  break;
@@ -9951,6 +13113,12 @@ var WorkerHost = class {
9951
13113
  case "fonts.clear":
9952
13114
  resultPack = this.handleFontsClear(msg);
9953
13115
  break;
13116
+ case "fonts.authorizeEditing":
13117
+ resultPack = this.handleFontsAuthorizeEditing(msg);
13118
+ break;
13119
+ case "document.setFontSettings":
13120
+ resultPack = this.handleDocumentSetFontSettings(msg);
13121
+ break;
9954
13122
  case "layer.close":
9955
13123
  resultPack = this.handleLayerClose(msg);
9956
13124
  break;
@@ -9982,10 +13150,13 @@ var WorkerHost = class {
9982
13150
  const key = sessionKey(req.docId, req.kind === "open.fatMem" ? void 0 : req.layerName);
9983
13151
  if (this.sessions.has(key)) throw new EngineError(EngineErrorCode.InvalidArg, `document session already open: ${key}`);
9984
13152
  const session = new DocumentSession(this.runtime);
13153
+ session.signedDocumentPolicy = req.signedDocumentPolicy ?? "protect";
13154
+ session.password = req.password;
9985
13155
  if (req.kind === "open.fatMem") {
13156
+ session.sessionKind = req.sessionKind ?? "layer";
9986
13157
  const bytes = new Uint8Array(req.bytes);
9987
13158
  try {
9988
- session.openFromHandle(openFatMemoryDocument(this.runtime, bytes, req.password));
13159
+ this.openSignedAware(session, bytes, req.password);
9989
13160
  } catch (error) {
9990
13161
  if (!isPasswordOpenError(error)) throw error;
9991
13162
  session.parkLocked(bytes);
@@ -10000,14 +13171,16 @@ var WorkerHost = class {
10000
13171
  const base = this.baseDocuments.acquireMemoryBase({
10001
13172
  key: req.baseKey,
10002
13173
  bytes: new Uint8Array(req.baseBytes),
10003
- password: req.password
13174
+ password: req.password,
13175
+ knownSha256: req.baseSha256
10004
13176
  });
10005
13177
  session.openFromHandle(openLayerDocument(this.runtime, base, req.layer, req.password));
10006
13178
  } else {
10007
13179
  const base = this.baseDocuments.acquireFileBase({
10008
13180
  key: req.baseKey,
10009
13181
  path: req.basePath,
10010
- password: req.password
13182
+ password: req.password,
13183
+ knownSha256: req.baseSha256
10011
13184
  });
10012
13185
  session.openFromHandle(openLayerDocument(this.runtime, base, req.layer, req.password));
10013
13186
  }
@@ -10015,9 +13188,160 @@ var WorkerHost = class {
10015
13188
  return wirePack({
10016
13189
  tag: "open",
10017
13190
  docId: req.docId,
10018
- security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password ?? "")
13191
+ security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password ?? ""),
13192
+ protection: this.probeProtection(session)
13193
+ });
13194
+ }
13195
+ /**
13196
+ * Open plain bytes into `session` in the shape it asked for. The default,
13197
+ * `layer`, makes the bytes an immutable base with a fresh layer on top:
13198
+ * reads fall through to the base, a write promotes only what it touches,
13199
+ * a save appends exactly that, and a signature keeps its validity across
13200
+ * later edits because the signature dictionary is never rewritten. The
13201
+ * `plain` shape keeps one in-memory document — unless the bytes already
13202
+ * carry a signature value, which always opens as a layer, because a plain
13203
+ * save rewrites every loaded object into the new revision and strict
13204
+ * validators reject that. Auto-layered sessions serialize no artifact per
13205
+ * mutation: the caller never asked for one.
13206
+ */
13207
+ openSignedAware(session, bytes, password) {
13208
+ if (session.sessionKind === "plain") {
13209
+ const plain = openFatMemoryDocument(this.runtime, bytes, password);
13210
+ let signed = false;
13211
+ try {
13212
+ signed = hasSignedSignature(this.runtime, plain.docPtr);
13213
+ } catch {
13214
+ signed = false;
13215
+ }
13216
+ if (!signed) {
13217
+ session.openFromHandle(plain);
13218
+ return;
13219
+ }
13220
+ plain.close();
13221
+ }
13222
+ const base = this.baseDocuments.acquireMemoryBase({
13223
+ key: `signed-open:${generateUuid()}`,
13224
+ bytes,
13225
+ password
13226
+ });
13227
+ session.openFromHandle(openLayerDocument(this.runtime, base, { kind: "fresh" }, password));
13228
+ session.persistLayerArtifact = false;
13229
+ }
13230
+ /**
13231
+ * What the document's signatures forbid, read at open and after an
13232
+ * unlock so the main-thread guard can subtract capabilities before the
13233
+ * first mutation. A document whose signature model cannot be built
13234
+ * still opens: protection is then `null` (nothing is known to be
13235
+ * signed), and the reads report the failure on their own.
13236
+ */
13237
+ probeProtection(session) {
13238
+ try {
13239
+ return new SignatureReader(this.runtime, session).readProtection();
13240
+ } catch {
13241
+ return null;
13242
+ }
13243
+ }
13244
+ handleSignaturesList(req) {
13245
+ const session = this.requireSession(req);
13246
+ return wirePack({
13247
+ tag: "signatures.list",
13248
+ snapshot: req.workingCopy ? new SignatureAnalyzer(this.runtime, session).readWorkingCopySnapshot() : new SignatureReader(this.runtime, session).readSnapshot()
13249
+ });
13250
+ }
13251
+ handleSignaturesContents(req) {
13252
+ const session = this.requireSession(req);
13253
+ const bytes = new SignatureReader(this.runtime, session).readContents(req.ref);
13254
+ return wirePack({
13255
+ tag: "signatures.contents",
13256
+ bytes
13257
+ }, [bytes]);
13258
+ }
13259
+ handleSignaturesDigest(req) {
13260
+ const session = this.requireSession(req);
13261
+ const digest = new SignatureReader(this.runtime, session).digest(req.ref, req.algorithm);
13262
+ return wirePack({
13263
+ tag: "signatures.digest",
13264
+ digest
13265
+ }, [digest]);
13266
+ }
13267
+ handleSignaturesRevisionBytes(req) {
13268
+ const session = this.requireSession(req);
13269
+ const bytes = new SignatureReader(this.runtime, session).revisionBytes(req.revisionIndex);
13270
+ return wirePack({
13271
+ tag: "signatures.revisionBytes",
13272
+ bytes,
13273
+ size: bytes.byteLength
13274
+ }, [bytes]);
13275
+ }
13276
+ handleDocumentVersion(req) {
13277
+ const session = this.requireSession(req);
13278
+ return wirePack({
13279
+ tag: "document.version",
13280
+ version: new SignatureReader(this.runtime, session).version()
13281
+ });
13282
+ }
13283
+ handleSignaturesPrepare(req) {
13284
+ const session = this.requireSession(req);
13285
+ return wirePack({
13286
+ tag: "signatures.prepare",
13287
+ result: new SignatureMutator(this.runtime, session, this.baseDocuments, this.options.signingCandidatePath).prepare(req.input)
13288
+ });
13289
+ }
13290
+ handleSignaturesComplete(req) {
13291
+ const session = this.requireSession(req);
13292
+ const result = new SignatureMutator(this.runtime, session, this.baseDocuments, this.options.signingCandidatePath).complete(req.input);
13293
+ if (result.status === "already-completed") return wirePack({
13294
+ tag: "signatures.complete",
13295
+ result
13296
+ });
13297
+ return this.finishMutation(session, {
13298
+ tag: "signatures.complete",
13299
+ result
13300
+ }, req.artifactPath);
13301
+ }
13302
+ handleSignaturesAnalyze(req) {
13303
+ const session = this.requireSession(req);
13304
+ return wirePack({
13305
+ tag: "signatures.analyze",
13306
+ analysis: new SignatureAnalyzer(this.runtime, session).analyze(req.input)
13307
+ });
13308
+ }
13309
+ /**
13310
+ * Session-less, like `document.renderPageFile`: the candidate file is
13311
+ * the caller's, patched in place and read back through a transient
13312
+ * session that is never stored in `this.sessions`.
13313
+ */
13314
+ handleSignaturesFinalizeCandidate(req) {
13315
+ return wirePack({
13316
+ tag: "signatures.finalizeCandidate",
13317
+ ...new CandidateFinalizer(this.runtime, this.baseDocuments).finalize({
13318
+ path: req.path,
13319
+ byteRange: req.byteRange,
13320
+ contentsSize: req.contentsSize,
13321
+ fieldObjectNumber: req.fieldObjectNumber,
13322
+ cms: new Uint8Array(req.cms),
13323
+ password: req.password ?? null
13324
+ })
13325
+ });
13326
+ }
13327
+ handleSignaturesAbort(req) {
13328
+ const session = this.requireSession(req);
13329
+ return wirePack({
13330
+ tag: "signatures.abort",
13331
+ result: new SignatureMutator(this.runtime, session, this.baseDocuments, this.options.signingCandidatePath).abort(req.signingId)
10019
13332
  });
10020
13333
  }
13334
+ /**
13335
+ * The dispatch fence for a parked signing candidate. Only mutating kinds
13336
+ * are refused; a request for a session that is not open falls through
13337
+ * to its handler's own `DocNotOpen`.
13338
+ */
13339
+ assertNoPendingSigning(msg) {
13340
+ if (!MUTATING_KINDS.has(msg.kind) || !("docId" in msg)) return;
13341
+ const layerName = "layerName" in msg ? msg.layerName : void 0;
13342
+ const session = this.sessions.get(sessionKey(msg.docId, layerName));
13343
+ if (session?.pendingSigning) throw new EngineError(EngineErrorCode.SigningPending, `a signing is pending (${session.pendingSigning.prepared.signingId}); complete or abort it before mutating the document`);
13344
+ }
10021
13345
  handleMetadataRead(req, signal) {
10022
13346
  const session = this.requireSession(req);
10023
13347
  return wirePack({
@@ -10044,21 +13368,21 @@ var WorkerHost = class {
10044
13368
  const session = this.requireSession(req);
10045
13369
  return wirePack({
10046
13370
  tag: "annotations.listRawAll",
10047
- snapshot: new RawAnnotationReader(this.runtime, session).listAll(signal)
13371
+ snapshot: new RawAnnotationReader(this.runtime, session, this.fonts).listAll(signal)
10048
13372
  });
10049
13373
  }
10050
13374
  handleAnnotationsListRawPage(req, signal) {
10051
13375
  const session = this.requireSession(req);
10052
13376
  return wirePack({
10053
13377
  tag: "annotations.listRawPage",
10054
- snapshot: new RawAnnotationReader(this.runtime, session).listOne(req.pageObjectNumber, signal)
13378
+ snapshot: new RawAnnotationReader(this.runtime, session, this.fonts).listOne(req.pageObjectNumber, signal)
10055
13379
  });
10056
13380
  }
10057
13381
  handleAnnotationsListFullPage(req, signal) {
10058
13382
  const session = this.requireSession(req);
10059
13383
  return wirePack({
10060
13384
  tag: "annotations.listFullPage",
10061
- snapshot: new AnnotationReader(this.runtime, session).list(req.pageObjectNumber, signal)
13385
+ snapshot: new AnnotationReader(this.runtime, session, this.fonts).list(req.pageObjectNumber, signal)
10062
13386
  });
10063
13387
  }
10064
13388
  handleAnnotationsRenderAppearances(req, signal) {
@@ -10265,6 +13589,27 @@ var WorkerHost = class {
10265
13589
  content
10266
13590
  }, content.bytes !== void 0 ? [content.bytes] : []);
10267
13591
  }
13592
+ handleMeasureViewports(req, signal) {
13593
+ const session = this.requireSession(req);
13594
+ return wirePack({
13595
+ tag: "measure.viewports",
13596
+ viewports: new MeasureReader(this.runtime, session).viewports(req.pageObjectNumber, signal)
13597
+ });
13598
+ }
13599
+ handleMeasureSetScale(req, signal) {
13600
+ const session = this.requireSession(req);
13601
+ new MeasureMutator(this.runtime, session).setScale(req.pageObjectNumber, req.measure, signal);
13602
+ return this.finishMutation(session, {
13603
+ tag: "measure.setScale",
13604
+ result: {
13605
+ pageObjectNumber: req.pageObjectNumber,
13606
+ meta: {
13607
+ affectedPages: [],
13608
+ cacheDelta: null
13609
+ }
13610
+ }
13611
+ }, req.artifactPath);
13612
+ }
10268
13613
  handlePieceInfoRead(req, signal) {
10269
13614
  const session = this.requireSession(req);
10270
13615
  return wirePack({
@@ -10419,6 +13764,14 @@ var WorkerHost = class {
10419
13764
  }
10420
13765
  handleDocumentSaveBuffer(req) {
10421
13766
  const session = this.requireSession(req);
13767
+ if (req.mode === "incremental") {
13768
+ const snap = new DocumentSaver(this.runtime, session).snapshot();
13769
+ return wirePack({
13770
+ tag: "document.saveBuffer",
13771
+ bytes: snap.bytes,
13772
+ size: snap.bytes.byteLength
13773
+ }, [snap.bytes]);
13774
+ }
10422
13775
  const saved = new DocumentSaver(this.runtime, session).saveStandaloneToBuffer(req.mode);
10423
13776
  return wirePack({
10424
13777
  tag: "document.saveBuffer",
@@ -10428,6 +13781,17 @@ var WorkerHost = class {
10428
13781
  }
10429
13782
  handleDocumentSaveFile(req) {
10430
13783
  const session = this.requireSession(req);
13784
+ if (req.mode === "incremental" && (!session.hasUnsavedEdits() || new DocumentSaver(this.runtime, session).saveStandaloneToFileEx(req.path, req.mode).unchangedSinceLoad)) {
13785
+ new DocumentSaver(this.runtime, session).copyLoadedBytesToFile(req.path);
13786
+ return wirePack({
13787
+ tag: "document.saveFile",
13788
+ path: req.path
13789
+ });
13790
+ }
13791
+ if (req.mode === "incremental") return wirePack({
13792
+ tag: "document.saveFile",
13793
+ path: req.path
13794
+ });
10431
13795
  return wirePack({
10432
13796
  tag: "document.saveFile",
10433
13797
  path: new DocumentSaver(this.runtime, session).saveStandaloneToFile(req.path, req.mode).path
@@ -10484,21 +13848,47 @@ var WorkerHost = class {
10484
13848
  const parked = this.sessions.get(sessionKey(req.docId));
10485
13849
  let session;
10486
13850
  if (parked?.isLocked()) {
10487
- parked.openFromHandle(openFatMemoryDocument(this.runtime, parked.lockedBytes(), req.password));
13851
+ this.openSignedAware(parked, parked.lockedBytes(), req.password);
13852
+ parked.password = req.password;
10488
13853
  session = parked;
10489
13854
  } else session = this.requireSession(req);
10490
13855
  return wirePack({
10491
13856
  tag: "document.checkPasswordPermissions",
10492
- security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password, req.mode ?? "any")
13857
+ security: new SecurityReader(this.runtime).checkPasswordPermissions(session, req.password, req.mode ?? "any"),
13858
+ protection: this.probeProtection(session)
10493
13859
  });
10494
13860
  }
10495
13861
  handleFontsRegister(req) {
10496
13862
  this.fonts.register(req.fontKey, req.familyName, req.weight, req.italic, new Uint8Array(req.data));
10497
13863
  return wirePack({
10498
13864
  tag: "fonts.register",
10499
- fontKey: req.fontKey
13865
+ fontKey: req.fontKey,
13866
+ identity: this.fonts.describe(req.fontKey)
13867
+ });
13868
+ }
13869
+ handleFontsAuthorizeEditing(req) {
13870
+ return wirePack({
13871
+ tag: "fonts.authorizeEditing",
13872
+ identity: this.fonts.authorizeEditing(req.fontKey)
10500
13873
  });
10501
13874
  }
13875
+ /** Per-document font and text-layout settings (session state). */
13876
+ handleDocumentSetFontSettings(req) {
13877
+ const docPtr = this.requireSession(req).requireDocPtr();
13878
+ const { fn } = this.runtime;
13879
+ if (req.embeddingPolicy !== void 0) {
13880
+ const code = {
13881
+ default: 0,
13882
+ subset: 1,
13883
+ full: 2
13884
+ }[req.embeddingPolicy];
13885
+ if (!fn.EPDFDoc_SetFontEmbeddingPolicy(docPtr, code)) throw new EngineError(EngineErrorCode.InvalidArg, "EPDFDoc_SetFontEmbeddingPolicy failed");
13886
+ }
13887
+ if (req.typographicFeatures !== void 0) {
13888
+ if (!fn.EPDFDoc_SetTypographicFeatures(docPtr, req.typographicFeatures)) throw new EngineError(EngineErrorCode.InvalidArg, "EPDFDoc_SetTypographicFeatures failed");
13889
+ }
13890
+ return wirePack({ tag: "document.setFontSettings" });
13891
+ }
10502
13892
  handleFontsAddFallback(req) {
10503
13893
  this.fonts.addFallback(req.fontKey);
10504
13894
  return wirePack({ tag: "fonts.addFallback" });
@@ -10524,6 +13914,7 @@ var WorkerHost = class {
10524
13914
  const session = this.sessions.get(key);
10525
13915
  if (session) {
10526
13916
  disposeFormModel(this.runtime, session);
13917
+ disposeSignatureModel(this.runtime, session);
10527
13918
  session.close();
10528
13919
  this.sessions.delete(key);
10529
13920
  }
@@ -10533,6 +13924,7 @@ var WorkerHost = class {
10533
13924
  for (const [key, session] of Array.from(this.sessions.entries())) {
10534
13925
  if (!sessionKeyBelongsToDoc(key, req.docId)) continue;
10535
13926
  disposeFormModel(this.runtime, session);
13927
+ disposeSignatureModel(this.runtime, session);
10536
13928
  session.close();
10537
13929
  this.sessions.delete(key);
10538
13930
  }
@@ -10543,6 +13935,7 @@ var WorkerHost = class {
10543
13935
  this.destroyed = true;
10544
13936
  for (const session of this.sessions.values()) {
10545
13937
  disposeFormModel(this.runtime, session);
13938
+ disposeSignatureModel(this.runtime, session);
10546
13939
  session.close();
10547
13940
  }
10548
13941
  this.sessions.clear();
@@ -10648,6 +14041,21 @@ var WorkerHost = class {
10648
14041
  }
10649
14042
  }, req.artifactPath);
10650
14043
  }
14044
+ handleFormsSetSignatureAppearance(req, signal) {
14045
+ const session = this.requireSession(req);
14046
+ const { field, pages } = new FormMutator(this.runtime, session).setSignatureAppearance(req.ref, new Uint8Array(req.pdf), req.pageIndex, signal);
14047
+ const meta = {
14048
+ affectedPages: pages.map((pon) => session.pageState(pon)),
14049
+ cacheDelta: null
14050
+ };
14051
+ return this.finishMutation(session, {
14052
+ tag: "forms.setSignatureAppearance",
14053
+ result: {
14054
+ field,
14055
+ meta
14056
+ }
14057
+ }, req.artifactPath);
14058
+ }
10651
14059
  handleFormsDeleteField(req, signal) {
10652
14060
  const session = this.requireSession(req);
10653
14061
  const { deletedFieldObjectNumber, detachedWidgets } = new FormMutator(this.runtime, session).deleteField(req.ref, signal);
@@ -10693,8 +14101,8 @@ var WorkerHost = class {
10693
14101
  }, req.artifactPath);
10694
14102
  }
10695
14103
  finishMutation(session, payload, artifactPath) {
10696
- if (!payload.tag.startsWith("forms.") && payload.tag !== "pages.flatten" && payload.tag !== "redaction.apply") session.noteMutation();
10697
- if (session.kind !== "layer") return wirePack(payload);
14104
+ if (!payload.tag.startsWith("forms.") && payload.tag !== "pages.flatten" && payload.tag !== "redaction.apply" && payload.tag !== "signatures.complete") session.noteMutation();
14105
+ if (session.kind !== "layer" || !session.persistLayerArtifact) return wirePack(payload);
10698
14106
  const saved = this.saveLayerArtifact(session, artifactPath);
10699
14107
  return wirePack({
10700
14108
  ...payload,
@@ -10720,6 +14128,40 @@ const EMPTY_FORM_META = {
10720
14128
  cacheDelta: null
10721
14129
  };
10722
14130
  const BASE_SESSION_SUFFIX = "__base__";
14131
+ /** Every request kind that writes to a session's document. */
14132
+ const MUTATING_KINDS = /* @__PURE__ */ new Set([
14133
+ "metadata.update",
14134
+ "annotations.create",
14135
+ "annotations.update",
14136
+ "annotations.delete",
14137
+ "annotations.move",
14138
+ "annotations.flatten",
14139
+ "forms.setValue",
14140
+ "forms.reset",
14141
+ "forms.applyEffects",
14142
+ "forms.import",
14143
+ "forms.repair",
14144
+ "forms.createField",
14145
+ "forms.updateField",
14146
+ "forms.setSignatureAppearance",
14147
+ "forms.deleteField",
14148
+ "forms.attachWidget",
14149
+ "forms.detachWidget",
14150
+ "pages.move",
14151
+ "pages.rotate",
14152
+ "pages.delete",
14153
+ "pages.setName",
14154
+ "pages.removeName",
14155
+ "pages.flatten",
14156
+ "pages.insert",
14157
+ "pages.insertBlank",
14158
+ "redaction.apply",
14159
+ "attachments.create",
14160
+ "attachments.delete",
14161
+ "measure.setScale",
14162
+ "pieceInfo.update",
14163
+ "pieceInfo.clear"
14164
+ ]);
10723
14165
  function sessionKey(docId, layerName) {
10724
14166
  return `${docId}::${layerName ? `layer:${layerName}` : BASE_SESSION_SUFFIX}`;
10725
14167
  }
@@ -10748,6 +14190,6 @@ function passwordRequiredProbe() {
10748
14190
  };
10749
14191
  }
10750
14192
  //#endregion
10751
- export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationFlattener, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CloseStack, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EPDF_CHAR_GEOMETRY_LAYOUT, EventHub, FontRegistrar, FormMutator, FormReader, FormsEffectsApplier, LocalRevisionAuthority, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SessionEventPublisher, WorkerHost, buildRunsFromRawGlyphs, computeMutationImpact, destroyLibrary, disposeFormModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, throwIfAborted, writeAttachmentFilePayload };
14193
+ export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationFlattener, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CandidateFinalizer, CloseStack, DIGEST_CODE, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EPDF_CHAR_GEOMETRY_LAYOUT, EventHub, FontRegistrar, FormMutator, FormReader, FormsEffectsApplier, LocalRevisionAuthority, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SessionEventPublisher, SignatureAnalyzer, SignatureMutator, SignatureReader, WorkerHost, acquireSignatureModel, buildRunsFromRawGlyphs, computeMutationImpact, destroyLibrary, disposeFormModel, disposeSignatureModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, hasSignedSignature, normalizeU32, openFatMemoryDocument, openLayerDocument, pdfDateToIso, readActionModel, readAttachmentFileInfo, readRevisions, readSignaturesFromModel, readStructure, throwIfAborted, writeAttachmentFilePayload };
10752
14194
 
10753
14195
  //# sourceMappingURL=index.js.map