@embedpdf/engine-services 3.0.0-next.15 → 3.0.0-next.16

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 { ANNOTATION_DEFAULTS, ANNOTATION_RESOURCE_ROLES, AbortError, DEFAULT_BUNDLE_LIMITS, EMPTY_TRANSFER, EdgeResolver, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, OBJECT_NUMBER_CEILING, OBJECT_NUMBER_ISSUE_LIMIT, PAGE_INSERT_BLANK_MAX_COUNT, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, PermissionDenied, SIGNATURE_POLICY_VERSION, USAGE_INCOMPLETE, WIDGET_ACTION_EVENTS, allowsCapability, allowsFieldWrite, allowsSomeFieldWrite, anchorOf, annotationDefaultsOf, annotationKey, annotationWriteCapabilities, annotationWriteCapability, appearanceImpactOf, appearanceModesOf, assertAnnotationBundleManifest, assertAnnotationResources, assertFormBundleManifest, assertWithinLimit, assertWritableMeasure, assessmentOf, authorizeAnnotationCreate, authorizeAnnotationDelete, authorizeAnnotationUpdate, authorizeCapability, authorizeFieldGroup, authorizeFieldWrite, authorizeUnprotected, buildSnippet, changeFingerprint, charMapViolation, charRangeForTextOffsets, closeExportSelection, colorOf, combine, concatAnnotationLists, createPdfTextLayout, decodeSubmitFormFlags, defaultEntryOfDraft, deletedWith, deriveProtection, deserializeError, draftWritesScripts, encodePageKey, evaluateStep, faceForFreeTextFont, familyKey, fieldAfterReset, fieldAfterValue, fieldLockFor, fieldScriptOf, fieldValueOf, fieldValueWriteOf, fitWidgetToFamily, foldOptionsFor, foldText, formExportRowsOf, formFieldAfter, formWidget, freeTextFontForFace, generateUuid, generateUuidV7, isAtDefault, isDimension, isFieldScript, isKeptRefusal, isNoOpWrite, isSamePdfRect, isSkippedItem, isUndoChange, isValidPageObjectNumber, itemWrote, manifestBytesOf, mapDocumentActions, matchLiteral, matchRegex, normalizePdfQuad, normalizePdfRect, pageAnnotationOf, pageAppearancesOf, pageBoxOf, pageBoxesOf, pageDestinationOf, pageFormFieldOf, pageFormSnapshotOf, pageGeometryOf, pageListOf, pageRefsIn, pageRotationOf, pageSearchSliceOf, pageViewportsOf, pageWidgetOf, parsePdfValue, pdfAnnotationDraftOf, pdfAnnotationPatchOf, pdfAppearanceTurnOf, pdfFormFieldDraftOf, pdfMeasureOf, pdfPointTurned, pdfPointUnturned, pdfPointsBounds, pdfQuadBounds, pdfQuarterTurnBox, pdfRectSize, pdfRectTurnedBounds, pdfRenderTargetOf, pdfTurnOfUpright, pdfWidgetPlacementOf, placedBefore, placedWidgetOf, quarterTurnOf, renderMatrix, renderSize, reorderedList, resolvePageLayers, restrictionsFor, rgbOf, sameEffectiveValue, searchQueryOf, semanticEqual, serializeError, sniffBinaryMetadata, subtypeFromCode, toAttachmentRef, toPageRef, topRowOf, touchesCaption, validateSearchQuery, valueEntriesEqual, widgetActionsOf, wirePack, writesScripts } from "@embedpdf/engine-core/runtime";
1
+ import { ANNOTATION_DEFAULTS, ANNOTATION_RESOURCE_ROLES, AbortError, DEFAULT_BUNDLE_LIMITS, EMPTY_TRANSFER, EdgeResolver, EngineError, EngineErrorCode, NO_ANNOTATION_FLAGS, OBJECT_NUMBER_CEILING, OBJECT_NUMBER_ISSUE_LIMIT, OUTLINE_READ_LIMITS, PAGE_INSERT_BLANK_MAX_COUNT, PDF_SUBTYPE_TO_CODE, PdfAnnotationSubtypeCode, PermissionDenied, SIGNATURE_POLICY_VERSION, USAGE_INCOMPLETE, WIDGET_ACTION_EVENTS, allowsCapability, allowsFieldWrite, allowsSomeFieldWrite, anchorOf, annotationDefaultsOf, annotationKey, annotationWriteCapabilities, annotationWriteCapability, appearanceImpactOf, appearanceModesOf, assertAnnotationBundleManifest, assertAnnotationResources, assertFormBundleManifest, assertWithinLimit, assertWritableMeasure, assessmentOf, authorizeAnnotationCreate, authorizeAnnotationDelete, authorizeAnnotationUpdate, authorizeCapability, authorizeFieldGroup, authorizeFieldWrite, authorizeUnprotected, buildSnippet, changeFingerprint, charMapViolation, charRangeForTextOffsets, checkRenderUsage, closeExportSelection, colorOf, combine, concatAnnotationLists, createPdfTextLayout, decodeSubmitFormFlags, defaultEntryOfDraft, deletedWith, deriveProtection, deserializeError, draftWritesScripts, encodePageKey, evaluateStep, faceForFreeTextFont, familyKey, fieldAfterReset, fieldAfterValue, fieldLockFor, fieldScriptOf, fieldValueOf, fieldValueWriteOf, fitWidgetToFamily, foldOptionsFor, foldText, formExportRowsOf, formFieldAfter, formWidget, freeTextFontForFace, generateUuid, generateUuidV7, isAtDefault, isDimension, isFieldScript, isKeptRefusal, isNoOpWrite, isSamePdfRect, isSkippedItem, isUndoChange, isValidPageObjectNumber, itemWrote, manifestBytesOf, mapDocumentActions, matchLiteral, matchRegex, normalizePdfQuad, normalizePdfRect, pageAnnotationOf, pageAppearancesOf, pageBoxOf, pageBoxesOf, pageDestinationOf, pageFormFieldOf, pageFormSnapshotOf, pageGeometryOf, pageListOf, pageOutlineItemOf, pageOutlineOf, pageRefsIn, pageRotationOf, pageSearchSliceOf, pageViewportsOf, pageWidgetOf, parsePdfValue, pdfAnnotationDraftOf, pdfAnnotationPatchOf, pdfAppearanceTurnOf, pdfFormFieldDraftOf, pdfMeasureOf, pdfOutlineDraftOf, pdfOutlinePatchOf, pdfPointTurned, pdfPointUnturned, pdfPointsBounds, pdfQuadBounds, pdfQuarterTurnBox, pdfRectSize, pdfRectTurnedBounds, pdfRenderTargetOf, pdfTurnOfUpright, pdfWidgetPlacementOf, placedBefore, placedWidgetOf, positionIndex, quarterTurnOf, renderMatrix, renderSize, reorderedList, resolvePageLayers, restrictionsFor, rgbOf, sameEffectiveValue, searchQueryOf, semanticEqual, serializeError, sniffBinaryMetadata, subtypeFromCode, toAttachmentRef, toOutlineItemRef, toPageRef, topRowOf, touchesCaption, validateSearchQuery, valueEntriesEqual, widgetActionsOf, wirePack, writesScripts } from "@embedpdf/engine-core/runtime";
2
2
  import { NULL_PTR } from "@embedpdf/engine-runtime";
3
3
  import { pdfResolveAnnotationDraft, pdfResolveAnnotationPatch, planAnnotationImport, planFormImport, planFormValuesImport } from "@embedpdf/engine-core/validation";
4
4
  //#region src/runtime/lifecycle/bootstrap.ts
@@ -1263,7 +1263,7 @@ var DocumentSession = class {
1263
1263
  * Nothing else creates objects at held numbers, so a held number is free
1264
1264
  * otherwise; the fork checks that again as it creates.
1265
1265
  */
1266
- useObjectNumber(objectNumber) {
1266
+ claimObjectNumber(objectNumber) {
1267
1267
  if (!Number.isInteger(objectNumber) || objectNumber < 1) throw new EngineError(EngineErrorCode.InvalidArg, `${objectNumber} is not an object number`, { details: { field: "objectNumber" } });
1268
1268
  if (this.objectNumberAuthority === "session" && !this.heldObjectNumbers.has(objectNumber)) throw objectNumberUnavailable(objectNumber, "not-held");
1269
1269
  if (this.objectNumbersInUse.includes(objectNumber)) throw objectNumberUnavailable(objectNumber, "taken");
@@ -2419,6 +2419,10 @@ function readBoxes(fn, mem, docPtr, index, rectPtr) {
2419
2419
  art: read(BOX_ART)
2420
2420
  });
2421
2421
  }
2422
+ /** The turn of the page at `index` as it reads: its own `/Rotate`, or the one it inherits. */
2423
+ function rotationAt(runtime, docPtr, index) {
2424
+ return withScratch(runtime.mem, 4, (intPtr) => readRotation(runtime.fn, runtime.mem, docPtr, index, intPtr));
2425
+ }
2422
2426
  /** The page's turn: its `/Rotate` as written, read by the rules of `pageRotationOf`. */
2423
2427
  function readRotation(fn, mem, docPtr, index, intPtr) {
2424
2428
  if (!fn.EPDF_GetPageRotateByIndex(docPtr, index, intPtr)) return 0;
@@ -2697,12 +2701,18 @@ var PagesMutator = class {
2697
2701
  //#endregion
2698
2702
  //#region src/features/pages/PagesExtractor.ts
2699
2703
  const FPDF_NO_INCREMENTAL$3 = 2;
2704
+ const FLAT_NORMALDISPLAY = 0;
2705
+ const FLAT_PRINT = 1;
2706
+ const EXPORT_FLATTENED_ANNOTATIONS = 1;
2707
+ const EXPORT_FLATTENED_FORM_FIELDS = 2;
2700
2708
  /**
2701
2709
  * Export a subset of pages as a standalone PDF. A read over the session:
2702
- * `FPDF_ImportPagesByIndex` copies pages into a scratch document, so the
2703
- * source is untouched — no registry refresh, no layer
2704
- * artifact. Lives next to `PagesReader`/`PagesMutator` (one file per page
2705
- * verb family) so both worker hosts share the code path.
2710
+ * the pages are copied into a scratch document, so the source is untouched —
2711
+ * no registry refresh, no layer artifact. A plain copy is
2712
+ * `FPDF_ImportPagesByIndex`; a flattened one `EPDFDoc_ExportPagesFlattened`,
2713
+ * which copies the pages as they draw. Either keeps the document's title.
2714
+ * Lives next to `PagesReader`/`PagesMutator` (one file per page verb family)
2715
+ * so both worker hosts share the code path.
2706
2716
  */
2707
2717
  var PagesExtractor = class {
2708
2718
  constructor(runtime, session) {
@@ -2711,11 +2721,13 @@ var PagesExtractor = class {
2711
2721
  }
2712
2722
  /**
2713
2723
  * Copy the given pages, in the supplied caller order, into a new
2714
- * document and serialize it. Validation up front: non-empty, no
2715
- * duplicates, every page object number resolvable (`NotFound` from the session on a
2716
- * bad one). The scratch document is always closed, success or throw.
2724
+ * document and serialize it: as they are, or, with `flatten`, as they draw
2725
+ * (checked against the caller's rights before it gets here). Validation up
2726
+ * front: non-empty, no duplicates, every page object number resolvable
2727
+ * (`NotFound` from the session on a bad one). The scratch document is
2728
+ * always closed, success or throw.
2717
2729
  */
2718
- extract(pages, signal) {
2730
+ extract(pages, flatten, signal) {
2719
2731
  throwIfAborted(signal);
2720
2732
  if (pages.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "pages.extract requires at least one page");
2721
2733
  const pageObjectNumbers = this.session.resolvePageRefs(pages);
@@ -2727,14 +2739,20 @@ var PagesExtractor = class {
2727
2739
  const indices = pageObjectNumbers.map((pageObjectNumber) => this.session.recordByObjectNumber(pageObjectNumber).pageIndex);
2728
2740
  const { fn, mem } = this.runtime;
2729
2741
  const srcPtr = this.session.requireDocPtr();
2730
- const destPtr = fn.FPDF_CreateNewDocument();
2731
- if (!destPtr) throw new EngineError(EngineErrorCode.Unknown, "FPDF_CreateNewDocument failed");
2732
2742
  const idxPtr = mem.alloc(4 * indices.length);
2743
+ for (let i = 0; i < indices.length; i++) mem.poke(idxPtr, "i32", indices[i], 4 * i);
2744
+ let destPtr;
2745
+ try {
2746
+ destPtr = flatten ? this.copyFlattened(srcPtr, idxPtr, indices, flatten) : this.copyPlain(srcPtr, idxPtr, indices);
2747
+ } catch (error) {
2748
+ mem.free(idxPtr);
2749
+ throw error;
2750
+ }
2733
2751
  const sizePtr = mem.alloc(8);
2734
2752
  let pdfPtr = null;
2735
2753
  try {
2736
- for (let i = 0; i < indices.length; i++) mem.poke(idxPtr, "i32", indices[i], 4 * i);
2737
- if (!fn.FPDF_ImportPagesByIndex(destPtr, srcPtr, idxPtr, indices.length, 0)) throw new EngineError(EngineErrorCode.Unknown, `FPDF_ImportPagesByIndex rejected the request (indices=[${indices.join(",")}])`);
2754
+ const title = readMetaText(fn, mem, srcPtr, "Title");
2755
+ if (title !== null) writeMetaText(fn, mem, destPtr, "Title", title);
2738
2756
  pokeU64(mem, sizePtr, 0);
2739
2757
  pdfPtr = fn.EPDF_SaveDocumentToOwnedBuffer(destPtr, FPDF_NO_INCREMENTAL$3, sizePtr);
2740
2758
  const size = Number(mem.peek(sizePtr, "i32"));
@@ -2753,6 +2771,28 @@ var PagesExtractor = class {
2753
2771
  fn.FPDF_CloseDocument(destPtr);
2754
2772
  }
2755
2773
  }
2774
+ /** A new document with copies of the pages at `indices`, as they are. */
2775
+ copyPlain(srcPtr, idxPtr, indices) {
2776
+ const { fn } = this.runtime;
2777
+ const destPtr = fn.FPDF_CreateNewDocument();
2778
+ if (!destPtr) throw new EngineError(EngineErrorCode.Unknown, "FPDF_CreateNewDocument failed");
2779
+ if (!fn.FPDF_ImportPagesByIndex(destPtr, srcPtr, idxPtr, indices.length, 0)) {
2780
+ fn.FPDF_CloseDocument(destPtr);
2781
+ throw new EngineError(EngineErrorCode.Unknown, `FPDF_ImportPagesByIndex rejected the request (indices=[${indices.join(",")}])`);
2782
+ }
2783
+ return destPtr;
2784
+ }
2785
+ /**
2786
+ * A new document with the pages at `indices` as they draw for
2787
+ * `flatten.usage`: what `flatten` includes painted in, nothing interactive
2788
+ * left, layers in their state for the usage.
2789
+ */
2790
+ copyFlattened(srcPtr, idxPtr, indices, flatten) {
2791
+ const flags = (flatten.includeAnnotations ? EXPORT_FLATTENED_ANNOTATIONS : 0) | (flatten.includeFormFields ? EXPORT_FLATTENED_FORM_FIELDS : 0);
2792
+ const destPtr = this.runtime.fn.EPDFDoc_ExportPagesFlattened(srcPtr, idxPtr, indices.length, flatten.usage === "print" ? FLAT_PRINT : FLAT_NORMALDISPLAY, flags);
2793
+ if (!destPtr) throw new EngineError(EngineErrorCode.Unknown, `EPDFDoc_ExportPagesFlattened rejected the request (indices=[${indices.join(",")}])`);
2794
+ return destPtr;
2795
+ }
2756
2796
  };
2757
2797
  //#endregion
2758
2798
  //#region src/features/annotations/internal/write/promoteInlineAnnotations.ts
@@ -2851,7 +2891,7 @@ var PagesInserter = class {
2851
2891
  const destPtr = this.session.requireDocPtr();
2852
2892
  const at = pageIndexAt(this.session, position, fn.FPDF_GetPageCount(destPtr));
2853
2893
  if (objectNumbers && objectNumbers.length !== count) throw new EngineError(EngineErrorCode.InvalidArg, `pages.insertBlank needs one object number per page: ${objectNumbers.length} for ${count}`, { details: { field: "objectNumbers" } });
2854
- for (const objectNumber of objectNumbers ?? []) this.session.useObjectNumber(objectNumber);
2894
+ for (const objectNumber of objectNumbers ?? []) this.session.claimObjectNumber(objectNumber);
2855
2895
  for (let i = 0; i < count; i++) {
2856
2896
  const objectNumber = objectNumbers?.[i];
2857
2897
  if (!fn.EPDFPage_InsertBlankRaw(destPtr, at + i, size.width, size.height, objectNumber ?? 0)) {
@@ -3873,6 +3913,11 @@ function toExactArrayBuffer(bytes) {
3873
3913
  const FPDF_RENDER_ANNOT = 1;
3874
3914
  /** `EPDF_RENDER_WIDGETS`: the form fields' widgets. */
3875
3915
  const EPDF_RENDER_WIDGETS = 32768;
3916
+ /**
3917
+ * `FPDF_PRINTING`: the print rules. Layers take their print state, and
3918
+ * annotations and widgets draw by their `print` flag (no-view ones too).
3919
+ */
3920
+ const FPDF_PRINTING = 2048;
3876
3921
  const FPDF_RENDER_TOBECONTINUED = 1;
3877
3922
  const FPDF_RENDER_DONE = 2;
3878
3923
  var PageRenderReader = class {
@@ -3892,6 +3937,8 @@ var PageRenderReader = class {
3892
3937
  const pagePtr = await pool.acquireInSlices(pageObjectNumber, signal, slices);
3893
3938
  try {
3894
3939
  throwIfAborted(signal);
3940
+ checkRenderUsage(options, null);
3941
+ const print = options.usage === "print";
3895
3942
  const page = readPageBox(this.runtime, pagePtr);
3896
3943
  const target = resolveTarget(options.target, page);
3897
3944
  const rotation = options.rotation ?? 0;
@@ -3906,6 +3953,7 @@ var PageRenderReader = class {
3906
3953
  let flags = 16;
3907
3954
  if (layers.includeAnnotations) flags |= FPDF_RENDER_ANNOT;
3908
3955
  if (layers.includeFormFields) flags |= EPDF_RENDER_WIDGETS;
3956
+ if (print) flags |= FPDF_PRINTING;
3909
3957
  const raster = await rasterizeAsync(this.runtime, {
3910
3958
  rect: target,
3911
3959
  page,
@@ -4789,7 +4837,7 @@ function extractAttachmentToBuffer(runtime, attachmentPtr, maxDecodedBytes) {
4789
4837
  mem.poke(bufPtrSlot, "ptr", 0);
4790
4838
  mem.poke(sizePtr, "i32", 0);
4791
4839
  mem.poke(statusPtr, "i32", -1);
4792
- if (!fn.EPDFAttachment_ExtractFileToOwnedBuffer(attachmentPtr, BigInt(maxDecodedBytes ?? 0), bufPtrSlot, sizePtr, statusPtr)) throw extractStatusError(Number(mem.peek(statusPtr, "i32")));
4840
+ if (!fn.EPDFAttachment_ExtractFileToOwnedBuffer(attachmentPtr, BigInt(maxDecodedBytes ?? 0), bufPtrSlot, sizePtr, statusPtr)) throw extractStatusError(Number(mem.peek(statusPtr, "i32")), maxDecodedBytes);
4793
4841
  const size = Number(mem.peek(sizePtr, "i32")) >>> 0;
4794
4842
  dataPtr = mem.peek(bufPtrSlot, "ptr");
4795
4843
  if (size === 0 || !dataPtr) return {
@@ -4825,7 +4873,7 @@ function extractAttachmentToFile(runtime, attachmentPtr, path, maxDecodedBytes)
4825
4873
  try {
4826
4874
  mem.poke(sizePtr, "i32", 0);
4827
4875
  mem.poke(statusPtr, "i32", -1);
4828
- if (!fn.EPDFAttachment_ExtractFile(attachmentPtr, writer.ptr, BigInt(maxDecodedBytes ?? 0), sizePtr, statusPtr)) throw extractStatusError(Number(mem.peek(statusPtr, "i32")));
4876
+ if (!fn.EPDFAttachment_ExtractFile(attachmentPtr, writer.ptr, BigInt(maxDecodedBytes ?? 0), sizePtr, statusPtr)) throw extractStatusError(Number(mem.peek(statusPtr, "i32")), maxDecodedBytes);
4829
4877
  return { size: Number(mem.peek(sizePtr, "i32")) >>> 0 };
4830
4878
  } finally {
4831
4879
  writer.close();
@@ -4834,11 +4882,11 @@ function extractAttachmentToFile(runtime, attachmentPtr, path, maxDecodedBytes)
4834
4882
  }
4835
4883
  }
4836
4884
  /** `EPDFAttachmentExtractStatus` (public/fpdf_attachment.h) → EngineError. */
4837
- function extractStatusError(status) {
4885
+ function extractStatusError(status, maxDecodedBytes) {
4838
4886
  switch (status) {
4839
4887
  case 1: return new EngineError(EngineErrorCode.InvalidArg, "attachment has no embedded file stream (missing /EF)");
4840
4888
  case 2: return new EngineError(EngineErrorCode.MalformedPdf, "embedded file stream failed to decode");
4841
- case 3: return new EngineError(EngineErrorCode.PayloadTooLarge, "embedded file exceeds the configured decoded-size limit");
4889
+ case 3: return new EngineError(EngineErrorCode.PayloadTooLarge, `the embedded file decodes to more than ${maxDecodedBytes} bytes, the limit`, { details: { limit: maxDecodedBytes } });
4842
4890
  case 4: return new EngineError(EngineErrorCode.Unknown, "writing the extracted attachment failed");
4843
4891
  default: return new EngineError(EngineErrorCode.Unknown, `attachment extraction failed (status ${status})`);
4844
4892
  }
@@ -5661,34 +5709,50 @@ function readLine(fn, mem, annotPtr, base) {
5661
5709
  };
5662
5710
  }
5663
5711
  //#endregion
5664
- //#region src/features/annotations/internal/read/readLinkAnnotation.ts
5712
+ //#region src/features/destinations/createDestination.ts
5665
5713
  /**
5666
- * Link reader: rect/flags/relationship ride the base (a link grouped to an
5667
- * annotation is plain `inReplyTo` + `replyType: 'group'`); this module only
5668
- * materialises the normalized target.
5669
- *
5670
- * One truth, one projection: the target is a pure function of the same
5671
- * payload-carrying action tree the scripting plane reads
5672
- * (`base.actions.activate`) — no second native read exists any more, so the
5673
- * two action-shaped surfaces cannot drift by construction.
5674
- *
5675
- * `/A` precedence, pinned: an activate action that exists but cannot be
5676
- * executed (an `incomplete` tree, a degraded/unreadable root) projects
5677
- * `unsupported` — a broken action is a dead link, never an invitation to
5678
- * guess. The direct `/Dest` (which is data, not an action) is consulted
5679
- * only when no `/A` exists at all.
5714
+ * Build an indirect explicit-destination array for `dest`. The target page
5715
+ * is named by its object number and never loaded: a destination only refers
5716
+ * to the page's dictionary.
5680
5717
  */
5681
- function readLink(fn, mem, annotPtr, base, _rawSubtypeCode, ctx) {
5682
- return {
5683
- ...base,
5684
- subtype: "link",
5685
- target: readLinkTarget(fn, mem, annotPtr, base, ctx)
5686
- };
5718
+ function createDestination(fn, mem, docPtr, dest) {
5719
+ const page = dest.page.objectNumber;
5720
+ const destPtr = dest.kind === "xyz" ? fn.EPDFDest_CreateXYZByObjectNumber(docPtr, page, dest.left != null, dest.left ?? 0, dest.top != null, dest.top ?? 0, dest.zoom != null, dest.zoom ?? 0) : createViewDestination(fn, mem, docPtr, page, dest);
5721
+ if (!destPtr) throw new EngineError(EngineErrorCode.NotFound, `destination page not found: pageObjectNumber=${page}`);
5722
+ return destPtr;
5723
+ }
5724
+ function createViewDestination(fn, mem, docPtr, page, dest) {
5725
+ const params = (() => {
5726
+ switch (dest.kind) {
5727
+ case "fitH":
5728
+ case "fitBH": return dest.top != null ? [dest.top] : [];
5729
+ case "fitV":
5730
+ case "fitBV": return dest.left != null ? [dest.left] : [];
5731
+ case "fitR": return [
5732
+ dest.left,
5733
+ dest.bottom,
5734
+ dest.right,
5735
+ dest.top
5736
+ ];
5737
+ default: return [];
5738
+ }
5739
+ })();
5740
+ const view = VIEW_CODE_BY_KIND[dest.kind];
5741
+ if (!params.length) return fn.EPDFDest_CreateViewByObjectNumber(docPtr, page, view, NULL_PTR, 0);
5742
+ return withScratch(mem, params.length * 4, (buf) => {
5743
+ for (let i = 0; i < params.length; i++) mem.poke(buf, "f32", params[i], i * 4);
5744
+ return fn.EPDFDest_CreateViewByObjectNumber(docPtr, page, view, buf, params.length);
5745
+ });
5687
5746
  }
5747
+ //#endregion
5748
+ //#region src/features/destinations/linkTarget.ts
5688
5749
  /**
5689
- * Project the navigation view from a payload-carrying activate tree. Pure —
5690
- * shared with any consumer that wants the root-level navigation reading of a
5691
- * tree (the link plugin's no-actions-plugin fallback uses the same law).
5750
+ * A link target read the way every reader reads one (links, outline items):
5751
+ * the navigation view of a payload-carrying activate tree. Pure.
5752
+ *
5753
+ * The law, enforced at the projection too: never execute — not even
5754
+ * navigate the root of — a tree marked incomplete. A broken action is a
5755
+ * dead target, never an invitation to guess.
5692
5756
  */
5693
5757
  function linkTargetFromActionTree(tree) {
5694
5758
  if (tree.incomplete) return { kind: "unsupported" };
@@ -5719,6 +5783,41 @@ function linkTargetFromActionTree(tree) {
5719
5783
  default: return { kind: "unsupported" };
5720
5784
  }
5721
5785
  }
5786
+ /**
5787
+ * A new indirect action of the document for a writable target: a GoTo to a
5788
+ * new destination, a URI, or a standard named verb. Links and outline items
5789
+ * write their targets with it, so both read back the same way.
5790
+ */
5791
+ function createLinkTargetAction(fn, mem, docPtr, target) {
5792
+ const actionPtr = target.kind === "uri" ? fn.EPDFAction_CreateURI(docPtr, target.uri) : target.kind === "named" ? fn.EPDFAction_CreateNamed(docPtr, target.name) : fn.EPDFAction_CreateGoTo(docPtr, createDestination(fn, mem, docPtr, target.destination));
5793
+ if (!actionPtr) throw new EngineError(EngineErrorCode.Unknown, `failed to create '${target.kind}' action`);
5794
+ return actionPtr;
5795
+ }
5796
+ //#endregion
5797
+ //#region src/features/annotations/internal/read/readLinkAnnotation.ts
5798
+ /**
5799
+ * Link reader: rect/flags/relationship ride the base (a link grouped to an
5800
+ * annotation is plain `inReplyTo` + `replyType: 'group'`); this module only
5801
+ * materialises the normalized target.
5802
+ *
5803
+ * One truth, one projection: the target is a pure function of the same
5804
+ * payload-carrying action tree the scripting plane reads
5805
+ * (`base.actions.activate`) — no second native read exists any more, so the
5806
+ * two action-shaped surfaces cannot drift by construction.
5807
+ *
5808
+ * `/A` precedence, pinned: an activate action that exists but cannot be
5809
+ * executed (an `incomplete` tree, a degraded/unreadable root) projects
5810
+ * `unsupported` — a broken action is a dead link, never an invitation to
5811
+ * guess. The direct `/Dest` (which is data, not an action) is consulted
5812
+ * only when no `/A` exists at all.
5813
+ */
5814
+ function readLink(fn, mem, annotPtr, base, _rawSubtypeCode, ctx) {
5815
+ return {
5816
+ ...base,
5817
+ subtype: "link",
5818
+ target: readLinkTarget(fn, mem, annotPtr, base, ctx)
5819
+ };
5820
+ }
5722
5821
  function readLinkTarget(fn, mem, annotPtr, base, ctx) {
5723
5822
  const activate = base.actions?.activate;
5724
5823
  if (activate) return linkTargetFromActionTree(activate);
@@ -8524,42 +8623,6 @@ function isLineSubtype(subtype) {
8524
8623
  return subtype === "line";
8525
8624
  }
8526
8625
  //#endregion
8527
- //#region src/features/destinations/createDestination.ts
8528
- /**
8529
- * Build an indirect explicit-destination array for `dest`. The target page
8530
- * is named by its object number and never loaded: a destination only refers
8531
- * to the page's dictionary.
8532
- */
8533
- function createDestination(fn, mem, docPtr, dest) {
8534
- const page = dest.page.objectNumber;
8535
- const destPtr = dest.kind === "xyz" ? fn.EPDFDest_CreateXYZByObjectNumber(docPtr, page, dest.left != null, dest.left ?? 0, dest.top != null, dest.top ?? 0, dest.zoom != null, dest.zoom ?? 0) : createViewDestination(fn, mem, docPtr, page, dest);
8536
- if (!destPtr) throw new EngineError(EngineErrorCode.NotFound, `destination page not found: pageObjectNumber=${page}`);
8537
- return destPtr;
8538
- }
8539
- function createViewDestination(fn, mem, docPtr, page, dest) {
8540
- const params = (() => {
8541
- switch (dest.kind) {
8542
- case "fitH":
8543
- case "fitBH": return dest.top != null ? [dest.top] : [];
8544
- case "fitV":
8545
- case "fitBV": return dest.left != null ? [dest.left] : [];
8546
- case "fitR": return [
8547
- dest.left,
8548
- dest.bottom,
8549
- dest.right,
8550
- dest.top
8551
- ];
8552
- default: return [];
8553
- }
8554
- })();
8555
- const view = VIEW_CODE_BY_KIND[dest.kind];
8556
- if (!params.length) return fn.EPDFDest_CreateViewByObjectNumber(docPtr, page, view, NULL_PTR, 0);
8557
- return withScratch(mem, params.length * 4, (buf) => {
8558
- for (let i = 0; i < params.length; i++) mem.poke(buf, "f32", params[i], i * 4);
8559
- return fn.EPDFDest_CreateViewByObjectNumber(docPtr, page, view, buf, params.length);
8560
- });
8561
- }
8562
- //#endregion
8563
8626
  //#region src/features/annotations/internal/write/writeLinkAnnotation.ts
8564
8627
  /**
8565
8628
  * Link writer: rect + the `/A` action. `goto`, `uri` and the standard
@@ -8602,8 +8665,7 @@ function isLinkSubtype(subtype) {
8602
8665
  function applyLinkTarget(fn, mem, annotPtr, target, ctx) {
8603
8666
  const docPtr = ctx?.docPtr;
8604
8667
  if (!docPtr) throw new EngineError(EngineErrorCode.InvalidArg, "writing a link target requires a document pointer on the write context");
8605
- const actionPtr = target.kind === "uri" ? fn.EPDFAction_CreateURI(docPtr, target.uri) : target.kind === "named" ? fn.EPDFAction_CreateNamed(docPtr, target.name) : fn.EPDFAction_CreateGoTo(docPtr, createDestination(fn, mem, docPtr, target.destination));
8606
- if (!actionPtr) throw new EngineError(EngineErrorCode.Unknown, `failed to create '${target.kind}' action`);
8668
+ const actionPtr = createLinkTargetAction(fn, mem, docPtr, target);
8607
8669
  if (!fn.EPDFAnnot_SetAction(annotPtr, actionPtr)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAnnot_SetAction failed for link");
8608
8670
  }
8609
8671
  //#endregion
@@ -9799,7 +9861,7 @@ var AnnotationBatchApplier = class {
9799
9861
  ...given,
9800
9862
  nm: generateUuidV7()
9801
9863
  };
9802
- if (create.objectNumber !== void 0) labelled(create.label, () => this.session.useObjectNumber(create.objectNumber));
9864
+ if (create.objectNumber !== void 0) labelled(create.label, () => this.session.claimObjectNumber(create.objectNumber));
9803
9865
  const replyTo = create.replyTo && {
9804
9866
  type: create.replyTo.type,
9805
9867
  to: labelled(create.label, () => this.linkTarget(create.replyTo.to, create.page, creates.length, "reply"))
@@ -9817,7 +9879,7 @@ var AnnotationBatchApplier = class {
9817
9879
  throwIfAborted(signal);
9818
9880
  if (prepared.length === 0) return {
9819
9881
  created: [],
9820
- meta: metaOf(this.session.writeStamp(), [], [])
9882
+ meta: metaOf$1(this.session.writeStamp(), [], [])
9821
9883
  };
9822
9884
  const placed = prepared.map(({ create, record, ctx, draft }) => {
9823
9885
  throwIfAborted(signal);
@@ -9863,7 +9925,7 @@ var AnnotationBatchApplier = class {
9863
9925
  joinWidgetFieldNumbers(this.runtime, this.session, created);
9864
9926
  return {
9865
9927
  created,
9866
- meta: metaOf(this.session.writeStamp(), placed, linked)
9928
+ meta: metaOf$1(this.session.writeStamp(), placed, linked)
9867
9929
  };
9868
9930
  }
9869
9931
  /**
@@ -9934,7 +9996,7 @@ var AnnotationBatchApplier = class {
9934
9996
  * and each it linked a popup to. Creates only append, so no other annotation
9935
9997
  * moves.
9936
9998
  */
9937
- function metaOf(stamp, placed, linked) {
9999
+ function metaOf$1(stamp, placed, linked) {
9938
10000
  return {
9939
10001
  affectedPages: [...new Set(placed.map((at) => at.pageObjectNumber))].map((page) => toPageRef(page)),
9940
10002
  cacheDelta: null,
@@ -10782,6 +10844,16 @@ function importAnnots(runtime, docPtr, pageIndex, capture) {
10782
10844
  function exportDict(runtime, docPtr, objectNumber, deepKeys) {
10783
10845
  return takeOwned(runtime, `object ${objectNumber}`, (sizePtr) => runtime.fn.EPDFDoc_ExportDictRawToOwnedBuffer(docPtr, objectNumber, deepKeys.join(" "), sizePtr));
10784
10846
  }
10847
+ /**
10848
+ * The dictionary `objectNumber` as bytes a guard compares: without
10849
+ * `skipKeys`, and under `deepKeys` the objects the layer holds, each stream
10850
+ * as it reads. They stay the same while it reads the same, also after a save
10851
+ * encoded its streams again or the layer learned its file's hash; never
10852
+ * imported.
10853
+ */
10854
+ function canonicalDict(runtime, docPtr, objectNumber, deepKeys, skipKeys = []) {
10855
+ return takeOwned(runtime, `object ${objectNumber}`, (sizePtr) => runtime.fn.EPDFDoc_ExportDictCanonicalToOwnedBuffer(docPtr, objectNumber, deepKeys.join(" "), skipKeys.join(" "), sizePtr));
10856
+ }
10785
10857
  /** Puts the dictionary of `capture` back as it was: exactly its keys. */
10786
10858
  function importDict(runtime, docPtr, capture) {
10787
10859
  withBytes(runtime, capture, (ptr) => {
@@ -10798,6 +10870,49 @@ function importField(runtime, docPtr, capture) {
10798
10870
  if (!runtime.fn.EPDFForm_ImportFieldRaw(docPtr, ptr, capture.byteLength)) throw new EngineError(EngineErrorCode.Unknown, "EPDFForm_ImportFieldRaw refused a capture");
10799
10871
  });
10800
10872
  }
10873
+ /** An outline item as its delete removes it: the item, the items under it, its place. */
10874
+ function exportOutlineItem(runtime, docPtr, objectNumber) {
10875
+ return takeOwned(runtime, `outline item ${objectNumber}`, (sizePtr) => runtime.fn.EPDFOutline_ExportItemRawToOwnedBuffer(docPtr, objectNumber, sizePtr));
10876
+ }
10877
+ /** Puts the outline item of `capture` back, with the items under it, at its place. */
10878
+ function importOutlineItem(runtime, docPtr, capture) {
10879
+ withBytes(runtime, capture, (ptr) => {
10880
+ if (!runtime.fn.EPDFOutline_ImportItemRaw(docPtr, ptr, capture.byteLength)) throw new EngineError(EngineErrorCode.Unknown, "EPDFOutline_ImportItemRaw refused a capture");
10881
+ });
10882
+ }
10883
+ /** A page as its delete removes it: the page, what it reaches, its names, its place. */
10884
+ function exportPage(runtime, docPtr, pageObjectNumber) {
10885
+ return takeOwned(runtime, `page ${pageObjectNumber}`, (sizePtr) => runtime.fn.EPDFPage_ExportRawToOwnedBuffer(docPtr, pageObjectNumber, sizePtr));
10886
+ }
10887
+ /** Puts the page of `capture` back into the page tree, at its place, with its names. */
10888
+ function importPage(runtime, docPtr, capture) {
10889
+ withBytes(runtime, capture, (ptr) => {
10890
+ if (!runtime.fn.EPDFPage_ImportRaw(docPtr, ptr, capture.byteLength)) throw new EngineError(EngineErrorCode.Unknown, "EPDFPage_ImportRaw refused a capture");
10891
+ });
10892
+ }
10893
+ /** The embedded file under `key` as its delete removes it: its key, its file specification, its streams. */
10894
+ function exportAttachment(runtime, docPtr, key) {
10895
+ return takeOwned(runtime, `attachment '${key}'`, (sizePtr) => withUtf16String(runtime.mem, key, (keyPtr) => runtime.fn.EPDFAttachment_ExportRawToOwnedBuffer(docPtr, keyPtr, sizePtr)));
10896
+ }
10897
+ /**
10898
+ * The embedded file under `key` as bytes a guard compares: the same while it
10899
+ * is the same file, as it was (its number included); null when no file has
10900
+ * the key. Never imported.
10901
+ */
10902
+ function canonicalAttachment(runtime, docPtr, key) {
10903
+ try {
10904
+ return takeOwned(runtime, `attachment '${key}'`, (sizePtr) => withUtf16String(runtime.mem, key, (keyPtr) => runtime.fn.EPDFAttachment_ExportCanonicalToOwnedBuffer(docPtr, keyPtr, sizePtr)));
10905
+ } catch (error) {
10906
+ if (EngineError.is(error)) return null;
10907
+ throw error;
10908
+ }
10909
+ }
10910
+ /** Puts the embedded file of `capture` back under its key. */
10911
+ function importAttachment(runtime, docPtr, capture) {
10912
+ withBytes(runtime, capture, (ptr) => {
10913
+ if (!runtime.fn.EPDFAttachment_ImportRaw(docPtr, ptr, capture.byteLength)) throw new EngineError(EngineErrorCode.Unknown, "EPDFAttachment_ImportRaw refused a capture");
10914
+ });
10915
+ }
10801
10916
  /** The bytes of a buffer an export returned, which is then released. */
10802
10917
  function takeOwned(runtime, what, call) {
10803
10918
  const { fn, mem } = runtime;
@@ -10876,6 +10991,14 @@ function assertExpected(opIndex, current, expected) {
10876
10991
  } });
10877
10992
  }
10878
10993
  //#endregion
10994
+ //#region src/shared/bytesEqual.ts
10995
+ /** Whether two byte arrays hold the same bytes. */
10996
+ function bytesEqual(a, b) {
10997
+ if (a.byteLength !== b.byteLength) return false;
10998
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
10999
+ return true;
11000
+ }
11001
+ //#endregion
10879
11002
  //#region src/features/changes/internal/objectCaptures.ts
10880
11003
  /**
10881
11004
  * Captures each dictionary before an op writes it: every key, and under
@@ -10894,18 +11017,21 @@ function captureBefore(runtime, session, objects) {
10894
11017
  }
10895
11018
  return [...captured.values()];
10896
11019
  }
10897
- /** Completes captures once the op has written: each dictionary as the op left it, the guard. */
11020
+ /**
11021
+ * Completes captures once the op has written: each dictionary as the op left
11022
+ * it, the guard, in canonical bytes (`canonicalDict`).
11023
+ */
10898
11024
  function captureAfter(runtime, session, pending) {
10899
11025
  return pending.map((object) => ({
10900
11026
  ...object,
10901
- after: exportDict(runtime, session.requireDocPtr(), object.objectNumber, [])
11027
+ after: canonicalDict(runtime, session.requireDocPtr(), object.objectNumber, [])
10902
11028
  }));
10903
11029
  }
10904
11030
  /** Whether every dictionary still reads exactly as the op left it. */
10905
11031
  function unchangedSince(runtime, session, objects) {
10906
11032
  return objects.every((object) => {
10907
- const now = tryExportDict(runtime, session, object.objectNumber, []);
10908
- return now !== null && sameBytes(now, object.after);
11033
+ const now = tryCanonicalDict(runtime, session, object.objectNumber);
11034
+ return now !== null && bytesEqual(now, object.after);
10909
11035
  });
10910
11036
  }
10911
11037
  /**
@@ -10920,6 +11046,15 @@ function revertObjects(runtime, session, objects) {
10920
11046
  session.invalidateDerived();
10921
11047
  return captureAfter(runtime, session, redo);
10922
11048
  }
11049
+ /** A dictionary's canonical bytes, or null when there is none at that number. */
11050
+ function tryCanonicalDict(runtime, session, objectNumber) {
11051
+ try {
11052
+ return canonicalDict(runtime, session.requireDocPtr(), objectNumber, []);
11053
+ } catch (error) {
11054
+ if (EngineError.is(error)) return null;
11055
+ throw error;
11056
+ }
11057
+ }
10923
11058
  /** The capture of a dictionary, or null when there is none at that number. */
10924
11059
  function tryExportDict(runtime, session, objectNumber, deep) {
10925
11060
  try {
@@ -10929,11 +11064,6 @@ function tryExportDict(runtime, session, objectNumber, deep) {
10929
11064
  throw error;
10930
11065
  }
10931
11066
  }
10932
- function sameBytes(a, b) {
10933
- if (a.byteLength !== b.byteLength) return false;
10934
- for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
10935
- return true;
10936
- }
10937
11067
  //#endregion
10938
11068
  //#region src/features/annotations/internal/stackingOrder.ts
10939
11069
  /**
@@ -11176,7 +11306,7 @@ function updateAnnotation(ctx, op, opIndex) {
11176
11306
  if (current.subtype === "widget") throw new EngineError(EngineErrorCode.InvalidArg, "a widget is the form's: change its place and look with doc.forms.updateWidget");
11177
11307
  authorizeUnprotected(ctx.authority, annotationWriteCapability(current.subtype));
11178
11308
  assertExpected(opIndex, current, op.expect);
11179
- return writeUpdate$1(ctx, op.ref, op.patch, "annotations.update", op.resources);
11309
+ return writeUpdate$2(ctx, op.ref, op.patch, "annotations.update", op.resources);
11180
11310
  }
11181
11311
  /** `annotations.delete`; its reverse brings back everything it removed. */
11182
11312
  function deleteAnnotation(ctx, op, opIndex) {
@@ -11184,7 +11314,7 @@ function deleteAnnotation(ctx, op, opIndex) {
11184
11314
  if (current.subtype === "widget") throw widgetDeleteRefused(current);
11185
11315
  authorizeUnprotected(ctx.authority, annotationWriteCapability(current.subtype));
11186
11316
  assertExpected(opIndex, current, op.expect);
11187
- return writeDelete$1(ctx, op.ref);
11317
+ return writeDelete$4(ctx, op.ref);
11188
11318
  }
11189
11319
  /** Why a widget isn't deleted as an annotation, naming the form op that removes it. */
11190
11320
  function widgetDeleteRefused(widget) {
@@ -11251,7 +11381,7 @@ function revertAnnotation(ctx, step) {
11251
11381
  const kept = keys.filter((key) => valuesEqual(now[key], left[key]));
11252
11382
  if (kept.length === 0) return leftAlone(ctx, reports);
11253
11383
  const writable = (key, value) => key === "actions" && current.subtype === "widget" ? widgetActionsOf(value) : value;
11254
- const done = writeUpdate$1(ctx, ref, Object.fromEntries(kept.map((key) => [key, writable(key, restore[key])])), reports);
11384
+ const done = writeUpdate$2(ctx, ref, Object.fromEntries(kept.map((key) => [key, writable(key, restore[key])])), reports);
11255
11385
  const skipped = keys.filter((key) => !kept.includes(key));
11256
11386
  const item = done.item;
11257
11387
  if (skipped.length === 0 || isSkippedItem(item) || item.type !== reports) return done;
@@ -11275,7 +11405,7 @@ function removeAnnotation(ctx, step) {
11275
11405
  }))) return leftAlone(ctx, "annotations.delete");
11276
11406
  for (const capability of annotationWriteCapabilities(members)) authorizeUnprotected(ctx.authority, capability);
11277
11407
  if (step.rights) authorizeImportRights(ctx.authority);
11278
- return writeDelete$1(ctx, step.ref, step.rights);
11408
+ return writeDelete$4(ctx, step.ref, step.rights);
11279
11409
  }
11280
11410
  /**
11281
11411
  * Brings back what a delete removed, at the same numbers and positions. Skipped
@@ -11396,7 +11526,7 @@ function reorderItem(ctx, family, page, order, moved) {
11396
11526
  * An update of `ref` with `patch`, recording the dictionaries it writes, and
11397
11527
  * reported as `reports`: an annotation's update, or a widget's form op.
11398
11528
  */
11399
- function writeUpdate$1(ctx, ref, patch, reports, resources) {
11529
+ function writeUpdate$2(ctx, ref, patch, reports, resources) {
11400
11530
  promoteInlineAnnotations(ctx.runtime, ctx.session, ref.page.objectNumber);
11401
11531
  const before = readAnnotation(ctx, ref);
11402
11532
  const written = before.subtype === "widget" ? fitWidgetToFamily(before.fieldFamily, patch) : patch;
@@ -11444,7 +11574,7 @@ function updatedItem(ctx, reports, ref, result) {
11444
11574
  }
11445
11575
  /** A delete of `ref` and what goes with it, capturing them first. */
11446
11576
  /** Deletes `ref` and what goes with it; `rights` is what a step under an import's rights checked. */
11447
- function writeDelete$1(ctx, ref, rights) {
11577
+ function writeDelete$4(ctx, ref, rights) {
11448
11578
  const page = ref.page.objectNumber;
11449
11579
  promoteInlineAnnotations(ctx.runtime, ctx.session, page);
11450
11580
  const members = deletedWith(listPage(ctx, page), ref);
@@ -11561,66 +11691,314 @@ function objectNumberOf$1(ctx, ref) {
11561
11691
  }
11562
11692
  }
11563
11693
  //#endregion
11564
- //#region src/features/forms/internal/authorWidget.ts
11565
- const WIDGET_SUBTYPE_CODE = 20;
11694
+ //#region src/features/attachments/AttachmentReader.ts
11566
11695
  /**
11567
- * Birth a widget through the annotation plane (EPDFPage_CreateAnnotRaw -
11568
- * indirect, durable object number, no page load), place it and turn it
11569
- * (`/MK /R`), style it with the placement's style fields through the
11570
- * widget-plane writer (`applyWidgetStyle` - the same code the widget
11571
- * annotation kind uses for create/patch), and give it the placement's
11572
- * actions. Like every annotation the engine makes, it gets a fresh UUIDv7
11573
- * `/NM`. Returns the widget's object number (`objectNumber`, which the
11574
- * session checked, or the next free one), ready for EPDFForm_AttachWidget
11575
- * adoption, which draws its appearance turned.
11696
+ * Read-only access to embedded files, at both of their homes: the
11697
+ * document catalog's `/EmbeddedFiles` name tree (list / readFile by
11698
+ * index) and a FileAttachment annotation's `/FS` (readAnnotationFile by
11699
+ * ref). Pure reads over the session — no layer artifacts (the
11700
+ * `PagesExtractor` shape).
11701
+ *
11702
+ * Byte delivery mirrors the request's `path?`: absent → a standalone
11703
+ * buffer for the transfer list (browser); present → the decoded file is
11704
+ * streamed to `path` via `FPDF_FILEWRITE` (server), and only metadata
11705
+ * rides the response.
11576
11706
  */
11577
- function createUnattachedWidget(runtime, docPtr, pageIndex, placement, objectNumber) {
11578
- const { fn, mem } = runtime;
11579
- const annotPtr = fn.EPDFPage_CreateAnnotRaw(docPtr, pageIndex, WIDGET_SUBTYPE_CODE, objectNumber ?? 0);
11580
- if (annotPtr === NULL_PTR && objectNumber !== void 0) throw objectNumberUnavailable(objectNumber, "taken");
11581
- if (annotPtr === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to create widget annotation");
11582
- try {
11583
- writeAnnotString(fn, mem, annotPtr, "NM", generateUuidV7());
11584
- const { page: _page, rect, rotation, exportValue: _exportValue, actions, ...style } = placement;
11585
- setAnnotRect(fn, mem, annotPtr, rect);
11586
- if (rotation) setWidgetTurn(fn, annotPtr, rotation);
11587
- if (Object.keys(style).length > 0) applyWidgetStyle(fn, mem, annotPtr, style);
11588
- if (actions) writeWidgetActions(runtime, docPtr, annotPtr, actions);
11589
- const widgetObjectNumber = fn.EPDFAnnot_GetObjectNumber(annotPtr);
11590
- if (widgetObjectNumber <= 0) throw new EngineError(EngineErrorCode.Unknown, "widget annotation has no object number");
11591
- return widgetObjectNumber;
11592
- } finally {
11593
- fn.FPDFPage_CloseAnnot(annotPtr);
11707
+ var AttachmentReader = class {
11708
+ constructor(runtime, session) {
11709
+ this.runtime = runtime;
11710
+ this.session = session;
11594
11711
  }
11595
- }
11596
- //#endregion
11597
- //#region src/features/forms/internal/fieldFlagBits.ts
11598
- /**
11599
- * /Ff bits writable through drafts and patches (1-based spec bit numbers).
11600
- * Family-defining bits (Radio 1<<15, Pushbutton 1<<16, Combo 1<<17) are set
11601
- * once by EPDFForm_CreateField and immutable afterwards - the native
11602
- * setter rejects them.
11603
- */
11604
- const FIELD_FLAG_BITS = {
11605
- readOnly: 1,
11606
- required: 2,
11607
- noExport: 4,
11608
- multiline: 4096,
11609
- password: 8192,
11610
- noToggleToOff: 16384,
11611
- edit: 1 << 18,
11612
- multiSelect: 1 << 21,
11613
- comb: 1 << 24,
11614
- radiosInUnison: 1 << 25
11615
- };
11616
- /**
11617
- * Fold optional booleans into set/clear masks: `true` sets the bit,
11618
- * `false` clears it, `undefined` leaves it alone.
11619
- */
11620
- function flagMasks(flags) {
11621
- let setBits = 0;
11622
- let clearBits = 0;
11623
- for (const [name, bit] of Object.entries(FIELD_FLAG_BITS)) {
11712
+ /** Snapshot of the `/EmbeddedFiles` name tree, in tree (key-sorted) order. */
11713
+ list(signal) {
11714
+ throwIfAborted(signal);
11715
+ const { fn, mem } = this.runtime;
11716
+ const docPtr = this.session.requireDocPtr();
11717
+ const count = fn.FPDFDoc_GetAttachmentCount(docPtr);
11718
+ const items = [];
11719
+ for (let index = 0; index < count; index++) {
11720
+ const attachmentPtr = fn.FPDFDoc_GetAttachment(docPtr, index);
11721
+ if (!attachmentPtr) continue;
11722
+ const key = readAttachmentKey(fn, mem, docPtr, index) ?? "";
11723
+ items.push({
11724
+ ...readAttachmentFileInfo(fn, mem, attachmentPtr),
11725
+ ref: toAttachmentRef(key),
11726
+ index
11727
+ });
11728
+ }
11729
+ return items;
11730
+ }
11731
+ /** Decode one document-level embedded file, addressed by key. */
11732
+ readFile(ref, path, maxDecodedBytes, signal) {
11733
+ throwIfAborted(signal);
11734
+ const { fn, mem } = this.runtime;
11735
+ const docPtr = this.session.requireDocPtr();
11736
+ const index = resolveAttachmentIndex(fn, mem, docPtr, ref);
11737
+ if (index < 0) throw new EngineError(EngineErrorCode.NotFound, `no embedded file with key '${ref.key}' in the document`);
11738
+ const attachmentPtr = fn.FPDFDoc_GetAttachment(docPtr, index);
11739
+ if (!attachmentPtr) throw new EngineError(EngineErrorCode.Unknown, `embedded file with key '${ref.key}' resolved to index ${index} but could not be opened`);
11740
+ const info = readAttachmentFileInfo(fn, mem, attachmentPtr);
11741
+ return this.extract(attachmentPtr, info.name, info.mimeType ?? void 0, path, maxDecodedBytes);
11742
+ }
11743
+ /** Decode the file embedded in a FileAttachment annotation's `/FS`. */
11744
+ /** A file attachment annotation's file; a page not parsed yet loads in slices. */
11745
+ async readAnnotationFile(pageObjectNumber, ref, path, maxDecodedBytes, signal, slices) {
11746
+ throwIfAborted(signal);
11747
+ const { fn, mem } = this.runtime;
11748
+ const pool = this.session.pagePool();
11749
+ const pagePtr = await pool.acquireInSlices(pageObjectNumber, signal, slices);
11750
+ let annotPtr = null;
11751
+ try {
11752
+ annotPtr = resolveAnnotPtr(this.runtime, this.session, pagePtr, ref);
11753
+ const attachmentPtr = fn.FPDFAnnot_GetFileAttachment(annotPtr);
11754
+ if (!attachmentPtr) throw new EngineError(EngineErrorCode.InvalidArg, "annotation is not a file attachment or carries no embedded file (/FS)");
11755
+ const info = readAttachmentFileInfo(fn, mem, attachmentPtr);
11756
+ return this.extract(attachmentPtr, info.name, info.mimeType ?? void 0, path, maxDecodedBytes);
11757
+ } finally {
11758
+ if (annotPtr !== null) fn.FPDFPage_CloseAnnot(annotPtr);
11759
+ pool.release(pageObjectNumber);
11760
+ }
11761
+ }
11762
+ extract(attachmentPtr, name, mimeType, path, maxDecodedBytes) {
11763
+ if (path !== void 0) {
11764
+ const { size } = extractAttachmentToFile(this.runtime, attachmentPtr, path, maxDecodedBytes);
11765
+ return {
11766
+ name,
11767
+ ...mimeType ? { mimeType } : {},
11768
+ size,
11769
+ path
11770
+ };
11771
+ }
11772
+ const { bytes, size } = extractAttachmentToBuffer(this.runtime, attachmentPtr, maxDecodedBytes);
11773
+ return {
11774
+ name,
11775
+ ...mimeType ? { mimeType } : {},
11776
+ size,
11777
+ bytes
11778
+ };
11779
+ }
11780
+ };
11781
+ //#endregion
11782
+ //#region src/features/attachments/AttachmentMutator.ts
11783
+ /**
11784
+ * Create/delete writes over the document catalog's `/EmbeddedFiles` name
11785
+ * tree, sharing the write path (`writeAttachmentFilePayload`) with the
11786
+ * file-attachment annotation writer. The change ops run them
11787
+ * (`attachmentChanges.ts`), which decide what an undo needs.
11788
+ *
11789
+ * Identity is the name-tree key (unique by construction). Note the
11790
+ * tree is key-sorted: both writes shift other entries' indices; keys
11791
+ * never move.
11792
+ */
11793
+ var AttachmentMutator = class {
11794
+ constructor(runtime, session) {
11795
+ this.runtime = runtime;
11796
+ this.session = session;
11797
+ }
11798
+ /**
11799
+ * Create an embedded file from `bytes`. `file.name` becomes the tree key
11800
+ * (PDFium writes key = /UF = /F); a duplicate key rejects with
11801
+ * `InvalidArg`. The result carries the read-back entry,
11802
+ * `checksum`/`size`/dates included.
11803
+ */
11804
+ create(file, bytes, signal) {
11805
+ throwIfAborted(signal);
11806
+ const { fn, mem } = this.runtime;
11807
+ const docPtr = this.session.requireDocPtr();
11808
+ if (!bytes) throw new EngineError(EngineErrorCode.InvalidArg, "an attachments.create op needs its bytes as its file resource", { details: { field: "resources.file" } });
11809
+ if (file.name.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "attachment file requires a name");
11810
+ if (resolveAttachmentIndex(fn, mem, docPtr, {
11811
+ kind: "key",
11812
+ key: file.name
11813
+ }) !== -1) throw new EngineError(EngineErrorCode.InvalidArg, `an embedded file with key '${file.name}' already exists — keys are unique; rename and retry`);
11814
+ throwIfAborted(signal);
11815
+ const namePtr = mem.writeU16String(file.name);
11816
+ let attachmentPtr;
11817
+ try {
11818
+ attachmentPtr = fn.FPDFDoc_AddAttachment(docPtr, namePtr);
11819
+ } finally {
11820
+ mem.free(namePtr);
11821
+ }
11822
+ if (!attachmentPtr) throw new EngineError(EngineErrorCode.Unknown, "FPDFDoc_AddAttachment returned NULL");
11823
+ writeAttachmentFilePayload(fn, mem, attachmentPtr, docPtr, file, {
11824
+ bytes,
11825
+ ...file.mimeType !== void 0 ? { mimeType: file.mimeType } : {},
11826
+ name: file.name
11827
+ });
11828
+ const ref = toAttachmentRef(file.name);
11829
+ const attachment = this.find(ref);
11830
+ if (!attachment) throw new EngineError(EngineErrorCode.Unknown, "created attachment did not resolve by key after the write");
11831
+ return {
11832
+ attachment,
11833
+ meta: this.meta(ref)
11834
+ };
11835
+ }
11836
+ /** The entry under `ref`'s key as it reads now; `null` when no file has the key. */
11837
+ find(ref) {
11838
+ const { fn, mem } = this.runtime;
11839
+ const docPtr = this.session.requireDocPtr();
11840
+ const index = resolveAttachmentIndex(fn, mem, docPtr, ref);
11841
+ const attachmentPtr = index < 0 ? null : fn.FPDFDoc_GetAttachment(docPtr, index);
11842
+ if (!attachmentPtr) return null;
11843
+ return {
11844
+ ...readAttachmentFileInfo(fn, mem, attachmentPtr),
11845
+ ref,
11846
+ index
11847
+ };
11848
+ }
11849
+ /** A write's meta, naming the file it changed. Cloud coherence pins are a server concern. */
11850
+ meta(ref) {
11851
+ return {
11852
+ affectedPages: [],
11853
+ cacheDelta: null,
11854
+ ...this.session.writeStamp(),
11855
+ changed: [ref]
11856
+ };
11857
+ }
11858
+ /** Delete an embedded file by key. Unlinks the tree entry only. */
11859
+ delete(ref, signal) {
11860
+ throwIfAborted(signal);
11861
+ const { fn, mem } = this.runtime;
11862
+ const docPtr = this.session.requireDocPtr();
11863
+ const index = resolveAttachmentIndex(fn, mem, docPtr, ref);
11864
+ if (index < 0) throw new EngineError(EngineErrorCode.NotFound, `no embedded file with key '${ref.key}' in the document`);
11865
+ if (!fn.FPDFDoc_DeleteAttachment(docPtr, index)) throw new EngineError(EngineErrorCode.Unknown, "FPDFDoc_DeleteAttachment returned false");
11866
+ return { meta: this.meta(ref) };
11867
+ }
11868
+ };
11869
+ //#endregion
11870
+ //#region src/features/changes/internal/attachmentChanges.ts
11871
+ const mutatorOf$2 = (ctx) => new AttachmentMutator(ctx.runtime, ctx.session);
11872
+ const authorize$2 = (ctx) => authorizeCapability(ctx.authority, "doc.attachments.modify");
11873
+ /** `attachments.create`; its reverse deletes the file while it is still the file this made. */
11874
+ function createAttachment(ctx, op) {
11875
+ authorize$2(ctx);
11876
+ const result = mutatorOf$2(ctx).create(op.file, op.resources.file, ctx.signal);
11877
+ return {
11878
+ item: {
11879
+ type: "attachments.create",
11880
+ ...result
11881
+ },
11882
+ reverse: [removeStepOf$1(ctx, result.attachment.ref)]
11883
+ };
11884
+ }
11885
+ /** `attachments.delete`, captured in its transaction so an undo brings back the same file. */
11886
+ function deleteAttachment(ctx, op) {
11887
+ authorize$2(ctx);
11888
+ return writeDelete$3(ctx, op.ref);
11889
+ }
11890
+ /** Deletes a file a create or a restore brought, while it reads as the change left it. */
11891
+ function removeAttachment(ctx, step) {
11892
+ const now = canonicalAttachment(ctx.runtime, ctx.session.requireDocPtr(), step.ref.key);
11893
+ if (!now || !bytesEqual(now, step.left)) return leftAlone(ctx, "attachments.delete");
11894
+ authorize$2(ctx);
11895
+ return writeDelete$3(ctx, step.ref);
11896
+ }
11897
+ /** Brings back a file a delete removed, while no file has its key. */
11898
+ function restoreAttachment(ctx, step) {
11899
+ const mutator = mutatorOf$2(ctx);
11900
+ if (mutator.find(step.ref)) return leftAlone(ctx, "attachments.restore");
11901
+ authorize$2(ctx);
11902
+ importAttachment(ctx.runtime, ctx.session.requireDocPtr(), step.capture);
11903
+ const attachment = mutator.find(step.ref);
11904
+ if (!attachment) throw new EngineError(EngineErrorCode.Unknown, `attachment '${step.ref.key}' did not come back`);
11905
+ return {
11906
+ item: {
11907
+ type: "attachments.restore",
11908
+ attachment,
11909
+ meta: mutator.meta(step.ref)
11910
+ },
11911
+ reverse: [removeStepOf$1(ctx, step.ref)]
11912
+ };
11913
+ }
11914
+ /** Deletes the file under `ref`, captured first so its restore brings it back. */
11915
+ function writeDelete$3(ctx, ref) {
11916
+ const mutator = mutatorOf$2(ctx);
11917
+ if (!mutator.find(ref)) throw new EngineError(EngineErrorCode.NotFound, `no embedded file with key '${ref.key}' in the document`, { details: { field: "ref" } });
11918
+ const capture = exportAttachment(ctx.runtime, ctx.session.requireDocPtr(), ref.key);
11919
+ return {
11920
+ item: {
11921
+ type: "attachments.delete",
11922
+ ...mutator.delete(ref, ctx.signal)
11923
+ },
11924
+ reverse: [{
11925
+ kind: "attachments.restore",
11926
+ ref,
11927
+ capture
11928
+ }]
11929
+ };
11930
+ }
11931
+ /** The step that deletes the file under `ref` again while it reads as it does now. */
11932
+ function removeStepOf$1(ctx, ref) {
11933
+ const left = canonicalAttachment(ctx.runtime, ctx.session.requireDocPtr(), ref.key);
11934
+ if (!left) throw new EngineError(EngineErrorCode.Unknown, `attachment '${ref.key}' is not there to guard`);
11935
+ return {
11936
+ kind: "attachments.remove",
11937
+ ref,
11938
+ left
11939
+ };
11940
+ }
11941
+ //#endregion
11942
+ //#region src/features/forms/internal/authorWidget.ts
11943
+ const WIDGET_SUBTYPE_CODE = 20;
11944
+ /**
11945
+ * Birth a widget through the annotation plane (EPDFPage_CreateAnnotRaw -
11946
+ * indirect, durable object number, no page load), place it and turn it
11947
+ * (`/MK /R`), style it with the placement's style fields through the
11948
+ * widget-plane writer (`applyWidgetStyle` - the same code the widget
11949
+ * annotation kind uses for create/patch), and give it the placement's
11950
+ * actions. Like every annotation the engine makes, it gets a fresh UUIDv7
11951
+ * `/NM`. Returns the widget's object number (`objectNumber`, which the
11952
+ * session checked, or the next free one), ready for EPDFForm_AttachWidget
11953
+ * adoption, which draws its appearance turned.
11954
+ */
11955
+ function createUnattachedWidget(runtime, docPtr, pageIndex, placement, objectNumber) {
11956
+ const { fn, mem } = runtime;
11957
+ const annotPtr = fn.EPDFPage_CreateAnnotRaw(docPtr, pageIndex, WIDGET_SUBTYPE_CODE, objectNumber ?? 0);
11958
+ if (annotPtr === NULL_PTR && objectNumber !== void 0) throw objectNumberUnavailable(objectNumber, "taken");
11959
+ if (annotPtr === NULL_PTR) throw new EngineError(EngineErrorCode.Unknown, "failed to create widget annotation");
11960
+ try {
11961
+ writeAnnotString(fn, mem, annotPtr, "NM", generateUuidV7());
11962
+ const { page: _page, rect, rotation, exportValue: _exportValue, actions, ...style } = placement;
11963
+ setAnnotRect(fn, mem, annotPtr, rect);
11964
+ if (rotation) setWidgetTurn(fn, annotPtr, rotation);
11965
+ if (Object.keys(style).length > 0) applyWidgetStyle(fn, mem, annotPtr, style);
11966
+ if (actions) writeWidgetActions(runtime, docPtr, annotPtr, actions);
11967
+ const widgetObjectNumber = fn.EPDFAnnot_GetObjectNumber(annotPtr);
11968
+ if (widgetObjectNumber <= 0) throw new EngineError(EngineErrorCode.Unknown, "widget annotation has no object number");
11969
+ return widgetObjectNumber;
11970
+ } finally {
11971
+ fn.FPDFPage_CloseAnnot(annotPtr);
11972
+ }
11973
+ }
11974
+ //#endregion
11975
+ //#region src/features/forms/internal/fieldFlagBits.ts
11976
+ /**
11977
+ * /Ff bits writable through drafts and patches (1-based spec bit numbers).
11978
+ * Family-defining bits (Radio 1<<15, Pushbutton 1<<16, Combo 1<<17) are set
11979
+ * once by EPDFForm_CreateField and immutable afterwards - the native
11980
+ * setter rejects them.
11981
+ */
11982
+ const FIELD_FLAG_BITS = {
11983
+ readOnly: 1,
11984
+ required: 2,
11985
+ noExport: 4,
11986
+ multiline: 4096,
11987
+ password: 8192,
11988
+ noToggleToOff: 16384,
11989
+ edit: 1 << 18,
11990
+ multiSelect: 1 << 21,
11991
+ comb: 1 << 24,
11992
+ radiosInUnison: 1 << 25
11993
+ };
11994
+ /**
11995
+ * Fold optional booleans into set/clear masks: `true` sets the bit,
11996
+ * `false` clears it, `undefined` leaves it alone.
11997
+ */
11998
+ function flagMasks(flags) {
11999
+ let setBits = 0;
12000
+ let clearBits = 0;
12001
+ for (const [name, bit] of Object.entries(FIELD_FLAG_BITS)) {
11624
12002
  const value = flags[name];
11625
12003
  if (value === true) setBits |= bit;
11626
12004
  else if (value === false) clearBits |= bit;
@@ -12526,8 +12904,8 @@ var FormMutator = class {
12526
12904
  }
12527
12905
  const { objectNumber, widgetObjectNumbers } = numbers;
12528
12906
  if (widgetObjectNumbers && widgetObjectNumbers.length !== placements.length) throw new EngineError(EngineErrorCode.InvalidArg, `forms.create needs one widget object number per widget: ${widgetObjectNumbers.length} for ${placements.length}`, { details: { field: "widgetObjectNumbers" } });
12529
- if (objectNumber !== void 0) this.session.useObjectNumber(objectNumber);
12530
- for (const number of widgetObjectNumbers ?? []) this.session.useObjectNumber(number);
12907
+ if (objectNumber !== void 0) this.session.claimObjectNumber(objectNumber);
12908
+ for (const number of widgetObjectNumbers ?? []) this.session.claimObjectNumber(number);
12531
12909
  throwIfAborted(signal);
12532
12910
  const fieldObjectNumber = this.createFieldNode(draft, objectNumber);
12533
12911
  this.configureNewField(draft, fieldObjectNumber);
@@ -12747,9 +13125,9 @@ var FormMutator = class {
12747
13125
  const pageIndex = this.preflightPlacement(fitted);
12748
13126
  const onState = onStateOf(before.family, fitted);
12749
13127
  const mergedPage = before.widgets.find((w) => w.objectNumber === resolved.fieldObjectNumber)?.page ?? null;
12750
- if (numbers.objectNumber !== void 0) this.session.useObjectNumber(numbers.objectNumber);
13128
+ if (numbers.objectNumber !== void 0) this.session.claimObjectNumber(numbers.objectNumber);
12751
13129
  const splitObjectNumber = mergedPage ? numbers.splitObjectNumber : void 0;
12752
- if (splitObjectNumber !== void 0) this.session.useObjectNumber(splitObjectNumber);
13130
+ if (splitObjectNumber !== void 0) this.session.claimObjectNumber(splitObjectNumber);
12753
13131
  throwIfAborted(signal);
12754
13132
  if (mergedPage) promoteInlineAnnotations(this.runtime, this.session, mergedPage.objectNumber);
12755
13133
  const widgetObjectNumber = createUnattachedWidget(this.runtime, docPtr, pageIndex, fitted, numbers.objectNumber);
@@ -13314,14 +13692,14 @@ function updateField(ctx, op, opIndex) {
13314
13692
  const before = readField(ctx, op.field);
13315
13693
  if (op.patch.groupId !== void 0 && op.patch.groupId !== before.field.groupId) authorizeFieldGroup(ctx.authority, op.patch.groupId);
13316
13694
  assertExpected(opIndex, propertiesOf(before.field), op.expect);
13317
- return writeUpdate(ctx, before, op.patch);
13695
+ return writeUpdate$1(ctx, before, op.patch);
13318
13696
  }
13319
13697
  /** `forms.delete`; its reverse brings back the field, its widgets and the parents it pruned. */
13320
13698
  function deleteField(ctx, op, opIndex) {
13321
13699
  authorizeCapability(ctx.authority, "doc.forms.modify");
13322
13700
  const before = readField(ctx, op.field);
13323
13701
  assertExpected(opIndex, before.field, op.expect);
13324
- return writeDelete(ctx, before);
13702
+ return writeDelete$2(ctx, before);
13325
13703
  }
13326
13704
  /**
13327
13705
  * `forms.addWidget`. Its reverse puts back the field and the pages the widget
@@ -13450,7 +13828,7 @@ function updateWidget(ctx, op, opIndex) {
13450
13828
  const current = readAnnotation(ctx, op.widget);
13451
13829
  if (current.subtype !== "widget") throw new EngineError(EngineErrorCode.InvalidArg, `${current.subtype} is not a widget: change it with doc.annotations.update`, { details: { field: "widget" } });
13452
13830
  assertExpected(opIndex, current, op.expect);
13453
- return writeUpdate$1(ctx, op.widget, {
13831
+ return writeUpdate$2(ctx, op.widget, {
13454
13832
  ...op.patch,
13455
13833
  subtype: "widget"
13456
13834
  }, "forms.updateWidget");
@@ -13726,7 +14104,7 @@ function revertProperties(ctx, fallback) {
13726
14104
  const keys = Object.keys(left);
13727
14105
  const kept = keys.filter((key) => valuesEqual(now[key], left[key]) && (key !== "name" || nameIsFree(ctx, current.field.name, String(restore.name))));
13728
14106
  if (kept.length === 0) return leftAlone(ctx, "forms.update");
13729
- const done = writeUpdate(ctx, current, pick$1(restore, kept));
14107
+ const done = writeUpdate$1(ctx, current, pick$1(restore, kept));
13730
14108
  const skipped = keys.filter((key) => !kept.includes(key));
13731
14109
  const item = done.item;
13732
14110
  if (skipped.length === 0 || isSkippedItem(item) || item.type !== "forms.update") return done;
@@ -13751,7 +14129,7 @@ function removeField(ctx, step) {
13751
14129
  if (step.rights) authorizeCapability(ctx.authority, "doc.forms.import");
13752
14130
  const current = tryReadFieldByNumber(ctx, step.objectNumber);
13753
14131
  if (!current || !valuesEqual(current.field, step.left)) return leftAlone(ctx, "forms.delete");
13754
- return writeDelete(ctx, current, step.rights);
14132
+ return writeDelete$2(ctx, current, step.rights);
13755
14133
  }
13756
14134
  /**
13757
14135
  * Brings back a field a delete removed. Left alone when another field took its
@@ -13886,7 +14264,7 @@ function writeEffect(ctx, type, write, before) {
13886
14264
  * An update of the field `before` reads, recording the dictionaries it writes
13887
14265
  * and the properties it changes: their old values, and the values it left.
13888
14266
  */
13889
- function writeUpdate(ctx, before, patch) {
14267
+ function writeUpdate$1(ctx, before, patch) {
13890
14268
  const ref = {
13891
14269
  kind: "objectNumber",
13892
14270
  objectNumber: before.objectNumber
@@ -13961,7 +14339,7 @@ function recordRevert(ctx, before, type, write, fallbackOf) {
13961
14339
  * A delete of the field `before` reads, capturing it first. Its restore
13962
14340
  * takes `rights` too.
13963
14341
  */
13964
- function writeDelete(ctx, before, rights) {
14342
+ function writeDelete$2(ctx, before, rights) {
13965
14343
  const capture = exportField(ctx.runtime, ctx.session.requireDocPtr(), before.objectNumber);
13966
14344
  const ref = {
13967
14345
  kind: "objectNumber",
@@ -14294,57 +14672,1153 @@ function entryOf(values, key) {
14294
14672
  function pick(values, keys) {
14295
14673
  return Object.fromEntries(keys.map((key) => [key, entryOf(values, key)]));
14296
14674
  }
14297
- //#endregion
14298
- //#region src/features/changes/ChangeApplier.ts
14299
14675
  /**
14300
- * Runs changes inside the job's layer transaction, which its caller opens and
14301
- * ends, so a failure anywhere leaves the document as it was: a change's ops in
14302
- * order (`apply`), or the steps of a recorded reverse (`undo`). Each op and
14303
- * step is checked against the change's authority as it runs, as its single
14304
- * verb's job checks it.
14305
- *
14306
- * Every op records the steps that reverse it as it runs, from what it found
14307
- * and wrote; the record lists them in reverse op order, so each step meets the
14308
- * document as the op after it left it.
14309
- */
14310
- var ChangeApplier = class {
14311
- constructor(runtime, session, fonts) {
14312
- this.runtime = runtime;
14313
- this.session = session;
14314
- this.fonts = fonts;
14676
+ * The outline's shape as the document links it now, by object number: each
14677
+ * item's parent ({@link TOP} for a top-level item) and each parent's
14678
+ * children in order. Only what a write needs: no titles, no targets. An item
14679
+ * that isn't an indirect object is left out with the items under it, a loop
14680
+ * ends its list, and items deeper than a read goes aren't in it, as every
14681
+ * read of the outline does.
14682
+ */
14683
+ var OutlineTree = class {
14684
+ constructor(fn, docPtr) {
14685
+ _defineProperty(this, "childrenByParent", /* @__PURE__ */ new Map());
14686
+ _defineProperty(this, "parentByItem", /* @__PURE__ */ new Map());
14687
+ const visit = (parentHandle, parent, depth) => {
14688
+ if (depth >= OUTLINE_READ_LIMITS.depth) return;
14689
+ const children = [];
14690
+ this.childrenByParent.set(parent, children);
14691
+ for (let handle = fn.FPDFBookmark_GetFirstChild(docPtr, parentHandle); handle; handle = fn.FPDFBookmark_GetNextSibling(docPtr, handle)) {
14692
+ const item = fn.EPDFOutline_GetObjectNumber(handle);
14693
+ if (!item) continue;
14694
+ if (this.parentByItem.has(item)) break;
14695
+ this.parentByItem.set(item, parent);
14696
+ children.push(item);
14697
+ visit(handle, item, depth + 1);
14698
+ }
14699
+ };
14700
+ visit(NULL_PTR, 0, 0);
14315
14701
  }
14316
- apply(ops, authority, signal) {
14317
- const ctx = this.context(authority, signal, (self) => opsFillOwnField(self, ops));
14318
- return collect(authority, ops.map((op, at) => () => runOp(ctx, op, at)), signal);
14702
+ has(item) {
14703
+ return this.parentByItem.has(item);
14319
14704
  }
14320
- undo(record, authority, signal) {
14321
- const ctx = this.context(authority, signal, (self) => stepsFillOwnField(self, record.steps));
14322
- return collect(authority, record.steps.map((step) => () => runStep(ctx, step)), signal);
14705
+ /** The item's parent ({@link TOP} at the top level); `undefined` when it isn't in the outline. */
14706
+ parentOf(item) {
14707
+ return this.parentByItem.get(item);
14323
14708
  }
14324
- context(authority, signal, ownFill) {
14325
- let answer;
14326
- const ctx = {
14327
- runtime: this.runtime,
14328
- session: this.session,
14329
- ...this.fonts ? { fonts: this.fonts } : {},
14330
- authority,
14331
- signal,
14332
- fillsOwnField: () => answer ?? (answer = ownFill(ctx))
14333
- };
14334
- return ctx;
14709
+ /** A parent's children in order: none for a leaf, or an item the outline doesn't have. */
14710
+ childrenOf(parent) {
14711
+ return this.childrenByParent.get(parent) ?? [];
14335
14712
  }
14336
- };
14337
- /** Runs each in order; the record reverses them in the opposite order. */
14338
- function collect(authority, runs, signal) {
14339
- const items = [];
14340
- const reverses = [];
14341
- for (const run of runs) {
14342
- throwIfAborted(signal);
14343
- const done = run();
14344
- items.push(done.item);
14345
- reverses.push(done.reverse);
14713
+ /** The item and the items under it, depth first: the item first. */
14714
+ subtreeOf(item) {
14715
+ const items = [item];
14716
+ for (const child of this.childrenOf(item)) items.push(...this.subtreeOf(child));
14717
+ return items;
14346
14718
  }
14347
- return {
14719
+ /** How deep the item sits: 0 at the top level; `undefined` when it isn't in the outline. */
14720
+ levelOf(item) {
14721
+ let level = -1;
14722
+ for (let current = item; current; current = this.parentOf(current)) level += 1;
14723
+ return this.has(item) ? level : void 0;
14724
+ }
14725
+ /** Whether `node` is `item` or one of the items under it. */
14726
+ isAtOrUnder(node, item) {
14727
+ for (let current = node; current; current = this.parentOf(current)) if (current === item) return true;
14728
+ return false;
14729
+ }
14730
+ /** Every item, depth first: what an order of the whole outline is. */
14731
+ items() {
14732
+ return this.childrenOf(0).flatMap((item) => this.subtreeOf(item));
14733
+ }
14734
+ };
14735
+ //#endregion
14736
+ //#region src/features/outline/internal/readOutlineItem.ts
14737
+ /**
14738
+ * What one read of an outline may spend on its items' actions: one action
14739
+ * model per item at most, and room for a URI or a script on each. Past it,
14740
+ * an item's action reads as missing and the outline as incomplete.
14741
+ */
14742
+ const OUTLINE_ACTION_READ_BUDGET = {
14743
+ maxModels: 5e4,
14744
+ maxNodes: 2e5,
14745
+ maxScriptCodeUnits: 16e6,
14746
+ maxTargetEntries: 65536,
14747
+ maxPayloadCodeUnits: 16e6
14748
+ };
14749
+ /** `/F` bit 1. */
14750
+ const ITALIC$1 = 1;
14751
+ /** `/F` bit 2. */
14752
+ const BOLD$1 = 2;
14753
+ /** A budget for one read of the outline's actions. */
14754
+ function outlineActionBudget() {
14755
+ return new ActionReadBudgetTracker(OUTLINE_ACTION_READ_BUDGET);
14756
+ }
14757
+ /**
14758
+ * The item `handle` reads, at `objectNumber` under `parent` ({@link TOP} for
14759
+ * the top level). Its target is read the way a link's is: the `/A` tree's
14760
+ * navigation view, else the `/Dest`; an action that can't be read makes the
14761
+ * target `unsupported`.
14762
+ */
14763
+ function readOutlineItem(runtime, docPtr, handle, objectNumber, parent, budget) {
14764
+ const { fn, mem } = runtime;
14765
+ const title = readUtf16String(mem, (buf, capacity) => fn.FPDFBookmark_GetTitle(handle, buf, capacity)) ?? "";
14766
+ const hasChildren = fn.FPDFBookmark_GetFirstChild(docPtr, handle) !== NULL_PTR;
14767
+ const style = fn.FPDFBookmark_GetStyle(handle);
14768
+ const { action, target, complete } = readTarget(runtime, docPtr, handle, budget);
14769
+ return {
14770
+ item: {
14771
+ ref: toOutlineItemRef(objectNumber),
14772
+ parent: parent === 0 ? null : toOutlineItemRef(parent),
14773
+ title,
14774
+ target,
14775
+ action,
14776
+ open: hasChildren && fn.FPDFBookmark_GetCount(handle) > 0,
14777
+ color: readColor(runtime, handle),
14778
+ bold: (style & BOLD$1) !== 0,
14779
+ italic: (style & ITALIC$1) !== 0
14780
+ },
14781
+ complete
14782
+ };
14783
+ }
14784
+ function readTarget(runtime, docPtr, handle, budget) {
14785
+ const { fn, mem } = runtime;
14786
+ const actionPtr = fn.FPDFBookmark_GetAction(handle);
14787
+ if (actionPtr) try {
14788
+ const action = readActionModel(fn, mem, docPtr, fn.EPDFAction_LoadModel(actionPtr), budget);
14789
+ return {
14790
+ action,
14791
+ target: action ? linkTargetFromActionTree(action) : { kind: "unsupported" },
14792
+ complete: action !== null && !action.incomplete
14793
+ };
14794
+ } catch (error) {
14795
+ if (!EngineError.is(error)) throw error;
14796
+ return {
14797
+ action: null,
14798
+ target: { kind: "unsupported" },
14799
+ complete: false
14800
+ };
14801
+ }
14802
+ const destPtr = fn.FPDFBookmark_GetDest(docPtr, handle);
14803
+ if (!destPtr) return {
14804
+ action: null,
14805
+ target: null,
14806
+ complete: true
14807
+ };
14808
+ const destination = readDestination(fn, mem, docPtr, destPtr);
14809
+ return {
14810
+ action: null,
14811
+ target: destination ? {
14812
+ kind: "goto",
14813
+ destination
14814
+ } : { kind: "unsupported" },
14815
+ complete: true
14816
+ };
14817
+ }
14818
+ /** `/C` as `#rrggbb`, or `null` when the item has none. */
14819
+ function readColor(runtime, handle) {
14820
+ const { fn, mem } = runtime;
14821
+ return withScratchN(mem, [
14822
+ 4,
14823
+ 4,
14824
+ 4
14825
+ ], ([r, g, b]) => {
14826
+ if (!fn.FPDFBookmark_GetColor(handle, r, g, b)) return null;
14827
+ const channel = (ptr) => Math.min(255, Math.max(0, Math.round(readF32(mem, ptr) * 255)));
14828
+ return colorOf(channel(r), channel(g), channel(b));
14829
+ });
14830
+ }
14831
+ //#endregion
14832
+ //#region src/features/outline/OutlineReader.ts
14833
+ /**
14834
+ * Synchronous read of the document's outline, shared by the local worker and
14835
+ * the server. Dictionary reads only: no page is loaded. It never throws for
14836
+ * a bad outline: what it can't read (a loop, an item that isn't an indirect
14837
+ * object, a broken action, past `OUTLINE_READ_LIMITS`) it leaves out or
14838
+ * marks, and says `incomplete`.
14839
+ */
14840
+ var OutlineReader = class {
14841
+ constructor(runtime, session) {
14842
+ this.runtime = runtime;
14843
+ this.session = session;
14844
+ }
14845
+ read(signal) {
14846
+ const { fn } = this.runtime;
14847
+ const docPtr = this.session.requireDocPtr();
14848
+ const budget = outlineActionBudget();
14849
+ const items = [];
14850
+ const seen = /* @__PURE__ */ new Set();
14851
+ let incomplete = false;
14852
+ const visit = (parentHandle, parent, depth) => {
14853
+ for (let handle = fn.FPDFBookmark_GetFirstChild(docPtr, parentHandle); handle; handle = fn.FPDFBookmark_GetNextSibling(docPtr, handle)) {
14854
+ throwIfAborted(signal);
14855
+ const objectNumber = fn.EPDFOutline_GetObjectNumber(handle);
14856
+ if (!objectNumber) {
14857
+ incomplete = true;
14858
+ continue;
14859
+ }
14860
+ if (seen.has(objectNumber)) {
14861
+ incomplete = true;
14862
+ break;
14863
+ }
14864
+ if (items.length >= OUTLINE_READ_LIMITS.items) {
14865
+ incomplete = true;
14866
+ return false;
14867
+ }
14868
+ seen.add(objectNumber);
14869
+ const read = readOutlineItem(this.runtime, docPtr, handle, objectNumber, parent, budget);
14870
+ items.push(read.item);
14871
+ if (!read.complete) incomplete = true;
14872
+ if (fn.FPDFBookmark_GetFirstChild(docPtr, handle) === NULL_PTR) continue;
14873
+ if (depth + 1 >= OUTLINE_READ_LIMITS.depth) {
14874
+ incomplete = true;
14875
+ continue;
14876
+ }
14877
+ if (!visit(handle, objectNumber, depth + 1)) return false;
14878
+ }
14879
+ return true;
14880
+ };
14881
+ visit(NULL_PTR, 0, 0);
14882
+ return {
14883
+ items,
14884
+ incomplete
14885
+ };
14886
+ }
14887
+ /** The item `objectNumber`, under `parent` ({@link TOP} for the top level), as it reads now. */
14888
+ readItem(objectNumber, parent) {
14889
+ const docPtr = this.session.requireDocPtr();
14890
+ const handle = this.runtime.fn.EPDFOutline_GetItem(docPtr, objectNumber);
14891
+ if (!handle) throw new EngineError(EngineErrorCode.NotFound, `no outline item ${objectNumber}`, { details: { field: "ref" } });
14892
+ return readOutlineItem(this.runtime, docPtr, handle, objectNumber, parent, outlineActionBudget()).item;
14893
+ }
14894
+ };
14895
+ //#endregion
14896
+ //#region src/features/outline/internal/writeOutlineFields.ts
14897
+ /** `/F` bit 1. */
14898
+ const ITALIC = 1;
14899
+ /** `/F` bit 2. */
14900
+ const BOLD = 2;
14901
+ /** The most a PDF text string holds in every PDF app (ISO 32000-2 Annex C). */
14902
+ const MAX_TITLE_LENGTH = 32767;
14903
+ const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
14904
+ /**
14905
+ * Refuses, before anything is written, fields no write can take: a title
14906
+ * too long, a colour that isn't `#rrggbb`, a target a write can't make.
14907
+ */
14908
+ function assertWritableFields(fields) {
14909
+ const refuse = (field, message) => {
14910
+ throw new EngineError(EngineErrorCode.InvalidArg, message, { details: { field } });
14911
+ };
14912
+ if (fields.title !== void 0) {
14913
+ if (typeof fields.title !== "string") refuse("title", "a title is a string");
14914
+ if (fields.title.length > MAX_TITLE_LENGTH) refuse("title", `a title holds at most ${MAX_TITLE_LENGTH} characters`);
14915
+ }
14916
+ if (fields.color != null && !HEX_COLOR.test(fields.color)) refuse("color", "expected a color as '#rrggbb'");
14917
+ const target = fields.target;
14918
+ if (target != null) {
14919
+ if (!(target.kind === "goto" || target.kind === "uri" || target.kind === "named" && [
14920
+ "NextPage",
14921
+ "PrevPage",
14922
+ "FirstPage",
14923
+ "LastPage"
14924
+ ].includes(target.name ?? ""))) refuse("target", `a '${target.kind}' target can't be written`);
14925
+ }
14926
+ }
14927
+ /**
14928
+ * Writes the fields `fields` names on the item `objectNumber`; the others
14929
+ * stay as they are. A target is always written as `/A`, as a link's is.
14930
+ */
14931
+ function writeOutlineFields(runtime, docPtr, objectNumber, fields) {
14932
+ const { fn, mem } = runtime;
14933
+ const check = (ok, what) => {
14934
+ if (!ok) throw new EngineError(EngineErrorCode.Unknown, `the ${what} of outline item ${objectNumber} could not be written`);
14935
+ };
14936
+ if (fields.title !== void 0) {
14937
+ const title = fields.title;
14938
+ check(withUtf16String(mem, title, (ptr) => fn.EPDFOutline_SetTitle(docPtr, objectNumber, ptr)), "title");
14939
+ }
14940
+ if (fields.target !== void 0) {
14941
+ const action = fields.target === null ? NULL_PTR : createLinkTargetAction(fn, mem, docPtr, fields.target);
14942
+ check(fn.EPDFOutline_SetAction(docPtr, objectNumber, action), "target");
14943
+ }
14944
+ if (fields.color !== void 0) if (fields.color === null) check(fn.EPDFOutline_SetColor(docPtr, objectNumber, false, 0, 0, 0), "colour");
14945
+ else {
14946
+ const { r, g, b } = rgbOf(fields.color);
14947
+ check(fn.EPDFOutline_SetColor(docPtr, objectNumber, true, r / 255, g / 255, b / 255), "colour");
14948
+ }
14949
+ if (fields.bold !== void 0 || fields.italic !== void 0) {
14950
+ let flags = fn.FPDFBookmark_GetStyle(fn.EPDFOutline_GetItem(docPtr, objectNumber));
14951
+ if (fields.bold !== void 0) flags = fields.bold ? flags | BOLD : flags & -3;
14952
+ if (fields.italic !== void 0) flags = fields.italic ? flags | ITALIC : flags & -2;
14953
+ check(fn.EPDFOutline_SetStyle(docPtr, objectNumber, flags), "style");
14954
+ }
14955
+ if (fields.open !== void 0) check(fn.EPDFOutline_SetOpen(docPtr, objectNumber, fields.open), "open state");
14956
+ }
14957
+ //#endregion
14958
+ //#region src/features/outline/OutlineMutator.ts
14959
+ /**
14960
+ * Synchronous outline writes, shared by the local worker and the server.
14961
+ * Each checks everything it can before the first native write, and answers
14962
+ * with what it wrote, re-read. The fork keeps the links and `/Count` right
14963
+ * (public/epdf_outline.h); what goes where is decided here.
14964
+ */
14965
+ var OutlineMutator = class {
14966
+ constructor(runtime, session) {
14967
+ this.runtime = runtime;
14968
+ this.session = session;
14969
+ _defineProperty(this, "reader", void 0);
14970
+ this.reader = new OutlineReader(runtime, session);
14971
+ }
14972
+ /** The outline's shape as it is now. */
14973
+ tree() {
14974
+ return new OutlineTree(this.runtime.fn, this.session.requireDocPtr());
14975
+ }
14976
+ create(parent, position, data, objectNumber) {
14977
+ assertWritableFields(data);
14978
+ const tree = this.tree();
14979
+ const parentNumber = this.parentNumberOf(tree, parent);
14980
+ const children = tree.childrenOf(parentNumber);
14981
+ const at = positionIndex(children.map(toOutlineItemRef), position, keyOf);
14982
+ const after = at === 0 ? 0 : children[at - 1];
14983
+ if (objectNumber !== void 0) this.session.claimObjectNumber(objectNumber);
14984
+ const { fn, mem } = this.runtime;
14985
+ const docPtr = this.session.requireDocPtr();
14986
+ const created = withUtf16String(mem, data.title, (title) => fn.EPDFOutline_CreateItem(docPtr, objectNumber ?? 0, parentNumber, after, title));
14987
+ if (!created) throw outlineRefused("create the item");
14988
+ const { title: _title, ...fields } = data;
14989
+ writeOutlineFields(this.runtime, docPtr, created, fields);
14990
+ const item = this.reader.readItem(created, parentNumber);
14991
+ return {
14992
+ item,
14993
+ order: this.orderOf(this.tree(), parentNumber),
14994
+ meta: this.meta([item.ref])
14995
+ };
14996
+ }
14997
+ update(ref, patch) {
14998
+ assertWritableFields(patch);
14999
+ const tree = this.tree();
15000
+ const parent = this.requireItem(tree, ref, "ref");
15001
+ writeOutlineFields(this.runtime, this.session.requireDocPtr(), ref.objectNumber, patch);
15002
+ return {
15003
+ item: this.reader.readItem(ref.objectNumber, parent),
15004
+ meta: this.meta([ref])
15005
+ };
15006
+ }
15007
+ /**
15008
+ * The items go together, in the order given, under `parent` at `position`
15009
+ * among its children; the rules of a reorder (`reorderedList`) hold.
15010
+ */
15011
+ move(refs, parent, position) {
15012
+ const tree = this.tree();
15013
+ const items = refs.map((ref) => {
15014
+ this.requireItem(tree, ref, "refs");
15015
+ return ref.objectNumber;
15016
+ });
15017
+ const moving = new Set(items);
15018
+ if (moving.size !== items.length) invalid("refs", "an item is named twice");
15019
+ for (const item of items) if (items.some((other) => other !== item && tree.isAtOrUnder(item, other))) invalid("refs", `item ${item} is under another item that moves`);
15020
+ const parentNumber = this.parentNumberOf(tree, parent);
15021
+ if (items.some((item) => tree.isAtOrUnder(parentNumber, item))) invalid("parent", "an item would go under itself");
15022
+ const staying = tree.childrenOf(parentNumber).filter((child) => !moving.has(child));
15023
+ const anchor = position === "start" || position === "end" ? null : "before" in position ? position.before : position.after;
15024
+ if (anchor && moving.has(anchor.objectNumber)) invalid("position", `the neighbour ${anchor.objectNumber} is one of the items that move`);
15025
+ const at = positionIndex(staying.map(toOutlineItemRef), position, keyOf);
15026
+ let after = at === 0 ? 0 : staying[at - 1];
15027
+ return this.place(items.map((item) => {
15028
+ const placement = {
15029
+ item,
15030
+ parent: parentNumber,
15031
+ after
15032
+ };
15033
+ after = item;
15034
+ return placement;
15035
+ }), tree);
15036
+ }
15037
+ /**
15038
+ * Puts items where `placements` say, in order: each right after its
15039
+ * `after` while that is a child of its parent, else first. What an undo of
15040
+ * a move does; the caller has checked that each item and parent is there.
15041
+ */
15042
+ place(placements, before = this.tree()) {
15043
+ const { fn } = this.runtime;
15044
+ const docPtr = this.session.requireDocPtr();
15045
+ const parents = /* @__PURE__ */ new Set();
15046
+ for (const { item, parent, after } of placements) {
15047
+ parents.add(before.parentOf(item) ?? 0);
15048
+ parents.add(parent);
15049
+ const now = this.tree();
15050
+ const anchor = after && now.parentOf(after) === parent ? after : 0;
15051
+ if (!fn.EPDFOutline_MoveItem(docPtr, item, parent, anchor)) throw outlineRefused(`move item ${item}`);
15052
+ }
15053
+ const after = this.tree();
15054
+ return {
15055
+ orders: [...parents].map((parent) => this.orderOf(after, parent)),
15056
+ meta: this.meta(placements.map(({ item }) => toOutlineItemRef(item)))
15057
+ };
15058
+ }
15059
+ /** Where `items` are now, in document order: an anchor always comes before what follows it. */
15060
+ placementsOf(items, tree = this.tree()) {
15061
+ const wanted = new Set(items);
15062
+ return tree.items().filter((item) => wanted.has(item)).map((item) => {
15063
+ const parent = tree.parentOf(item);
15064
+ const siblings = tree.childrenOf(parent);
15065
+ const index = siblings.indexOf(item);
15066
+ return {
15067
+ item,
15068
+ parent,
15069
+ after: index > 0 ? siblings[index - 1] : 0
15070
+ };
15071
+ });
15072
+ }
15073
+ /** The item goes, with the items under it. */
15074
+ delete(ref) {
15075
+ const tree = this.tree();
15076
+ this.requireItem(tree, ref, "ref");
15077
+ const going = tree.subtreeOf(ref.objectNumber);
15078
+ if (!this.runtime.fn.EPDFOutline_DeleteItem(this.session.requireDocPtr(), ref.objectNumber)) throw outlineRefused(`delete item ${ref.objectNumber}`);
15079
+ return { meta: this.meta(going.map(toOutlineItemRef)) };
15080
+ }
15081
+ /**
15082
+ * The items a capture brought back (`importOutlineItem`, run by the
15083
+ * caller): the item first, then the items under it, and its parent's
15084
+ * children.
15085
+ */
15086
+ restored(objectNumber) {
15087
+ const tree = this.tree();
15088
+ const parent = tree.parentOf(objectNumber);
15089
+ if (parent === void 0) throw outlineRefused(`restore item ${objectNumber}`);
15090
+ const items = this.subtree(objectNumber, tree);
15091
+ return {
15092
+ items,
15093
+ order: this.orderOf(tree, parent),
15094
+ meta: this.meta(items.map((item) => item.ref))
15095
+ };
15096
+ }
15097
+ /** The item as it reads now; `null` when the outline has no such item. */
15098
+ find(ref, tree = this.tree()) {
15099
+ const parent = tree.parentOf(ref.objectNumber);
15100
+ return parent === void 0 ? null : this.reader.readItem(ref.objectNumber, parent);
15101
+ }
15102
+ /** The item and the items under it, depth first, as they read now. */
15103
+ subtree(objectNumber, tree = this.tree()) {
15104
+ return tree.subtreeOf(objectNumber).map((item) => this.reader.readItem(item, tree.parentOf(item)));
15105
+ }
15106
+ /** An update's answer for an item written some other way: a revert put it back. */
15107
+ updated(ref) {
15108
+ const item = this.find(ref);
15109
+ if (!item) throw outlineRefused(`read item ${ref.objectNumber}`);
15110
+ return {
15111
+ item,
15112
+ meta: this.meta([ref])
15113
+ };
15114
+ }
15115
+ /**
15116
+ * Counts the items above `ref` again from the tree, as every outline write
15117
+ * does: a revert puts back the item's own dictionary, its `/Count` with
15118
+ * it, and nothing above it. Setting the open state it reads changes
15119
+ * nothing else.
15120
+ */
15121
+ countAgain(ref) {
15122
+ const item = this.find(ref);
15123
+ if (!item) return;
15124
+ writeOutlineFields(this.runtime, this.session.requireDocPtr(), ref.objectNumber, { open: item.open });
15125
+ }
15126
+ /**
15127
+ * The number a parent ref names: {@link TOP} for `null`; `NotFound` when it
15128
+ * isn't an item; `InvalidArg` for an item at the deepest level a read goes
15129
+ * to, whose children no read shows.
15130
+ */
15131
+ parentNumberOf(tree, parent) {
15132
+ if (parent === null) return 0;
15133
+ this.requireItem(tree, parent, "parent");
15134
+ if ((tree.levelOf(parent.objectNumber) ?? 0) >= OUTLINE_READ_LIMITS.depth - 1) invalid("parent", `an item under ${parent.objectNumber} would be deeper than reads go`);
15135
+ return parent.objectNumber;
15136
+ }
15137
+ /** The item's parent; `NotFound` when the outline has no such item. */
15138
+ requireItem(tree, ref, field) {
15139
+ const parent = tree.parentOf(ref.objectNumber);
15140
+ if (parent === void 0) throw new EngineError(EngineErrorCode.NotFound, `no outline item ${ref.objectNumber}`, { details: { field } });
15141
+ return parent;
15142
+ }
15143
+ orderOf(tree, parent) {
15144
+ return {
15145
+ parent: parent === 0 ? null : toOutlineItemRef(parent),
15146
+ children: tree.childrenOf(parent).map(toOutlineItemRef)
15147
+ };
15148
+ }
15149
+ meta(changed) {
15150
+ return {
15151
+ affectedPages: [],
15152
+ cacheDelta: null,
15153
+ ...this.session.writeStamp(),
15154
+ changed
15155
+ };
15156
+ }
15157
+ };
15158
+ const keyOf = (ref) => String(ref.objectNumber);
15159
+ function invalid(field, message) {
15160
+ throw new EngineError(EngineErrorCode.InvalidArg, message, { details: { field } });
15161
+ }
15162
+ /**
15163
+ * The fork refused a write the checks above let through: a list it would
15164
+ * change holds an item that isn't an indirect object, or `/Root /Outlines`
15165
+ * is a direct dictionary.
15166
+ */
15167
+ function outlineRefused(what) {
15168
+ return new EngineError(EngineErrorCode.MalformedPdf, `could not ${what}: the outline holds an item that isn't an indirect object`);
15169
+ }
15170
+ //#endregion
15171
+ //#region src/features/changes/internal/outlineChanges.ts
15172
+ /** The fields an update can change, each with a value a revert can put back. */
15173
+ const FIELDS = [
15174
+ "title",
15175
+ "target",
15176
+ "color",
15177
+ "bold",
15178
+ "italic",
15179
+ "open"
15180
+ ];
15181
+ /** What a capture of an item follows: its action, and its destination. */
15182
+ const TARGET_KEYS = ["A", "Dest"];
15183
+ const mutatorOf$1 = (ctx) => new OutlineMutator(ctx.runtime, ctx.session);
15184
+ const authorize$1 = (ctx) => authorizeCapability(ctx.authority, "doc.outline.modify");
15185
+ /** `outline.create`; its reverse deletes the item while nobody changed it since. */
15186
+ function createOutlineItem(ctx, op) {
15187
+ authorize$1(ctx);
15188
+ const result = mutatorOf$1(ctx).create(op.parent, op.position ?? "end", op.data, op.objectNumber);
15189
+ return {
15190
+ item: {
15191
+ type: "outline.create",
15192
+ ...result
15193
+ },
15194
+ reverse: [{
15195
+ kind: "outline.remove",
15196
+ ref: result.item.ref,
15197
+ left: guardOf(ctx, [result.item.ref])
15198
+ }]
15199
+ };
15200
+ }
15201
+ /**
15202
+ * `outline.update`; its reverse puts the item's dictionary back as it was,
15203
+ * or, when someone changed it since, the fields still holding what it set.
15204
+ */
15205
+ function updateOutlineItem(ctx, op, opIndex) {
15206
+ authorize$1(ctx);
15207
+ const before = requireItem(ctx, op.ref);
15208
+ assertExpected(opIndex, before, op.expect);
15209
+ return writeUpdate(ctx, op.ref, op.patch, before);
15210
+ }
15211
+ /**
15212
+ * `outline.move`; its reverse puts each item back beside the sibling it
15213
+ * followed, and opens again a parent it emptied that was open.
15214
+ */
15215
+ function moveOutlineItems(ctx, op) {
15216
+ authorize$1(ctx);
15217
+ const mutator = mutatorOf$1(ctx);
15218
+ const tree = mutator.tree();
15219
+ const items = op.refs.map((ref) => ref.objectNumber);
15220
+ const back = mutator.placementsOf(items, tree);
15221
+ const reopens = reopensOf(ctx, tree, items);
15222
+ const result = mutator.move(op.refs, op.parent, op.position);
15223
+ const parent = op.parent?.objectNumber ?? 0;
15224
+ return {
15225
+ item: {
15226
+ type: "outline.move",
15227
+ ...result
15228
+ },
15229
+ reverse: [{
15230
+ kind: "outline.move",
15231
+ placements: back,
15232
+ left: items.map((item) => ({
15233
+ item,
15234
+ parent
15235
+ }))
15236
+ }, ...reopens]
15237
+ };
15238
+ }
15239
+ /** `outline.delete`; its reverse brings the item and the items under it back. */
15240
+ function deleteOutlineItem(ctx, op, opIndex) {
15241
+ authorize$1(ctx);
15242
+ assertExpected(opIndex, requireItem(ctx, op.ref), op.expect);
15243
+ return writeDelete$1(ctx, op.ref);
15244
+ }
15245
+ /** Puts an item's dictionary back, or the fields that still show the update (see `updateOutlineItem`). */
15246
+ function revertOutlineItem(ctx, step) {
15247
+ const ref = step.subject.outlineItem;
15248
+ const mutator = mutatorOf$1(ctx);
15249
+ const current = mutator.find(ref);
15250
+ if (!current) return leftAlone(ctx, "outline.update");
15251
+ authorize$1(ctx);
15252
+ if (unchangedSince(ctx.runtime, ctx.session, step.objects)) {
15253
+ const reverted = revertObjects(ctx.runtime, ctx.session, step.objects);
15254
+ mutator.countAgain(ref);
15255
+ const redo = captureAfter(ctx.runtime, ctx.session, reverted);
15256
+ return {
15257
+ item: {
15258
+ type: "outline.update",
15259
+ ...mutator.updated(ref)
15260
+ },
15261
+ reverse: [{
15262
+ ...step,
15263
+ objects: redo,
15264
+ ...swapped(step.fallback)
15265
+ }]
15266
+ };
15267
+ }
15268
+ const fallback = step.fallback;
15269
+ if (fallback?.kind !== "outline") return leftAlone(ctx, "outline.update");
15270
+ const now = current;
15271
+ const left = fallback.left;
15272
+ const restore = fallback.restore;
15273
+ const keys = Object.keys(left);
15274
+ const kept = keys.filter((key) => valuesEqual(now[key], left[key]) && writable(ctx, key, restore[key]));
15275
+ if (kept.length === 0) return leftAlone(ctx, "outline.update");
15276
+ const done = writeUpdate(ctx, ref, Object.fromEntries(kept.map((key) => [key, restore[key]])), current);
15277
+ const skipped = keys.filter((key) => !kept.includes(key));
15278
+ if (skipped.length === 0 || done.item.type !== "outline.update") return done;
15279
+ return {
15280
+ ...done,
15281
+ item: {
15282
+ ...done.item,
15283
+ skipped
15284
+ }
15285
+ };
15286
+ }
15287
+ /**
15288
+ * Deletes what a create or a restore brought back, while the item and the
15289
+ * items under it are the same ones and read exactly as the change left them.
15290
+ */
15291
+ function removeOutlineItem(ctx, step) {
15292
+ const tree = mutatorOf$1(ctx).tree();
15293
+ if (!tree.has(step.ref.objectNumber)) return leftAlone(ctx, "outline.delete");
15294
+ authorize$1(ctx);
15295
+ if (!valuesEqual(tree.subtreeOf(step.ref.objectNumber), step.left.map((object) => object.objectNumber)) || !unchangedSince(ctx.runtime, ctx.session, step.left)) return leftAlone(ctx, "outline.delete");
15296
+ return writeDelete$1(ctx, step.ref);
15297
+ }
15298
+ /** Brings back what a delete removed, while its parent is there and it isn't back already. */
15299
+ function restoreOutlineItem(ctx, step) {
15300
+ const mutator = mutatorOf$1(ctx);
15301
+ const tree = mutator.tree();
15302
+ if (tree.has(step.ref.objectNumber)) return leftAlone(ctx, "outline.restore");
15303
+ if (step.parent && !tree.has(step.parent.objectNumber)) return leftAlone(ctx, "outline.restore");
15304
+ authorize$1(ctx);
15305
+ importOutlineItem(ctx.runtime, ctx.session.requireDocPtr(), step.capture);
15306
+ const result = mutator.restored(step.ref.objectNumber);
15307
+ return {
15308
+ item: {
15309
+ type: "outline.restore",
15310
+ ...result
15311
+ },
15312
+ reverse: [{
15313
+ kind: "outline.remove",
15314
+ ref: step.ref,
15315
+ left: guardOf(ctx, result.items.map((item) => item.ref))
15316
+ }]
15317
+ };
15318
+ }
15319
+ /**
15320
+ * Puts items back where a move found them: each still under the parent the
15321
+ * move left it under, its old parent still there, and not under itself.
15322
+ */
15323
+ function moveOutlineItemsBack(ctx, step) {
15324
+ const mutator = mutatorOf$1(ctx);
15325
+ const tree = mutator.tree();
15326
+ const leftUnder = new Map(step.left.map(({ item, parent }) => [item, parent]));
15327
+ const placements = step.placements.filter(({ item, parent }) => tree.parentOf(item) === leftUnder.get(item) && (parent === 0 || tree.has(parent)) && !tree.isAtOrUnder(parent, item));
15328
+ if (placements.length === 0) return leftAlone(ctx, "outline.move");
15329
+ authorize$1(ctx);
15330
+ const items = placements.map(({ item }) => item);
15331
+ const back = mutator.placementsOf(items, tree);
15332
+ const reopens = reopensOf(ctx, tree, items);
15333
+ return {
15334
+ item: {
15335
+ type: "outline.move",
15336
+ ...mutator.place(placements, tree)
15337
+ },
15338
+ reverse: [{
15339
+ kind: "outline.move",
15340
+ placements: back,
15341
+ left: placements.map(({ item, parent }) => ({
15342
+ item,
15343
+ parent
15344
+ }))
15345
+ }, ...reopens]
15346
+ };
15347
+ }
15348
+ /** Opens a parent again, as its step says (see `OutlineReopenStep`). */
15349
+ function reopenOutlineItem(ctx, step) {
15350
+ const mutator = mutatorOf$1(ctx);
15351
+ const tree = mutator.tree();
15352
+ const item = mutator.find(step.ref, tree);
15353
+ const children = tree.childrenOf(step.ref.objectNumber);
15354
+ const same = children.length === step.children.length && children.every((child) => step.children.includes(child));
15355
+ if (!item || item.open || !same) return leftAlone(ctx, "outline.update");
15356
+ authorize$1(ctx);
15357
+ return {
15358
+ item: {
15359
+ type: "outline.update",
15360
+ ...mutator.update(step.ref, { open: true })
15361
+ },
15362
+ reverse: []
15363
+ };
15364
+ }
15365
+ /** An update, captured so it can be put back exactly. */
15366
+ function writeUpdate(ctx, ref, patch, before) {
15367
+ const pending = captureBefore(ctx.runtime, ctx.session, [{
15368
+ objectNumber: ref.objectNumber,
15369
+ deep: TARGET_KEYS
15370
+ }]);
15371
+ const result = mutatorOf$1(ctx).update(ref, patch);
15372
+ const objects = captureAfter(ctx.runtime, ctx.session, pending);
15373
+ return {
15374
+ item: {
15375
+ type: "outline.update",
15376
+ ...result
15377
+ },
15378
+ reverse: [{
15379
+ kind: "objects.revert",
15380
+ reports: "outline.update",
15381
+ objects,
15382
+ subject: { outlineItem: ref },
15383
+ fallback: fallbackOf(before, result.item, patch)
15384
+ }]
15385
+ };
15386
+ }
15387
+ /**
15388
+ * A delete, captured in its transaction so an undo brings back the same
15389
+ * objects, and opens again the parent it emptied when it was open.
15390
+ */
15391
+ function writeDelete$1(ctx, ref) {
15392
+ const mutator = mutatorOf$1(ctx);
15393
+ const tree = mutator.tree();
15394
+ const parent = tree.parentOf(ref.objectNumber);
15395
+ const reopens = reopensOf(ctx, tree, [ref.objectNumber]);
15396
+ const capture = exportOutlineItem(ctx.runtime, ctx.session.requireDocPtr(), ref.objectNumber);
15397
+ return {
15398
+ item: {
15399
+ type: "outline.delete",
15400
+ ...mutator.delete(ref)
15401
+ },
15402
+ reverse: [{
15403
+ kind: "outline.restore",
15404
+ ref,
15405
+ parent: parent === void 0 || parent === 0 ? null : toOutlineItemRef(parent),
15406
+ capture
15407
+ }, ...reopens]
15408
+ };
15409
+ }
15410
+ /**
15411
+ * The parents that lose every child when `items` leave them, and are open:
15412
+ * each comes back closed with its children, so the undo opens it again.
15413
+ */
15414
+ function reopensOf(ctx, tree, items) {
15415
+ const leaving = new Set(items);
15416
+ const parents = new Set(items.map((item) => tree.parentOf(item) ?? 0));
15417
+ parents.delete(0);
15418
+ return [...parents].flatMap((parent) => {
15419
+ const children = tree.childrenOf(parent);
15420
+ if (!children.every((child) => leaving.has(child))) return [];
15421
+ if (!mutatorOf$1(ctx).find(toOutlineItemRef(parent), tree)?.open) return [];
15422
+ return [{
15423
+ kind: "outline.reopen",
15424
+ ref: toOutlineItemRef(parent),
15425
+ children
15426
+ }];
15427
+ });
15428
+ }
15429
+ /** The guard of items a change leaves: each dictionary as it reads now. */
15430
+ function guardOf(ctx, refs) {
15431
+ return captureAfter(ctx.runtime, ctx.session, refs.map((ref) => ({
15432
+ objectNumber: ref.objectNumber,
15433
+ deep: [],
15434
+ before: null
15435
+ })));
15436
+ }
15437
+ function requireItem(ctx, ref) {
15438
+ const item = mutatorOf$1(ctx).find(ref);
15439
+ if (!item) throw new EngineError(EngineErrorCode.NotFound, `no outline item ${ref.objectNumber}`, { details: { field: "ref" } });
15440
+ return item;
15441
+ }
15442
+ /** The fields an update changed: their values before, and the values it left. */
15443
+ function fallbackOf(before, after, patch) {
15444
+ const changed = FIELDS.filter((field) => patch[field] !== void 0 && !valuesEqual(before[field], after[field]));
15445
+ return {
15446
+ kind: "outline",
15447
+ restore: Object.fromEntries(changed.map((field) => [field, before[field]])),
15448
+ left: Object.fromEntries(changed.map((field) => [field, after[field]]))
15449
+ };
15450
+ }
15451
+ /**
15452
+ * Whether a value read back can be written again: a target another app made
15453
+ * may not be, nor one that goes to a page the document no longer has.
15454
+ */
15455
+ function writable(ctx, field, value) {
15456
+ if (field !== "target" || value === null) return true;
15457
+ const target = value;
15458
+ if (target.kind === "goto") {
15459
+ const page = target.destination?.page.objectNumber;
15460
+ return ctx.session.allRecords().some((record) => record.pageObjectNumber === page);
15461
+ }
15462
+ if (target.kind === "uri") return true;
15463
+ return target.kind === "named" && [
15464
+ "NextPage",
15465
+ "PrevPage",
15466
+ "FirstPage",
15467
+ "LastPage"
15468
+ ].includes(target.name ?? "");
15469
+ }
15470
+ function swapped(fallback) {
15471
+ if (fallback?.kind !== "outline") return fallback ? { fallback } : {};
15472
+ return { fallback: {
15473
+ kind: "outline",
15474
+ restore: fallback.left,
15475
+ left: fallback.restore
15476
+ } };
15477
+ }
15478
+ //#endregion
15479
+ //#region src/features/changes/internal/pageChanges.ts
15480
+ /** What a page's guard digests: the page, and what the layer holds of these. */
15481
+ const PAGE_DEEP_KEYS = [
15482
+ "Annots",
15483
+ "Contents",
15484
+ "Resources"
15485
+ ];
15486
+ /**
15487
+ * What it leaves out of the page's bytes: its place (/Parent), so moving
15488
+ * pages around it changes nothing, and its /Rotate, which counts by value
15489
+ * instead: turning a page and turning it back reads the same.
15490
+ */
15491
+ const PAGE_SKIPPED_KEYS = ["Parent", "Rotate"];
15492
+ /**
15493
+ * Runs a page op or step. It changes the page tree in place, so the
15494
+ * document's idle parsed pages close before and after it, as a
15495
+ * `contentWrite` job's do: no page that went, or came back, stays parsed.
15496
+ */
15497
+ function changingPageTree(ctx, write) {
15498
+ const pool = ctx.session.pagePool();
15499
+ pool.closeIdle();
15500
+ try {
15501
+ return write();
15502
+ } finally {
15503
+ pool.closeIdle();
15504
+ }
15505
+ }
15506
+ const mutatorOf = (ctx) => new PagesMutator(ctx.runtime, ctx.session);
15507
+ const inserterOf = (ctx) => new PagesInserter(ctx.runtime, ctx.session);
15508
+ const authorize = (ctx) => authorizeCapability(ctx.authority, "doc.pages.assemble");
15509
+ /** `pages.rotate`; its reverse turns each page back while it still has this rotation. */
15510
+ function rotatePages(ctx, op) {
15511
+ authorize(ctx);
15512
+ const before = rotationsOf(layoutOf(ctx));
15513
+ const result = mutatorOf(ctx).rotate([...op.pages], op.rotation, ctx.signal);
15514
+ const pages = op.pages.map((page) => page.objectNumber);
15515
+ return {
15516
+ item: {
15517
+ type: "pages.rotate",
15518
+ pages: [...op.pages],
15519
+ rotation: op.rotation,
15520
+ ...result
15521
+ },
15522
+ reverse: rotateSteps(pages, before, op.rotation)
15523
+ };
15524
+ }
15525
+ /** Turns pages to a step's rotation, those that still have the rotation it left. */
15526
+ function rotatePagesBack(ctx, step) {
15527
+ const now = rotationsOf(layoutOf(ctx));
15528
+ const pages = step.pages.filter((page) => now.get(page) === step.left);
15529
+ if (pages.length === 0) return leftAlone(ctx, "pages.rotate");
15530
+ authorize(ctx);
15531
+ const refs = pages.map(toPageRef);
15532
+ const result = mutatorOf(ctx).rotate(refs, step.rotation, ctx.signal);
15533
+ return {
15534
+ item: {
15535
+ type: "pages.rotate",
15536
+ pages: refs,
15537
+ rotation: step.rotation,
15538
+ ...result
15539
+ },
15540
+ reverse: [{
15541
+ kind: "pages.rotate",
15542
+ pages,
15543
+ rotation: step.left,
15544
+ left: step.rotation
15545
+ }]
15546
+ };
15547
+ }
15548
+ /** `pages.reorder`; its reverse puts each page back after the page it followed. */
15549
+ function reorderPages(ctx, op) {
15550
+ authorize(ctx);
15551
+ const pages = op.pages.map((page) => page.objectNumber);
15552
+ const back = placementsOf(orderOf(ctx), pages);
15553
+ return {
15554
+ item: {
15555
+ type: "pages.reorder",
15556
+ ...mutatorOf(ctx).reorder([...op.pages], op.position, ctx.signal)
15557
+ },
15558
+ reverse: [{
15559
+ kind: "pages.reorder",
15560
+ placements: back,
15561
+ left: placementsOf(orderOf(ctx), pages)
15562
+ }]
15563
+ };
15564
+ }
15565
+ /**
15566
+ * Puts pages back where a move found them: each still after the page the move
15567
+ * left it after, the page it goes after still there.
15568
+ */
15569
+ function reorderPagesBack(ctx, step) {
15570
+ const order = orderOf(ctx);
15571
+ const leftAfter = new Map(step.left.map(({ page, after }) => [page, after]));
15572
+ const placements = step.placements.filter(({ page, after }) => {
15573
+ const at = order.indexOf(page);
15574
+ return at >= 0 && (order[at - 1] ?? 0) === leftAfter.get(page) && (after === 0 || order.includes(after));
15575
+ });
15576
+ if (placements.length === 0) return leftAlone(ctx, "pages.reorder");
15577
+ authorize(ctx);
15578
+ const pages = placements.map(({ page }) => page);
15579
+ const back = placementsOf(order, pages);
15580
+ const mutator = mutatorOf(ctx);
15581
+ let layout = null;
15582
+ for (const { page, after } of placements) {
15583
+ const position = after === 0 ? "start" : { after: toPageRef(after) };
15584
+ ({layout} = mutator.reorder([toPageRef(page)], position, ctx.signal));
15585
+ }
15586
+ return {
15587
+ item: {
15588
+ type: "pages.reorder",
15589
+ pages: pages.map(toPageRef),
15590
+ layout,
15591
+ meta: metaOf(ctx)
15592
+ },
15593
+ reverse: [{
15594
+ kind: "pages.reorder",
15595
+ placements: back,
15596
+ left: placementsOf(orderOf(ctx), pages)
15597
+ }]
15598
+ };
15599
+ }
15600
+ /** `pages.delete`, captured in its transaction so an undo brings back the same pages. */
15601
+ function deletePages(ctx, op) {
15602
+ authorize(ctx);
15603
+ return writeDelete(ctx, inDisplayOrder(orderOf(ctx), ctx.session.resolvePageRefs(op.pages)));
15604
+ }
15605
+ /** Brings back pages a delete removed, those that aren't back already. */
15606
+ function restorePages(ctx, step) {
15607
+ const order = orderOf(ctx);
15608
+ const gone = step.pages.filter(({ page }) => !order.includes(page));
15609
+ if (gone.length === 0) return leftAlone(ctx, "pages.restore");
15610
+ authorize(ctx);
15611
+ const docPtr = ctx.session.requireDocPtr();
15612
+ for (const { capture } of gone) importPage(ctx.runtime, docPtr, capture);
15613
+ ctx.session.refreshPageRegistry();
15614
+ ctx.session.invalidateDerived();
15615
+ const pages = inDisplayOrder(orderOf(ctx), gone.map(({ page }) => page));
15616
+ return {
15617
+ item: {
15618
+ type: "pages.restore",
15619
+ pages: pages.map(toPageRef),
15620
+ layout: layoutOf(ctx),
15621
+ meta: metaOf(ctx)
15622
+ },
15623
+ reverse: [removeStepOf(ctx, pages)]
15624
+ };
15625
+ }
15626
+ /**
15627
+ * Deletes pages an insert or a restore brought, those that read as the change
15628
+ * left them; never every page of the document.
15629
+ */
15630
+ function removePages(ctx, step) {
15631
+ const order = orderOf(ctx);
15632
+ const pages = step.pages.filter(({ page, left }) => order.includes(page) && digestOf(ctx, page) === left).map(({ page }) => page);
15633
+ if (pages.length === 0 || pages.length >= order.length) return leftAlone(ctx, "pages.delete");
15634
+ authorize(ctx);
15635
+ return writeDelete(ctx, inDisplayOrder(order, pages));
15636
+ }
15637
+ /** `pages.insertBlank`; its reverse deletes the new pages while nobody changed them. */
15638
+ function insertBlankPages(ctx, op) {
15639
+ authorize(ctx);
15640
+ const result = inserterOf(ctx).insertBlank(op.spec, op.position ?? "end", ctx.signal, op.objectNumbers);
15641
+ const pages = result.insertedPages.map((page) => page.objectNumber);
15642
+ return {
15643
+ item: {
15644
+ type: "pages.insertBlank",
15645
+ ...result
15646
+ },
15647
+ reverse: [removeStepOf(ctx, pages)]
15648
+ };
15649
+ }
15650
+ /** `pages.insert`; its reverse deletes the pages it copied in while nobody changed them. */
15651
+ function insertPages(ctx, op) {
15652
+ authorize(ctx);
15653
+ const result = inserterOf(ctx).insert(bufferOf(op.source), op.position ?? "end", ctx.signal);
15654
+ ctx.session.invalidateDerived();
15655
+ const pages = result.insertedPages.map((page) => page.objectNumber);
15656
+ return {
15657
+ item: {
15658
+ type: "pages.insert",
15659
+ ...result
15660
+ },
15661
+ reverse: [removeStepOf(ctx, pages)]
15662
+ };
15663
+ }
15664
+ /** Deletes `pages`, in display order, each captured first so its restore brings it back. */
15665
+ function writeDelete(ctx, pages) {
15666
+ const docPtr = ctx.session.requireDocPtr();
15667
+ const captures = pages.map((page) => ({
15668
+ page,
15669
+ capture: exportPage(ctx.runtime, docPtr, page)
15670
+ }));
15671
+ const refs = pages.map(toPageRef);
15672
+ const result = mutatorOf(ctx).delete(refs, ctx.signal);
15673
+ ctx.session.invalidateDerived();
15674
+ return {
15675
+ item: {
15676
+ type: "pages.delete",
15677
+ pages: refs,
15678
+ ...result
15679
+ },
15680
+ reverse: [{
15681
+ kind: "pages.restore",
15682
+ pages: captures
15683
+ }]
15684
+ };
15685
+ }
15686
+ /** The step that deletes `pages` again while each reads as it does now. */
15687
+ function removeStepOf(ctx, pages) {
15688
+ return {
15689
+ kind: "pages.remove",
15690
+ pages: pages.map((page) => ({
15691
+ page,
15692
+ left: digestOf(ctx, page)
15693
+ }))
15694
+ };
15695
+ }
15696
+ /**
15697
+ * How a page reads now: the digest of its dictionary and of what the layer
15698
+ * holds of its annotations, contents and resources, as they read (canonical
15699
+ * bytes: a save that only encodes a stream again changes nothing), without
15700
+ * its place, and with its turn as a value.
15701
+ */
15702
+ function digestOf(ctx, page) {
15703
+ const { fn, mem } = ctx.runtime;
15704
+ const docPtr = ctx.session.requireDocPtr();
15705
+ const bytes = canonicalDict(ctx.runtime, docPtr, page, PAGE_DEEP_KEYS, PAGE_SKIPPED_KEYS);
15706
+ const index = ctx.session.allRecords().find((record) => record.pageObjectNumber === page);
15707
+ const turn = index ? rotationAt(ctx.runtime, docPtr, index.pageIndex) : 0;
15708
+ const turned = new TextEncoder().encode(`\nturn ${turn}`);
15709
+ const digested = new Uint8Array(bytes.byteLength + turned.byteLength);
15710
+ digested.set(bytes);
15711
+ digested.set(turned, bytes.byteLength);
15712
+ return sha256HexOf(fn, mem, digested);
15713
+ }
15714
+ /** The steps that turn each page back, one for each rotation the pages had. */
15715
+ function rotateSteps(pages, before, left) {
15716
+ const byRotation = /* @__PURE__ */ new Map();
15717
+ for (const page of pages) {
15718
+ const rotation = before.get(page);
15719
+ byRotation.set(rotation, [...byRotation.get(rotation) ?? [], page]);
15720
+ }
15721
+ return [...byRotation].map(([rotation, group]) => ({
15722
+ kind: "pages.rotate",
15723
+ pages: group,
15724
+ rotation,
15725
+ left
15726
+ }));
15727
+ }
15728
+ /** Where each of `pages` is in `order`: the page it follows (0: first), in display order. */
15729
+ function placementsOf(order, pages) {
15730
+ return inDisplayOrder(order, pages).map((page) => ({
15731
+ page,
15732
+ after: order[order.indexOf(page) - 1] ?? 0
15733
+ }));
15734
+ }
15735
+ /** `pages` sorted as `order` has them. */
15736
+ function inDisplayOrder(order, pages) {
15737
+ return [...pages].sort((left, right) => order.indexOf(left) - order.indexOf(right));
15738
+ }
15739
+ /** The document's pages by object number, in display order. */
15740
+ function orderOf(ctx) {
15741
+ return ctx.session.allRecords().map((record) => record.pageObjectNumber);
15742
+ }
15743
+ function layoutOf(ctx) {
15744
+ return new PagesReader(ctx.runtime, ctx.session).read(ctx.signal);
15745
+ }
15746
+ function rotationsOf(layout) {
15747
+ return new Map(layout.pages.map((page) => [page.ref.objectNumber, page.rotation]));
15748
+ }
15749
+ function metaOf(ctx) {
15750
+ return {
15751
+ affectedPages: [],
15752
+ cacheDelta: null,
15753
+ ...ctx.session.writeStamp()
15754
+ };
15755
+ }
15756
+ /** The bytes of `source` as an ArrayBuffer of their own. */
15757
+ function bufferOf(source) {
15758
+ return source.byteOffset === 0 && source.byteLength === source.buffer.byteLength ? source.buffer : source.slice().buffer;
15759
+ }
15760
+ //#endregion
15761
+ //#region src/features/changes/ChangeApplier.ts
15762
+ /**
15763
+ * The file's own coordinates, in and out: for a single verb's one-op change,
15764
+ * whose job converts at the worker's boundary.
15765
+ */
15766
+ const FILE_SPACE = {
15767
+ opInFileSpace: (op) => op,
15768
+ itemOut: (item) => item
15769
+ };
15770
+ /**
15771
+ * Runs changes inside the job's layer transaction, which its caller opens and
15772
+ * ends, so a failure anywhere leaves the document as it was: a change's ops in
15773
+ * order (`apply`), or the steps of a recorded reverse (`undo`). Each op and
15774
+ * step is checked against the change's authority as it runs, as its single
15775
+ * verb's job checks it.
15776
+ *
15777
+ * Every op records the steps that reverse it as it runs, from what it found
15778
+ * and wrote; the record lists them in reverse op order, so each step meets the
15779
+ * document as the op after it left it.
15780
+ */
15781
+ var ChangeApplier = class {
15782
+ constructor(runtime, session, fonts) {
15783
+ this.runtime = runtime;
15784
+ this.session = session;
15785
+ this.fonts = fonts;
15786
+ }
15787
+ apply(ops, authority, signal, space) {
15788
+ const ctx = this.context(authority, signal, (self) => opsFillOwnField(self, ops));
15789
+ return collect(authority, ops.map((op, at) => () => runOp(ctx, space.opInFileSpace(op), at)), space, signal);
15790
+ }
15791
+ undo(record, authority, signal, space) {
15792
+ const ctx = this.context(authority, signal, (self) => stepsFillOwnField(self, record.steps));
15793
+ return collect(authority, record.steps.map((step) => () => runStep(ctx, step)), space, signal);
15794
+ }
15795
+ context(authority, signal, ownFill) {
15796
+ let answer;
15797
+ const ctx = {
15798
+ runtime: this.runtime,
15799
+ session: this.session,
15800
+ ...this.fonts ? { fonts: this.fonts } : {},
15801
+ authority,
15802
+ signal,
15803
+ fillsOwnField: () => answer ?? (answer = ownFill(ctx))
15804
+ };
15805
+ return ctx;
15806
+ }
15807
+ };
15808
+ /**
15809
+ * Runs each in order, its item measured as soon as it ran; the record
15810
+ * reverses them in the opposite order.
15811
+ */
15812
+ function collect(authority, runs, space, signal) {
15813
+ const items = [];
15814
+ const reverses = [];
15815
+ for (const run of runs) {
15816
+ throwIfAborted(signal);
15817
+ const done = run();
15818
+ items.push(space.itemOut(done.item));
15819
+ reverses.push(done.reverse);
15820
+ }
15821
+ return {
14348
15822
  items,
14349
15823
  record: {
14350
15824
  userId: authority.identity.userId ?? null,
@@ -14377,17 +15851,34 @@ function runOp(ctx, op, at) {
14377
15851
  case "forms.importValues": return importFormValues(ctx, op);
14378
15852
  case "metadata.update": return updateMetadata(ctx, op, at);
14379
15853
  case "metadata.updateCustom": return updateCustomMetadata(ctx, op);
15854
+ case "outline.create": return createOutlineItem(ctx, op);
15855
+ case "outline.update": return updateOutlineItem(ctx, op, at);
15856
+ case "outline.move": return moveOutlineItems(ctx, op);
15857
+ case "outline.delete": return deleteOutlineItem(ctx, op, at);
15858
+ case "pages.rotate": return changingPageTree(ctx, () => rotatePages(ctx, op));
15859
+ case "pages.reorder": return changingPageTree(ctx, () => reorderPages(ctx, op));
15860
+ case "pages.delete": return changingPageTree(ctx, () => deletePages(ctx, op));
15861
+ case "pages.insertBlank": return changingPageTree(ctx, () => insertBlankPages(ctx, op));
15862
+ case "pages.insert": return changingPageTree(ctx, () => insertPages(ctx, op));
15863
+ case "attachments.create": return createAttachment(ctx, op);
15864
+ case "attachments.delete": return deleteAttachment(ctx, op);
14380
15865
  }
14381
15866
  }
14382
15867
  function runStep(ctx, step) {
14383
15868
  switch (step.kind) {
14384
- case "objects.revert": return "annotation" in step.subject ? revertAnnotation(ctx, {
14385
- ...step,
14386
- subject: step.subject
14387
- }) : revertForm(ctx, {
14388
- ...step,
14389
- subject: step.subject
14390
- });
15869
+ case "objects.revert":
15870
+ if ("annotation" in step.subject) return revertAnnotation(ctx, {
15871
+ ...step,
15872
+ subject: step.subject
15873
+ });
15874
+ if ("outlineItem" in step.subject) return revertOutlineItem(ctx, {
15875
+ ...step,
15876
+ subject: step.subject
15877
+ });
15878
+ return revertForm(ctx, {
15879
+ ...step,
15880
+ subject: step.subject
15881
+ });
14391
15882
  case "annotation.remove": return removeAnnotation(ctx, step);
14392
15883
  case "annotation.restore": return restoreAnnotations(ctx, step);
14393
15884
  case "annotation.reorder": return reorderBack(ctx, step);
@@ -14397,6 +15888,16 @@ function runStep(ctx, step) {
14397
15888
  case "widget.delete": return deleteRestoredWidget(ctx, step);
14398
15889
  case "calculations.restore": return restoreCalculationOrder(ctx, step);
14399
15890
  case "metadata.revert": return revertMetadata(ctx, step);
15891
+ case "outline.remove": return removeOutlineItem(ctx, step);
15892
+ case "outline.restore": return restoreOutlineItem(ctx, step);
15893
+ case "outline.move": return moveOutlineItemsBack(ctx, step);
15894
+ case "outline.reopen": return reopenOutlineItem(ctx, step);
15895
+ case "pages.rotate": return changingPageTree(ctx, () => rotatePagesBack(ctx, step));
15896
+ case "pages.reorder": return changingPageTree(ctx, () => reorderPagesBack(ctx, step));
15897
+ case "pages.restore": return changingPageTree(ctx, () => restorePages(ctx, step));
15898
+ case "pages.remove": return changingPageTree(ctx, () => removePages(ctx, step));
15899
+ case "attachments.remove": return removeAttachment(ctx, step);
15900
+ case "attachments.restore": return restoreAttachment(ctx, step);
14400
15901
  }
14401
15902
  }
14402
15903
  //#endregion
@@ -14416,6 +15917,19 @@ function recordBytes(record) {
14416
15917
  objects(step.unlinked);
14417
15918
  break;
14418
15919
  case "field.restore":
15920
+ case "outline.restore":
15921
+ bytes += step.capture.byteLength;
15922
+ break;
15923
+ case "outline.remove":
15924
+ objects(step.left);
15925
+ break;
15926
+ case "pages.restore":
15927
+ for (const { capture } of step.pages) bytes += capture.byteLength;
15928
+ break;
15929
+ case "attachments.remove":
15930
+ bytes += step.left.byteLength;
15931
+ break;
15932
+ case "attachments.restore":
14419
15933
  bytes += step.capture.byteLength;
14420
15934
  break;
14421
15935
  default: break;
@@ -14561,172 +16075,6 @@ function unpackChangeRecord(reverse, capture) {
14561
16075
  });
14562
16076
  }
14563
16077
  //#endregion
14564
- //#region src/features/attachments/AttachmentReader.ts
14565
- /**
14566
- * Read-only access to embedded files, at both of their homes: the
14567
- * document catalog's `/EmbeddedFiles` name tree (list / readFile by
14568
- * index) and a FileAttachment annotation's `/FS` (readAnnotationFile by
14569
- * ref). Pure reads over the session — no layer artifacts (the
14570
- * `PagesExtractor` shape).
14571
- *
14572
- * Byte delivery mirrors the request's `path?`: absent → a standalone
14573
- * buffer for the transfer list (browser); present → the decoded file is
14574
- * streamed to `path` via `FPDF_FILEWRITE` (server), and only metadata
14575
- * rides the response.
14576
- */
14577
- var AttachmentReader = class {
14578
- constructor(runtime, session) {
14579
- this.runtime = runtime;
14580
- this.session = session;
14581
- }
14582
- /** Snapshot of the `/EmbeddedFiles` name tree, in tree (key-sorted) order. */
14583
- list(signal) {
14584
- throwIfAborted(signal);
14585
- const { fn, mem } = this.runtime;
14586
- const docPtr = this.session.requireDocPtr();
14587
- const count = fn.FPDFDoc_GetAttachmentCount(docPtr);
14588
- const items = [];
14589
- for (let index = 0; index < count; index++) {
14590
- const attachmentPtr = fn.FPDFDoc_GetAttachment(docPtr, index);
14591
- if (!attachmentPtr) continue;
14592
- const key = readAttachmentKey(fn, mem, docPtr, index) ?? "";
14593
- items.push({
14594
- ...readAttachmentFileInfo(fn, mem, attachmentPtr),
14595
- ref: toAttachmentRef(key),
14596
- index
14597
- });
14598
- }
14599
- return items;
14600
- }
14601
- /** Decode one document-level embedded file, addressed by key. */
14602
- readFile(ref, path, maxDecodedBytes, signal) {
14603
- throwIfAborted(signal);
14604
- const { fn, mem } = this.runtime;
14605
- const docPtr = this.session.requireDocPtr();
14606
- const index = resolveAttachmentIndex(fn, mem, docPtr, ref);
14607
- if (index < 0) throw new EngineError(EngineErrorCode.NotFound, `no embedded file with key '${ref.key}' in the document`);
14608
- const attachmentPtr = fn.FPDFDoc_GetAttachment(docPtr, index);
14609
- if (!attachmentPtr) throw new EngineError(EngineErrorCode.Unknown, `embedded file with key '${ref.key}' resolved to index ${index} but could not be opened`);
14610
- const info = readAttachmentFileInfo(fn, mem, attachmentPtr);
14611
- return this.extract(attachmentPtr, info.name, info.mimeType ?? void 0, path, maxDecodedBytes);
14612
- }
14613
- /** Decode the file embedded in a FileAttachment annotation's `/FS`. */
14614
- /** A file attachment annotation's file; a page not parsed yet loads in slices. */
14615
- async readAnnotationFile(pageObjectNumber, ref, path, maxDecodedBytes, signal, slices) {
14616
- throwIfAborted(signal);
14617
- const { fn, mem } = this.runtime;
14618
- const pool = this.session.pagePool();
14619
- const pagePtr = await pool.acquireInSlices(pageObjectNumber, signal, slices);
14620
- let annotPtr = null;
14621
- try {
14622
- annotPtr = resolveAnnotPtr(this.runtime, this.session, pagePtr, ref);
14623
- const attachmentPtr = fn.FPDFAnnot_GetFileAttachment(annotPtr);
14624
- if (!attachmentPtr) throw new EngineError(EngineErrorCode.InvalidArg, "annotation is not a file attachment or carries no embedded file (/FS)");
14625
- const info = readAttachmentFileInfo(fn, mem, attachmentPtr);
14626
- return this.extract(attachmentPtr, info.name, info.mimeType ?? void 0, path, maxDecodedBytes);
14627
- } finally {
14628
- if (annotPtr !== null) fn.FPDFPage_CloseAnnot(annotPtr);
14629
- pool.release(pageObjectNumber);
14630
- }
14631
- }
14632
- extract(attachmentPtr, name, mimeType, path, maxDecodedBytes) {
14633
- if (path !== void 0) {
14634
- const { size } = extractAttachmentToFile(this.runtime, attachmentPtr, path, maxDecodedBytes);
14635
- return {
14636
- name,
14637
- ...mimeType ? { mimeType } : {},
14638
- size,
14639
- path
14640
- };
14641
- }
14642
- const { bytes, size } = extractAttachmentToBuffer(this.runtime, attachmentPtr, maxDecodedBytes);
14643
- return {
14644
- name,
14645
- ...mimeType ? { mimeType } : {},
14646
- size,
14647
- bytes
14648
- };
14649
- }
14650
- };
14651
- //#endregion
14652
- //#region src/features/attachments/AttachmentMutator.ts
14653
- /**
14654
- * Create/delete mutations over the document catalog's `/EmbeddedFiles`
14655
- * name tree — the doc-level sibling of `AnnotationMutator`, sharing the
14656
- * write path (`writeAttachmentFilePayload`) with the file-attachment
14657
- * annotation writer.
14658
- *
14659
- * Identity is the name-tree key (unique by construction). Note the
14660
- * tree is key-sorted: both mutations shift other entries' indices; keys
14661
- * never move. Layer persistence (`finishMutation`) and event publication
14662
- * happen in the callers, exactly like every other mutation family.
14663
- */
14664
- var AttachmentMutator = class {
14665
- constructor(runtime, session) {
14666
- this.runtime = runtime;
14667
- this.session = session;
14668
- }
14669
- /**
14670
- * Create an embedded file. `file.name` becomes the tree key (PDFium
14671
- * writes key = /UF = /F); a duplicate key rejects with `InvalidArg`.
14672
- * The result carries the read-back entry, `checksum`/`size`/dates
14673
- * included.
14674
- */
14675
- create(file, resources, signal) {
14676
- throwIfAborted(signal);
14677
- const { fn, mem } = this.runtime;
14678
- const docPtr = this.session.requireDocPtr();
14679
- const resource = resources?.[file.resources.file];
14680
- if (!resource) throw new EngineError(EngineErrorCode.InvalidArg, `attachment file references resource '${file.resources.file}' but no such binary payload accompanied the mutation`);
14681
- if (file.name.length === 0) throw new EngineError(EngineErrorCode.InvalidArg, "attachment file requires a name");
14682
- if (resolveAttachmentIndex(fn, mem, docPtr, {
14683
- kind: "key",
14684
- key: file.name
14685
- }) !== -1) throw new EngineError(EngineErrorCode.InvalidArg, `an embedded file with key '${file.name}' already exists — keys are unique; rename and retry`);
14686
- throwIfAborted(signal);
14687
- const namePtr = mem.writeU16String(file.name);
14688
- let attachmentPtr;
14689
- try {
14690
- attachmentPtr = fn.FPDFDoc_AddAttachment(docPtr, namePtr);
14691
- } finally {
14692
- mem.free(namePtr);
14693
- }
14694
- if (!attachmentPtr) throw new EngineError(EngineErrorCode.Unknown, "FPDFDoc_AddAttachment returned NULL");
14695
- writeAttachmentFilePayload(fn, mem, attachmentPtr, docPtr, file, resource);
14696
- const ref = toAttachmentRef(file.name);
14697
- const index = resolveAttachmentIndex(fn, mem, docPtr, ref);
14698
- if (index < 0) throw new EngineError(EngineErrorCode.Unknown, "created attachment did not resolve by key after the write");
14699
- return {
14700
- attachment: {
14701
- ...readAttachmentFileInfo(fn, mem, attachmentPtr),
14702
- ref,
14703
- index
14704
- },
14705
- meta: {
14706
- affectedPages: [],
14707
- cacheDelta: null,
14708
- ...this.session.writeStamp(),
14709
- changed: [ref]
14710
- }
14711
- };
14712
- }
14713
- /** Delete an embedded file by key. Unlinks the tree entry only. */
14714
- delete(ref, signal) {
14715
- throwIfAborted(signal);
14716
- const { fn, mem } = this.runtime;
14717
- const docPtr = this.session.requireDocPtr();
14718
- const index = resolveAttachmentIndex(fn, mem, docPtr, ref);
14719
- if (index < 0) throw new EngineError(EngineErrorCode.NotFound, `no embedded file with key '${ref.key}' in the document`);
14720
- if (!fn.FPDFDoc_DeleteAttachment(docPtr, index)) throw new EngineError(EngineErrorCode.Unknown, "FPDFDoc_DeleteAttachment returned false");
14721
- return { meta: {
14722
- affectedPages: [],
14723
- cacheDelta: null,
14724
- ...this.session.writeStamp(),
14725
- changed: [ref]
14726
- } };
14727
- }
14728
- };
14729
- //#endregion
14730
16078
  //#region src/features/security/internal/buildSecurityInfo.ts
14731
16079
  /**
14732
16080
  * Single constructor for the "successfully inspected" shape of
@@ -15561,11 +16909,6 @@ function assertSealedSignature(runtime, docPtr, model, expected) {
15561
16909
  function sameRange(a, b) {
15562
16910
  return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
15563
16911
  }
15564
- function bytesEqual(a, b) {
15565
- if (a.byteLength !== b.byteLength) return false;
15566
- for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
15567
- return true;
15568
- }
15569
16912
  //#endregion
15570
16913
  //#region src/features/forms/internal/fieldGroups.ts
15571
16914
  /** The group of the field `fieldObjectNumber`; `null` in none, or when the form has no such field. */
@@ -17095,13 +18438,8 @@ function resultInPageSpace(payload, boxOf) {
17095
18438
  ...payload,
17096
18439
  snapshot: pageListOf(payload.snapshot)
17097
18440
  };
17098
- case "pages.reorder":
17099
- case "pages.rotate":
17100
- case "pages.delete":
17101
18441
  case "pages.setName":
17102
- case "pages.removeName":
17103
- case "pages.insert":
17104
- case "pages.insertBlank": return {
18442
+ case "pages.removeName": return {
17105
18443
  ...payload,
17106
18444
  result: {
17107
18445
  ...payload.result,
@@ -17144,6 +18482,10 @@ function resultInPageSpace(payload, boxOf) {
17144
18482
  ...payload,
17145
18483
  snapshot: mapDocumentActions(payload.snapshot, toPage)
17146
18484
  };
18485
+ case "outline.read": return {
18486
+ ...payload,
18487
+ outline: pageOutlineOf(payload.outline, boxOf)
18488
+ };
17147
18489
  case "annotations.list": return {
17148
18490
  ...payload,
17149
18491
  list: {
@@ -17165,17 +18507,8 @@ function resultInPageSpace(payload, boxOf) {
17165
18507
  annotation: annotation(payload.result.annotation)
17166
18508
  }
17167
18509
  };
17168
- case "document.apply": return {
17169
- ...payload,
17170
- result: changeResultInPageSpace(payload.result, boxOf)
17171
- };
17172
- case "document.applyChanges": return {
17173
- ...payload,
17174
- outcomes: payload.outcomes.map((outcome) => outcome.status === "applied" ? {
17175
- ...outcome,
17176
- result: changeResultInPageSpace(outcome.result, boxOf)
17177
- } : outcome)
17178
- };
18510
+ case "document.apply":
18511
+ case "document.applyChanges": return payload;
17179
18512
  case "annotations.import": return {
17180
18513
  ...payload,
17181
18514
  result: {
@@ -17270,17 +18603,8 @@ function requestInFileSpace(job, boxOf) {
17270
18603
  ...job,
17271
18604
  draft: pdfFormFieldDraftOf(job.draft, boxOf)
17272
18605
  };
17273
- case "document.apply": return {
17274
- ...job,
17275
- change: changeInFileSpace(job.change, boxOf)
17276
- };
17277
- case "document.applyChanges": return {
17278
- ...job,
17279
- changes: job.changes.map((entry) => ({
17280
- ...entry,
17281
- change: changeInFileSpace(entry.change, boxOf)
17282
- }))
17283
- };
18606
+ case "document.apply":
18607
+ case "document.applyChanges": return job;
17284
18608
  case "forms.addWidget": return {
17285
18609
  ...job,
17286
18610
  placement: pdfWidgetPlacementOf(job.placement, boxOf)
@@ -17313,10 +18637,7 @@ function renderOptionsInFileSpace(options, visible) {
17313
18637
  target: target.kind === "rect" ? pdfRenderTargetOf(target, visible()) : target
17314
18638
  };
17315
18639
  }
17316
- function changeInFileSpace(change, boxOf) {
17317
- if ("undoOf" in change) return change;
17318
- return { ops: change.ops.map((op) => opInFileSpace(op, boxOf)) };
17319
- }
18640
+ /** An op's places in the file's coordinates, as its single verb's job's are. */
17320
18641
  function opInFileSpace(op, boxOf) {
17321
18642
  switch (op.type) {
17322
18643
  case "annotations.import":
@@ -17366,16 +18687,29 @@ function opInFileSpace(op, boxOf) {
17366
18687
  expect: pdfWidgetPatchOf(expect, boxOf(op.widget.page), boxOf)
17367
18688
  } : rest;
17368
18689
  }
18690
+ case "outline.create": return {
18691
+ ...op,
18692
+ data: pdfOutlineDraftOf(op.data, boxOf)
18693
+ };
18694
+ case "outline.update": {
18695
+ const { expect, ...rest } = op;
18696
+ return {
18697
+ ...rest,
18698
+ patch: pdfOutlinePatchOf(op.patch, boxOf),
18699
+ ...expect ? { expect: pdfOutlinePatchOf(expect, boxOf) } : {}
18700
+ };
18701
+ }
18702
+ case "outline.delete": {
18703
+ const { expect, ...rest } = op;
18704
+ return expect ? {
18705
+ ...rest,
18706
+ expect: pdfOutlinePatchOf(expect, boxOf)
18707
+ } : rest;
18708
+ }
17369
18709
  default: return op;
17370
18710
  }
17371
18711
  }
17372
- /** What a change did, each place in page space, as its single verbs' results are. */
17373
- function changeResultInPageSpace(result, boxOf) {
17374
- return {
17375
- ...result,
17376
- items: result.items.map((item) => itemInPageSpace(item, boxOf))
17377
- };
17378
- }
18712
+ /** What an op did, each place in page space, as its single verb's result is. */
17379
18713
  function itemInPageSpace(item, boxOf) {
17380
18714
  if (isSkippedItem(item)) return item;
17381
18715
  const annotation = (read) => pageAnnotationOf(read, boxOf(read.page), boxOf);
@@ -17427,6 +18761,24 @@ function itemInPageSpace(item, boxOf) {
17427
18761
  field: item.field && pageFormFieldOf(item.field, boxOf),
17428
18762
  widgets: widgets(item.widgets)
17429
18763
  };
18764
+ case "outline.create":
18765
+ case "outline.update": return {
18766
+ ...item,
18767
+ item: pageOutlineItemOf(item.item, boxOf)
18768
+ };
18769
+ case "outline.restore": return {
18770
+ ...item,
18771
+ items: item.items.map((read) => pageOutlineItemOf(read, boxOf))
18772
+ };
18773
+ case "pages.rotate":
18774
+ case "pages.reorder":
18775
+ case "pages.delete":
18776
+ case "pages.insertBlank":
18777
+ case "pages.insert":
18778
+ case "pages.restore": return {
18779
+ ...item,
18780
+ layout: pageListOf(item.layout)
18781
+ };
17430
18782
  default: return item;
17431
18783
  }
17432
18784
  }
@@ -17964,6 +19316,9 @@ var WorkerHost = class {
17964
19316
  case "actions.read":
17965
19317
  resultPack = this.handleActionsRead(msg, ctrl.signal);
17966
19318
  break;
19319
+ case "outline.read":
19320
+ resultPack = this.handleOutlineRead(msg, ctrl.signal);
19321
+ break;
17967
19322
  case "annotations.list":
17968
19323
  resultPack = this.handleAnnotationsList(msg, ctrl.signal);
17969
19324
  break;
@@ -18069,15 +19424,6 @@ var WorkerHost = class {
18069
19424
  case "pages.list":
18070
19425
  resultPack = this.handlePagesList(msg, ctrl.signal);
18071
19426
  break;
18072
- case "pages.reorder":
18073
- resultPack = this.handlePagesReorder(msg, ctrl.signal);
18074
- break;
18075
- case "pages.rotate":
18076
- resultPack = this.handlePagesRotate(msg, ctrl.signal);
18077
- break;
18078
- case "pages.delete":
18079
- resultPack = this.handlePagesDelete(msg, ctrl.signal);
18080
- break;
18081
19427
  case "pages.setName":
18082
19428
  resultPack = this.handlePagesSetName(msg, ctrl.signal);
18083
19429
  break;
@@ -18108,24 +19454,12 @@ var WorkerHost = class {
18108
19454
  case "pages.extract":
18109
19455
  resultPack = this.handlePagesExtract(msg, ctrl.signal);
18110
19456
  break;
18111
- case "pages.insert":
18112
- resultPack = this.handlePagesInsert(msg, ctrl.signal);
18113
- break;
18114
- case "pages.insertBlank":
18115
- resultPack = this.handlePagesInsertBlank(msg, ctrl.signal);
18116
- break;
18117
19457
  case "attachments.list":
18118
19458
  resultPack = this.handleAttachmentsList(msg, ctrl.signal);
18119
19459
  break;
18120
19460
  case "attachments.readFile":
18121
19461
  resultPack = this.handleAttachmentsReadFile(msg, ctrl.signal);
18122
19462
  break;
18123
- case "attachments.create":
18124
- resultPack = this.handleAttachmentsCreate(msg, ctrl.signal);
18125
- break;
18126
- case "attachments.delete":
18127
- resultPack = this.handleAttachmentsDelete(msg, ctrl.signal);
18128
- break;
18129
19463
  case "measure.setScale":
18130
19464
  resultPack = this.handleMeasureSetScale(msg, ctrl.signal);
18131
19465
  break;
@@ -18483,6 +19817,13 @@ var WorkerHost = class {
18483
19817
  snapshot: new DocumentActionsReader(this.runtime, session).read(signal)
18484
19818
  });
18485
19819
  }
19820
+ handleOutlineRead(req, signal) {
19821
+ const session = this.requireSession(req);
19822
+ return wirePack({
19823
+ tag: "outline.read",
19824
+ outline: new OutlineReader(this.runtime, session).read(signal)
19825
+ });
19826
+ }
18486
19827
  handleAnnotationsList(req, signal) {
18487
19828
  const session = this.requireSession(req);
18488
19829
  const pages = req.pages?.map((page) => session.resolvePageRef(page).pageObjectNumber);
@@ -18668,12 +20009,15 @@ var WorkerHost = class {
18668
20009
  const prior = ledger.outcome(opId);
18669
20010
  let result;
18670
20011
  if (prior) result = this.replayed(prior, fingerprint, opId);
18671
- else result = this.recorded(session, opId, fingerprint, () => new ChangeApplier(this.runtime, session, this.fonts).apply(change.ops, authority, signal));
20012
+ else result = this.recorded(session, opId, fingerprint, () => new ChangeApplier(this.runtime, session, this.fonts).apply(change.ops, authority, signal, FILE_SPACE));
18672
20013
  const item = result.items[0];
18673
20014
  if (!item || isSkippedItem(item)) throw new EngineError(EngineErrorCode.Unknown, `${op.type} wrote nothing`);
18674
20015
  return item;
18675
20016
  }
18676
- /** A change's answer the first time, kept under its `opId`; a retry's checks. */
20017
+ /**
20018
+ * A change's answer the first time, kept under its `opId`; a retry's
20019
+ * checks. The answer is in the space its job answered in (`ChangeOutcome`).
20020
+ */
18677
20021
  replayed(prior, fingerprint, opId) {
18678
20022
  if (prior.fingerprint !== fingerprint) throw new EngineError(EngineErrorCode.IdempotencyKeyReused, `opId ${opId} already answered a different change`);
18679
20023
  if (prior.kind === "refused") throw deserializeError(prior.error);
@@ -18706,12 +20050,13 @@ var WorkerHost = class {
18706
20050
  session.assertUsable();
18707
20051
  if (req.objectNumberFloor !== void 0) session.raiseLastObjectNumber(req.objectNumberFloor - 1);
18708
20052
  const applier = new ChangeApplier(this.runtime, session, this.fonts);
20053
+ const space = this.pageSpaceOf(session);
18709
20054
  const records = /* @__PURE__ */ new Map();
18710
20055
  const outcomes = [];
18711
20056
  for (const entry of req.changes) {
18712
20057
  session.beginTransaction(entry.opId);
18713
20058
  try {
18714
- const applied = runServerChange(applier, entry, records, signal);
20059
+ const applied = runServerChange(applier, entry, records, signal, space);
18715
20060
  const result = resultOf(session, applied);
18716
20061
  session.commitTransaction();
18717
20062
  const record = result.meta.undoable ? applied.record : null;
@@ -18740,11 +20085,23 @@ var WorkerHost = class {
18740
20085
  if (!outcomes.some((outcome) => outcome.status === "applied" && outcome.result.items.some(itemWrote))) return wirePack(payload);
18741
20086
  return this.finishMutation(session, payload, req.artifactPath);
18742
20087
  }
20088
+ /**
20089
+ * Page space for a change, measured as each op runs: the page boxes are read
20090
+ * anew each time, as an op can add, remove or put back the page a later
20091
+ * one names.
20092
+ */
20093
+ pageSpaceOf(session) {
20094
+ return {
20095
+ opInFileSpace: (op) => opInFileSpace(op, visibleBoxReader(this.runtime, session)),
20096
+ itemOut: (item) => itemInPageSpace(item, visibleBoxReader(this.runtime, session))
20097
+ };
20098
+ }
18743
20099
  /** A change's ops, or the steps of the record an undo names. */
18744
20100
  runChange(session, req, signal) {
18745
20101
  const applier = new ChangeApplier(this.runtime, session, this.fonts);
20102
+ const space = this.pageSpaceOf(session);
18746
20103
  const userId = req.authority.identity.userId ?? null;
18747
- if (!isUndoChange(req.change)) return applier.apply(req.change.ops, req.authority, signal);
20104
+ if (!isUndoChange(req.change)) return applier.apply(req.change.ops, req.authority, signal, space);
18748
20105
  const kept = changeLedgerOf(session).record(req.change.undoOf);
18749
20106
  if (!kept || "unavailable" in kept) throw new EngineError(EngineErrorCode.UndoUnavailable, `change ${req.change.undoOf} can no longer be undone`, { details: { reason: kept ? kept.unavailable : "expired" } });
18750
20107
  if (!kept.record) return {
@@ -18755,7 +20112,7 @@ var WorkerHost = class {
18755
20112
  }
18756
20113
  };
18757
20114
  if (kept.record.userId !== null && kept.record.userId !== userId) throw new PermissionDenied("changes:undo", "target");
18758
- return applier.undo(kept.record, req.authority, signal);
20115
+ return applier.undo(kept.record, req.authority, signal, space);
18759
20116
  }
18760
20117
  handlePagesList(req, signal) {
18761
20118
  const session = this.requireSession(req);
@@ -18764,30 +20121,6 @@ var WorkerHost = class {
18764
20121
  snapshot: new PagesReader(this.runtime, session).read(signal)
18765
20122
  });
18766
20123
  }
18767
- handlePagesReorder(req, signal) {
18768
- const session = this.requireSession(req);
18769
- const result = new PagesMutator(this.runtime, session).reorder(req.pages, req.position, signal);
18770
- return this.finishMutation(session, {
18771
- tag: "pages.reorder",
18772
- result
18773
- }, req.artifactPath);
18774
- }
18775
- handlePagesRotate(req, signal) {
18776
- const session = this.requireSession(req);
18777
- const result = new PagesMutator(this.runtime, session).rotate(req.pages, req.rotation, signal);
18778
- return this.finishMutation(session, {
18779
- tag: "pages.rotate",
18780
- result
18781
- }, req.artifactPath);
18782
- }
18783
- handlePagesDelete(req, signal) {
18784
- const session = this.requireSession(req);
18785
- const result = new PagesMutator(this.runtime, session).delete(req.pages, signal);
18786
- return this.finishMutation(session, {
18787
- tag: "pages.delete",
18788
- result
18789
- }, req.artifactPath);
18790
- }
18791
20124
  handlePagesSetName(req, signal) {
18792
20125
  const session = this.requireSession(req);
18793
20126
  const result = new PagesMutator(this.runtime, session).setName({
@@ -18840,32 +20173,13 @@ var WorkerHost = class {
18840
20173
  }
18841
20174
  handlePagesExtract(req, signal) {
18842
20175
  const session = this.requireSession(req);
18843
- const extracted = new PagesExtractor(this.runtime, session).extract(req.pages, signal);
20176
+ const extracted = new PagesExtractor(this.runtime, session).extract(req.pages, req.flatten, signal);
18844
20177
  return wirePack({
18845
20178
  tag: "pages.extract",
18846
20179
  bytes: extracted.bytes,
18847
20180
  size: extracted.size
18848
20181
  }, [extracted.bytes]);
18849
20182
  }
18850
- handlePagesInsert(req, signal) {
18851
- const session = this.requireSession(req);
18852
- const result = new PagesInserter(this.runtime, session).insert(req.bytes, req.position ?? "end", signal);
18853
- return this.finishMutation(session, {
18854
- tag: "pages.insert",
18855
- result
18856
- }, req.artifactPath);
18857
- }
18858
- handlePagesInsertBlank(req, signal) {
18859
- const session = this.requireSession(req);
18860
- const result = new PagesInserter(this.runtime, session).insertBlank({
18861
- size: req.size,
18862
- count: req.count
18863
- }, req.position ?? "end", signal, req.objectNumbers);
18864
- return this.finishMutation(session, {
18865
- tag: "pages.insertBlank",
18866
- result
18867
- }, req.artifactPath);
18868
- }
18869
20183
  handleAttachmentsList(req, signal) {
18870
20184
  const session = this.requireSession(req);
18871
20185
  return wirePack({
@@ -18881,22 +20195,6 @@ var WorkerHost = class {
18881
20195
  content
18882
20196
  }, content.bytes !== void 0 ? [content.bytes] : []);
18883
20197
  }
18884
- handleAttachmentsCreate(req, signal) {
18885
- const session = this.requireSession(req);
18886
- const result = new AttachmentMutator(this.runtime, session).create(req.file, req.resources, signal);
18887
- return this.finishMutation(session, {
18888
- tag: "attachments.create",
18889
- result
18890
- }, req.artifactPath);
18891
- }
18892
- handleAttachmentsDelete(req, signal) {
18893
- const session = this.requireSession(req);
18894
- const result = new AttachmentMutator(this.runtime, session).delete(req.ref, signal);
18895
- return this.finishMutation(session, {
18896
- tag: "attachments.delete",
18897
- result
18898
- }, req.artifactPath);
18899
- }
18900
20198
  async handleAnnotationsReadFile(req, signal) {
18901
20199
  const session = this.requireSession(req);
18902
20200
  const content = await new AttachmentReader(this.runtime, session).readAnnotationFile(session.resolvePageRef(req.page).pageObjectNumber, req.ref, req.path, req.maxDecodedBytes, signal, this.slices);
@@ -19661,9 +20959,9 @@ function resultOf(session, applied) {
19661
20959
  * this job kept for an earlier change of the request, or the one the server
19662
20960
  * sent. A change that left no record leaves nothing to undo.
19663
20961
  */
19664
- function runServerChange(applier, entry, records, signal) {
20962
+ function runServerChange(applier, entry, records, signal, space) {
19665
20963
  const { change, authority } = entry;
19666
- if (!isUndoChange(change)) return applier.apply(change.ops, authority, signal);
20964
+ if (!isUndoChange(change)) return applier.apply(change.ops, authority, signal, space);
19667
20965
  const userId = authority.identity.userId ?? null;
19668
20966
  const record = records.has(change.undoOf) ? records.get(change.undoOf) : entry.record;
19669
20967
  if (record === void 0) throw new EngineError(EngineErrorCode.UndoUnavailable, `change ${change.undoOf} can no longer be undone`, { details: { reason: "expired" } });
@@ -19674,7 +20972,7 @@ function runServerChange(applier, entry, records, signal) {
19674
20972
  steps: []
19675
20973
  }
19676
20974
  };
19677
- return applier.undo(record, authority, signal);
20975
+ return applier.undo(record, authority, signal, space);
19678
20976
  }
19679
20977
  /** Every page a change's items touched, once each, in order. */
19680
20978
  function pagesOf(items) {
@@ -19746,6 +21044,6 @@ function assertNoImport(change) {
19746
21044
  if (op) throw new EngineError(EngineErrorCode.InvalidArg, `doc.apply takes no import: import a bundle with ${verbs[op.type]}`);
19747
21045
  }
19748
21046
  //#endregion
19749
- export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationBatchApplier, AnnotationExporter, AnnotationFlattener, AnnotationImporter, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CandidateFinalizer, ChangeApplier, ChangeLedger, CloseStack, DIGEST_CODE, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EPDF_CHAR_GEOMETRY_LAYOUT, FontRegistrar, FormExporter, FormImporter, FormMutator, FormReader, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SignatureAnalyzer, SignatureMutator, SignatureReader, WorkerHost, acquireSignatureModel, buildRunsFromRawGlyphs, changeLedgerOf, destroyLibrary, disposeFormModel, disposeSignatureModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, hasSignedSignature, normalizeU32, openFatMemoryDocument, openLayerDocument, packChangeRecord, pdfDateToIso, readActionModel, readAttachmentFileInfo, readRevisions, readSignaturesFromModel, readStructure, throwIfAborted, unpackChangeRecord, visibleBoxReader, writeAttachmentFilePayload };
21047
+ export { ALL_STANDARD_SECURITY_PERMISSIONS, ActionReadBudgetTracker, AnnotationAppearanceReader, AnnotationBatchApplier, AnnotationExporter, AnnotationFlattener, AnnotationImporter, AnnotationMutator, AnnotationReader, AttachmentMutator, AttachmentReader, BaseDocumentRegistry, CandidateFinalizer, ChangeApplier, ChangeLedger, CloseStack, DIGEST_CODE, DocumentActionsReader, PagesMutator as DocumentPagesMutator, PagesMutator, DocumentSaver, DocumentSession, EPDF_CHAR_GEOMETRY_LAYOUT, FILE_SPACE, FontRegistrar, FormExporter, FormImporter, FormMutator, FormReader, MetadataMutator, MetadataReader, PageGeometryReader, PagesReader as PageLayoutReader, PagesReader, PagePtrPool, PageRenderReader as PageRasterReader, PageRenderReader, PageTextReader, PagesExtractor, PagesFlattener, PagesInserter, RawAnnotationReader, RedactionApplier, SearchReader, SecurityReader, SignatureAnalyzer, SignatureMutator, SignatureReader, WorkerHost, acquireSignatureModel, buildRunsFromRawGlyphs, changeLedgerOf, destroyLibrary, disposeFormModel, disposeSignatureModel, ensureInitialized, formatPdfDate, generateUuid, hasAllStandardSecurityPermissions, hasSignedSignature, normalizeU32, openFatMemoryDocument, openLayerDocument, packChangeRecord, pdfDateToIso, readActionModel, readAttachmentFileInfo, readRevisions, readSignaturesFromModel, readStructure, rotationAt, throwIfAborted, unpackChangeRecord, visibleBoxReader, writeAttachmentFilePayload };
19750
21048
 
19751
21049
  //# sourceMappingURL=index.js.map