@embedpdf/plugin-annotation 3.0.0-next.10 → 3.0.0-next.12

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.
Files changed (43) hide show
  1. package/dist/authoring-BhtyfsMx.d.cts +37 -0
  2. package/dist/authoring-R4ezdnXd.d.ts +37 -0
  3. package/dist/authoring.cjs +16 -0
  4. package/dist/authoring.d.cts +2 -0
  5. package/dist/authoring.d.ts +2 -0
  6. package/dist/authoring.js +3 -0
  7. package/dist/{capability-BRf0HrOi.js → capability-Bh_1PL5v.js} +266 -280
  8. package/dist/capability-Bh_1PL5v.js.map +1 -0
  9. package/dist/{capability-Cl9x8oLS.cjs → capability-R7dgogJs.cjs} +332 -364
  10. package/dist/capability-R7dgogJs.cjs.map +1 -0
  11. package/dist/contract/host.cjs +3 -0
  12. package/dist/contract/host.d.cts +2 -0
  13. package/dist/contract/host.d.ts +2 -0
  14. package/dist/contract/host.js +2 -0
  15. package/dist/contract.cjs +10 -0
  16. package/dist/contract.cjs.map +1 -0
  17. package/dist/contract.d.cts +10 -0
  18. package/dist/contract.d.ts +10 -0
  19. package/dist/contract.js +8 -0
  20. package/dist/contract.js.map +1 -0
  21. package/dist/index.cjs +25 -24
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +5 -47
  24. package/dist/index.d.ts +5 -47
  25. package/dist/index.js +15 -15
  26. package/dist/index.js.map +1 -1
  27. package/dist/internal.cjs +4 -2
  28. package/dist/internal.d.cts +2 -1
  29. package/dist/internal.d.ts +2 -1
  30. package/dist/internal.js +3 -1
  31. package/dist/seam-6pJeKOOJ.cjs +296 -0
  32. package/dist/seam-6pJeKOOJ.cjs.map +1 -0
  33. package/dist/seam-C4L_7rDt.js +225 -0
  34. package/dist/seam-C4L_7rDt.js.map +1 -0
  35. package/dist/{types-Due7UgzE.d.cts → types-BAPdbfPT.d.cts} +89 -18
  36. package/dist/{types-DITX_3CG.d.ts → types-BgetZ4BK.d.ts} +88 -17
  37. package/dist/types-BvB1vUKb.js +20 -0
  38. package/dist/types-BvB1vUKb.js.map +1 -0
  39. package/dist/types-Dlhdw2k_.cjs +31 -0
  40. package/dist/types-Dlhdw2k_.cjs.map +1 -0
  41. package/package.json +21 -7
  42. package/dist/capability-BRf0HrOi.js.map +0 -1
  43. package/dist/capability-Cl9x8oLS.cjs.map +0 -1
@@ -1,238 +1,13 @@
1
- import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, snapAppearanceScale } from "@embedpdf/core";
2
- import { InteractionToken } from "@embedpdf/plugin-interaction";
1
+ import { a as contentToPdfRect$1, c as insetPdfRectByRD, d as refKey, f as rotFromDTO, g as writableTarget, i as contentToPdfPoint, l as pdfToContentPoint$1, m as toPdfRotation, n as boxGeomFromDTO, o as cssToColor, p as styleFromDTO, r as colorToCss, s as fromPdfRotation, t as boxGeomFields, u as pdfToContentRect$1 } from "./seam-C4L_7rDt.js";
2
+ import { n as previewBucket } from "./types-BvB1vUKb.js";
3
+ import { CONTINUOUS_RENDER_POLICY, snapAppearanceScale } from "@embedpdf/core";
4
+ import { ActionsToken, createHoverPump } from "@embedpdf/plugin-actions/contract";
5
+ import { InteractionToken } from "@embedpdf/plugin-interaction/contract";
3
6
  import { buildCommentThreads, encodeStableIdKey, resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
4
- import { SelectionToken } from "@embedpdf/plugin-selection";
5
- import { FLAG_KEYS, annotContentsEditable, annotDeletable, annotTransformable, calloutLinePoints, canMove, chrome, clickCreateGeom, cloudyBorderExtent, contentToPdfPoint, contentToPdfRect, contentToPdfRect as contentToPdfRect$1, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, geomPdfBounds, geomRotation, geomVisualBounds, groupKeyOf, hitTest, initialTextStyle, isSubstrateOnly, linkChildrenOf, linkOf, normalizeDeg, pageItems, pdfToContentPoint, pdfToContentPoint as pdfToContentPoint$1, pdfToContentRect, pdfToContentRect as pdfToContentRect$1, propsFor, readProp, resolveClickPlacement, rotatedAabb, selectionAnchor, selectionQuad, sharedProps, styleFromProps, textBoxes, unionRect, update, uprightRotation, viewable } from "@embedpdf/core-annotation";
7
+ import { scriptColorToRgb } from "@embedpdf/core-acrojs";
8
+ import { SelectionToken } from "@embedpdf/plugin-selection/contract";
9
+ import { FLAG_KEYS, annotContentsEditable, annotDeletable, annotTransformable, calloutLinePoints, canMove, chrome, clickCreateGeom, cloudyBorderExtent, contentToPdfRect, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, geomPdfBounds, geomRotation, geomVisualBounds, groupKeyOf, hitTest, initialTextStyle, isSubstrateOnly, linkChildrenOf, linkOf, pageItems, pdfToContentPoint, pdfToContentRect, propsFor, readProp, resolveClickPlacement, rotatedAabb, selectionAnchor, selectionQuad, sharedProps, styleFromProps, textBoxes, unionRect, update, uprightRotation, viewable } from "@embedpdf/core-annotation";
6
10
  import { normalizeQuad, textQuadBounds } from "@embedpdf/core-geometry";
7
- //#region src/types.ts
8
- /**
9
- * The annotation capability token. Typed to the full {@link AnnotationHostCapability}
10
- * here (the package internals + the `/internal` entry use this view). The package
11
- * root re-exports the SAME token narrowed to {@link AnnotationCapability}.
12
- */
13
- const AnnotationToken = createCapabilityToken("annotation", { hint: `add annotationPlugin() from '@embedpdf/plugin-annotation' to your plugins list` });
14
- //#endregion
15
- //#region src/repository/seam.ts
16
- function refKey(ref) {
17
- return ref.kind === "objectNumber" ? `obj:${ref.annotObjectNumber}` : ref.kind === "nm" ? `nm:${ref.nm}` : `idx:${ref.pageObjectNumber}:${ref.index}`;
18
- }
19
- const h2 = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
20
- const colorToCss = (c) => `#${h2(c.r)}${h2(c.g)}${h2(c.b)}`;
21
- function cssToColor(css) {
22
- const s = css.trim();
23
- const m6 = /^#?([0-9a-f]{6})$/i.exec(s);
24
- if (m6) {
25
- const n = parseInt(m6[1], 16);
26
- return {
27
- r: n >> 16 & 255,
28
- g: n >> 8 & 255,
29
- b: n & 255
30
- };
31
- }
32
- const m3 = /^#?([0-9a-f]{3})$/i.exec(s);
33
- if (m3) {
34
- const [a, b, c] = m3[1];
35
- return {
36
- r: parseInt(a + a, 16),
37
- g: parseInt(b + b, 16),
38
- b: parseInt(c + c, 16)
39
- };
40
- }
41
- return {
42
- r: 0,
43
- g: 0,
44
- b: 0
45
- };
46
- }
47
- const toPdfRotation = (rotCW) => normalizeDeg(-rotCW);
48
- const fromPdfRotation = (rotPdf) => normalizeDeg(-rotPdf);
49
- /** Advisory `rot` for a vertex geom, from a DTO's (PDF-convention) `rotation`.
50
- * Absent → no `rot` key (kept off the geom so unrotated shapes stay clean). */
51
- const rotFromDTO = (rotation) => rotation ? { rot: fromPdfRotation(rotation) } : {};
52
- /**
53
- * Geometry/rotation fields for a BOX kind (square/circle/plain free-text). The
54
- * model's `rect` is the UNROTATED logical box and `rot` the applied tilt, so:
55
- * - rot == 0 → `/Rect` IS the box; the transform pair is stated as `null`
56
- * (TOTAL projection — the engine's tri-state writes preserve omissions, so
57
- * an omitted field would keep a stale rotation instead of flattening it).
58
- * - rot != 0 → `/Rect` is the rotated visual AABB (PDFium clips the baked /AP
59
- * to it), `unrotatedRect` the logical box, `rotation` the PDF angle.
60
- */
61
- function boxGeomFields(rect, rot, crop) {
62
- if (!rot) return {
63
- rect: contentToPdfRect(rect, crop),
64
- rotation: null,
65
- unrotatedRect: null
66
- };
67
- return {
68
- rect: contentToPdfRect(rotatedAabb(rect, rot), crop),
69
- unrotatedRect: contentToPdfRect(rect, crop),
70
- rotation: toPdfRotation(rot)
71
- };
72
- }
73
- /** A box geom (square/circle/stamp) from its DTO: when rotated, the LOCAL box
74
- * is the stored `unrotatedRect` (the AABB `/Rect` is the rendered envelope)
75
- * and `rot` the converted tilt; unrotated, `/Rect` IS the box. */
76
- function boxGeomFromDTO(dto, rotation, unrotatedRect, crop, ellipse) {
77
- const rot = rotation ? fromPdfRotation(rotation) : 0;
78
- return {
79
- t: "rect",
80
- rect: pdfToContentRect(rot && unrotatedRect ? unrotatedRect : dto.rect, crop),
81
- ellipse,
82
- ...rot ? { rot } : {}
83
- };
84
- }
85
- /** Inset a PdfRect by a `/RD` (PDF user space, y-up: all four are non-negative
86
- * insets from the matching `/Rect` edge). Used to recover the callout text box. */
87
- const insetPdfRectByRD = (r, rd) => rd ? {
88
- left: r.left + rd.left,
89
- bottom: r.bottom + rd.bottom,
90
- right: r.right - rd.right,
91
- top: r.top - rd.top
92
- } : r;
93
- /** Engine border fields (`/BS /S`, `/BS /D`, `/BE /I`) → the `Border` union. A
94
- * cloudy effect wins over the underlying border style (which stays solid). */
95
- function borderFromDTO(d) {
96
- if ((d.cloudyIntensity ?? 0) > 0) return {
97
- kind: "cloudy",
98
- intensity: d.cloudyIntensity
99
- };
100
- if (d.borderStyle === "dashed") return {
101
- kind: "dashed",
102
- dash: d.dashArray?.length ? d.dashArray : [3, 3]
103
- };
104
- return { kind: "solid" };
105
- }
106
- /** The writable projection of a `link` value: `goto`/`uri` pass through,
107
- * read-only arms (`javascript`, `named`, `goto-remote`, `launch`,
108
- * `unsupported`) yield `null` — they can be carried, never (re)written. */
109
- function writableTarget(t) {
110
- return t && (t.kind === "goto" || t.kind === "uri") ? t : null;
111
- }
112
- const TEXT_MARKUP = /* @__PURE__ */ new Set([
113
- "highlight",
114
- "underline",
115
- "squiggly",
116
- "strikeout"
117
- ]);
118
- const STROKE_KINDS = /* @__PURE__ */ new Set([
119
- "square",
120
- "circle",
121
- "line",
122
- "polygon",
123
- "polyline",
124
- "ink"
125
- ]);
126
- /** Engine DTO → content-space `Style` (CSS colours, `Border` union). Exported so
127
- * selection-aware UIs can read display values straight off a {@link AnnotationDTO}
128
- * without re-deriving the colour/border mapping. */
129
- function styleFromDTO(dto) {
130
- if (dto.subtype === "widget") return {
131
- color: dto.color ? colorToCss(dto.color) : "#1a1a1a",
132
- interiorColor: dto.interiorColor ? colorToCss(dto.interiorColor) : null,
133
- strokeWidth: dto.strokeWidth,
134
- opacity: 1,
135
- blendMode: dto.blendMode,
136
- border: dto.borderStyle === "dashed" ? {
137
- kind: "dashed",
138
- dash: [3, 3]
139
- } : { kind: "solid" }
140
- };
141
- if (STROKE_KINDS.has(dto.subtype)) {
142
- const d = dto;
143
- return {
144
- color: colorToCss(d.color),
145
- interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
146
- strokeWidth: d.strokeWidth,
147
- opacity: d.opacity,
148
- blendMode: dto.blendMode,
149
- border: borderFromDTO(d)
150
- };
151
- }
152
- if (TEXT_MARKUP.has(dto.subtype)) {
153
- const d = dto;
154
- return {
155
- color: colorToCss(d.color),
156
- interiorColor: null,
157
- strokeWidth: 0,
158
- opacity: d.opacity,
159
- blendMode: dto.blendMode,
160
- border: { kind: "solid" }
161
- };
162
- }
163
- if (dto.subtype === "caret") {
164
- const d = dto;
165
- return {
166
- color: colorToCss(d.color),
167
- interiorColor: null,
168
- strokeWidth: 1,
169
- opacity: d.opacity,
170
- blendMode: dto.blendMode,
171
- border: { kind: "solid" }
172
- };
173
- }
174
- if (dto.subtype === "free-text") {
175
- const d = dto;
176
- return {
177
- color: colorToCss(d.color),
178
- interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
179
- strokeWidth: d.strokeWidth,
180
- opacity: d.opacity,
181
- blendMode: dto.blendMode,
182
- border: borderFromDTO(d)
183
- };
184
- }
185
- if (dto.subtype === "redact") {
186
- const d = dto;
187
- return {
188
- color: colorToCss(d.color),
189
- interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
190
- strokeWidth: 1.5,
191
- opacity: d.opacity,
192
- blendMode: dto.blendMode,
193
- border: { kind: "solid" }
194
- };
195
- }
196
- if (dto.subtype === "text" || dto.subtype === "file-attachment") {
197
- const d = dto;
198
- return {
199
- color: colorToCss(d.color),
200
- interiorColor: null,
201
- strokeWidth: 1,
202
- opacity: d.opacity,
203
- blendMode: dto.blendMode,
204
- border: { kind: "solid" }
205
- };
206
- }
207
- return {
208
- color: "#444444",
209
- interiorColor: null,
210
- strokeWidth: 1,
211
- opacity: 1,
212
- blendMode: dto.blendMode,
213
- border: { kind: "solid" }
214
- };
215
- }
216
- /**
217
- * The flat props vocabulary (CSS colours, house keys) → the engine's widget
218
- * appearance for `doc.forms` authoring — the SAME mapping the widget patch
219
- * lowering uses, exported as a boundary utility so the form plugin can style
220
- * `placeField` from a tool's `currentDefaults` without growing a second CSS
221
- * parser. Absent keys stay absent (the engine writes nothing for them).
222
- */
223
- function widgetAppearanceFromProps(props) {
224
- return {
225
- ...props.color !== void 0 ? { color: cssToColor(props.color) } : {},
226
- ...props.interiorColor !== void 0 ? { interiorColor: props.interiorColor ? cssToColor(props.interiorColor) : null } : {},
227
- ...props.strokeWidth !== void 0 ? { strokeWidth: props.strokeWidth } : {},
228
- ...props.border !== void 0 ? { borderStyle: props.border.kind === "dashed" ? "dashed" : "solid" } : {},
229
- ...props.fontFamily !== void 0 ? { fontFamily: props.fontFamily } : {},
230
- ...props.fontSize !== void 0 ? { fontSize: props.fontSize } : {},
231
- ...props.fontColor !== void 0 ? { fontColor: cssToColor(props.fontColor) } : {},
232
- ...props.textAlign !== void 0 ? { textAlign: props.textAlign } : {}
233
- };
234
- }
235
- //#endregion
236
11
  //#region src/repository/props.ts
237
12
  /**
238
13
  * GENERIC per-key prop lowerings — the 1:1 mappings from the flat props
@@ -909,6 +684,46 @@ function toCreateDraft(a, crop) {
909
684
  };
910
685
  }
911
686
  //#endregion
687
+ //#region src/hover-feed.ts
688
+ /**
689
+ * The annotation plane's E/X trigger feed — a thin adapter from the ONE
690
+ * pointer-driven hover seam (the `hoverAt` diff) onto the shared hover pump.
691
+ *
692
+ * Anti-cascade is structural and lives in the CALLER's seam placement:
693
+ * reducer-side hover clears (session-hide, remove, reload) never pass
694
+ * through `hoverAt`, so an effect-induced hover loss can never masquerade as
695
+ * a cursor exit. This module only decides WHO is hoverable here:
696
+ *
697
+ * - Drafts (no engine ref) can't carry /AA — skipped.
698
+ * - Widgets and links belong to their own event planes (fill controls /
699
+ * LinkLayer anchors own their pixels). When they ARE hit-testable on this
700
+ * plane, an authoring tool owns them — firing hover actions while editing
701
+ * would be wrong — so they are skipped unconditionally.
702
+ * - Tree-less annotations are FREE: no target, no dispatch, and per-event
703
+ * flags keep a lone /E or /X from dispatching its inert twin.
704
+ */
705
+ function createAnnotationHoverFeed(actions, annotOf) {
706
+ const pump = createHoverPump(actions.dispatch);
707
+ const targetOf = (id) => {
708
+ if (!id) return null;
709
+ const annot = annotOf(id);
710
+ if (!annot?.ref) return null;
711
+ if (annot.subtype.startsWith("widget") || annot.subtype === "link") return null;
712
+ const enter = Boolean(annot.data?.actions?.cursorEnter?.root);
713
+ const exit = Boolean(annot.data?.actions?.cursorExit?.root);
714
+ if (!enter && !exit) return null;
715
+ return {
716
+ ref: annot.ref,
717
+ pon: annot.pon,
718
+ events: {
719
+ enter,
720
+ exit
721
+ }
722
+ };
723
+ };
724
+ return { hover: (id) => pump.hover(targetOf(id)) };
725
+ }
726
+ //#endregion
912
727
  //#region src/text-item.ts
913
728
  /**
914
729
  * The free-text presentation projection: the core's geometry-only `textBoxes`
@@ -960,6 +775,15 @@ function buildTextItems(m, pon, view) {
960
775
  //#endregion
961
776
  //#region src/tools.ts
962
777
  /**
778
+ * The tool `armStamp` activates — and the only tool an armed payload survives
779
+ * onto (plus the legacy `annotation-stamp` tag, honoured for embedder configs
780
+ * written before the tags were unified). Placement consults the armed payload
781
+ * BEFORE the active tool's own `source` spec, so a payload must not outlive
782
+ * its tool onto a sibling stamp preset: an `image` preset's click has to open
783
+ * its own picker, not silently place whatever the stamp panel last armed.
784
+ */
785
+ const ARMED_STAMP_TOOL_ID = "stamp";
786
+ /**
963
787
  * The creation properties each known authoring kind actually consumes. This is
964
788
  * deliberately separate from the flat internal patch vocabulary: it gives tool
965
789
  * configuration precise compile-time and runtime validation while mixed-selection
@@ -1293,7 +1117,7 @@ const DEFAULT_TOOLS = [
1293
1117
  }
1294
1118
  },
1295
1119
  {
1296
- id: "stamp",
1120
+ id: ARMED_STAMP_TOOL_ID,
1297
1121
  subtype: "stamp",
1298
1122
  cursor: "copy",
1299
1123
  enables: ["annotation-place", "annotation-edit"],
@@ -1522,6 +1346,8 @@ function createAnnotationCapability(ctx, config = {}) {
1522
1346
  * slot. See {@link FilePickerProvider}. */
1523
1347
  let filePickerProvider = null;
1524
1348
  const model = () => ctx.getState().model;
1349
+ const actionsForHover = ctx.tryGet(ActionsToken);
1350
+ const hoverFeed = actionsForHover ? createAnnotationHoverFeed(actionsForHover, (id) => model().byId[id] ?? null) : null;
1525
1351
  const chromeSettings = () => ctx.getState().chrome;
1526
1352
  /**
1527
1353
  * Ids on a page whose Behavior is currently ENGAGED (form widgets under a
@@ -1674,11 +1500,21 @@ function createAnnotationCapability(ctx, config = {}) {
1674
1500
  if (!viewable(a.flags, false)) continue;
1675
1501
  const target = a.link ?? (a.data?.subtype === "link" ? a.data.target ?? null : null);
1676
1502
  if (target == null || a.geom.t !== "rect") continue;
1503
+ const activate = a.data?.actions?.activate;
1504
+ const ref = a.ref ?? a.data?.ref ?? void 0;
1505
+ const hoverEnter = Boolean(a.data?.actions?.cursorEnter?.root);
1506
+ const hoverExit = Boolean(a.data?.actions?.cursorExit?.root);
1677
1507
  v.push({
1678
1508
  id,
1679
1509
  rect: a.geom.rect,
1680
1510
  target,
1681
- attached: a.group !== void 0
1511
+ attached: a.group !== void 0,
1512
+ ...activate ? { activate } : {},
1513
+ ...ref ? { ref } : {},
1514
+ ...hoverEnter || hoverExit ? { hoverEvents: {
1515
+ enter: hoverEnter,
1516
+ exit: hoverExit
1517
+ } } : {}
1682
1518
  });
1683
1519
  }
1684
1520
  linkItemsCache.set(pon, {
@@ -1739,6 +1575,14 @@ function createAnnotationCapability(ctx, config = {}) {
1739
1575
  * rendered, never synced, and dies with the tool.
1740
1576
  */
1741
1577
  let armedStamp = null;
1578
+ /** Fixed bytes as a provider: the same image at every size. */
1579
+ const fixedPreview = (bytes, mimeType) => {
1580
+ const preview = {
1581
+ bytes,
1582
+ ...mimeType ? { mimeType } : {}
1583
+ };
1584
+ return async () => preview;
1585
+ };
1742
1586
  /**
1743
1587
  * The desired stamp size (PDF points) from sniffed bytes: the image's
1744
1588
  * INTRINSIC pixel dimensions taken 1:1 as points (the v2 rule — keep the
@@ -1767,23 +1611,21 @@ function createAnnotationCapability(ctx, config = {}) {
1767
1611
  const meta = sniffBinaryMetadata(resolved.bytes);
1768
1612
  if (!meta) throw new Error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
1769
1613
  let preview = null;
1770
- if (input.preview) {
1614
+ if (typeof input.preview === "function") preview = input.preview;
1615
+ else if (input.preview) {
1771
1616
  const p = await resolveBinarySource(input.preview);
1772
- preview = {
1773
- bytes: new Uint8Array(p.bytes),
1774
- mimeType: p.mimeType
1775
- };
1776
- } else if (meta.mimeType !== "application/pdf") preview = {
1777
- bytes: new Uint8Array(resolved.bytes),
1778
- mimeType: meta.mimeType
1779
- };
1617
+ preview = fixedPreview(new Uint8Array(p.bytes), p.mimeType);
1618
+ } else if (meta.mimeType !== "application/pdf") preview = fixedPreview(new Uint8Array(resolved.bytes), meta.mimeType);
1780
1619
  armedStamp = {
1781
1620
  source: input.source,
1782
1621
  ...desiredStampSize(meta, input.targetWidth, input.intrinsicSize),
1783
- preview
1622
+ preview,
1623
+ previewCache: /* @__PURE__ */ new Map(),
1624
+ ...input.name !== void 0 ? { name: input.name } : {},
1625
+ ...input.subject !== void 0 ? { subject: input.subject } : {}
1784
1626
  };
1785
1627
  ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
1786
- ctx.tryGet(InteractionToken)?.activateTool("stamp");
1628
+ ctx.tryGet(InteractionToken)?.activateTool(ARMED_STAMP_TOOL_ID);
1787
1629
  };
1788
1630
  const disarmStamp = () => {
1789
1631
  if (!armedStamp) return;
@@ -1910,28 +1752,49 @@ function createAnnotationCapability(ctx, config = {}) {
1910
1752
  * emits the repository's box rotation fields — the engine bakes the tilted
1911
1753
  * /AP exactly as an interactively rotated stamp round-trips, and the fit uses
1912
1754
  * the ROTATED footprint. Returns false when the page/document isn't ready. */
1913
- const createStampAt = (pon, point, source, desired, rotCW = 0) => {
1755
+ const createStampAt = (pon, point, source, desired, rotCW = 0, identity = {}) => {
1914
1756
  const doc = ctx.doc;
1915
1757
  const crop = cropOf(pon);
1916
- if (!doc || !crop) return false;
1758
+ if (!doc || !crop) return null;
1917
1759
  const box = fitStampBox(point, desired, {
1918
1760
  width: crop.right - crop.left,
1919
1761
  height: crop.top - crop.bottom
1920
1762
  }, rotCW);
1921
- doc.page(pon).annotations.create({
1763
+ return doc.page(pon).annotations.create({
1922
1764
  subtype: "stamp",
1923
1765
  ...boxGeomFields(box, rotCW, crop),
1924
1766
  source,
1925
- fit: "contain"
1767
+ fit: "contain",
1768
+ ...identity.name !== void 0 ? { name: identity.name } : {},
1769
+ ...identity.subject !== void 0 ? { subject: identity.subject } : {}
1926
1770
  }).then((res) => {
1927
1771
  syncDTO(res.created, "baked");
1928
1772
  apply({
1929
1773
  t: "select",
1930
1774
  ids: [refKey(res.created.ref)]
1931
1775
  });
1932
- }, (err) => console.error("[annotation] stamp placement failed:", err));
1776
+ return res.created.ref;
1777
+ });
1778
+ };
1779
+ /** The click path's fire-and-forget wrapper: a rejected placement is logged,
1780
+ * never thrown into the gesture. */
1781
+ const createStampAtSync = (pon, point, source, desired, rotCW = 0, identity = {}) => {
1782
+ const placed = createStampAt(pon, point, source, desired, rotCW, identity);
1783
+ if (!placed) return false;
1784
+ placed.catch((err) => console.error("[annotation] stamp placement failed:", err));
1933
1785
  return true;
1934
1786
  };
1787
+ /** Programmatic placement — the same law as a click, awaited. */
1788
+ const placeStamp = async (input, placement) => {
1789
+ const meta = sniffBinaryMetadata((await resolveBinarySource(input.source)).bytes);
1790
+ if (!meta) throw new Error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
1791
+ const placed = createStampAt(placement.pageObjectNumber, placement.at, input.source, desiredStampSize(meta, placement.targetWidth ?? input.targetWidth, input.intrinsicSize), placement.rotation ?? 0, {
1792
+ ...input.name !== void 0 ? { name: input.name } : {},
1793
+ ...input.subject !== void 0 ? { subject: input.subject } : {}
1794
+ });
1795
+ if (!placed) throw new Error(`[annotation] cannot place a stamp on page ${placement.pageObjectNumber}: document or page not ready`);
1796
+ return placed;
1797
+ };
1935
1798
  /** The active tool's upright counter-rotation for a click at `displayRotation`
1936
1799
  * (0 when the tool doesn't ask for upright, or the display isn't rotated). */
1937
1800
  const uprightRotFor = (displayRotation) => {
@@ -1942,10 +1805,13 @@ function createAnnotationCapability(ctx, config = {}) {
1942
1805
  const placeArmedStamp = (pon, point, displayRotation) => {
1943
1806
  const armed = armedStamp;
1944
1807
  if (!armed) return false;
1945
- return createStampAt(pon, point, armed.source, {
1808
+ return createStampAtSync(pon, point, armed.source, {
1946
1809
  width: armed.width,
1947
1810
  height: armed.height
1948
- }, uprightRotFor(displayRotation));
1811
+ }, uprightRotFor(displayRotation), {
1812
+ ...armed.name !== void 0 ? { name: armed.name } : {},
1813
+ ...armed.subject !== void 0 ? { subject: armed.subject } : {}
1814
+ });
1949
1815
  };
1950
1816
  /** Sniff a stamp source (rejecting non-image bytes) and place it at its
1951
1817
  * intrinsic size (fit to the page in {@link createStampAt}); `targetWidth`
@@ -1956,7 +1822,7 @@ function createAnnotationCapability(ctx, config = {}) {
1956
1822
  console.error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
1957
1823
  return;
1958
1824
  }
1959
- createStampAt(pon, point, source, desiredStampSize(meta, targetWidth), rotCW);
1825
+ createStampAtSync(pon, point, source, desiredStampSize(meta, targetWidth), rotCW);
1960
1826
  };
1961
1827
  /**
1962
1828
  * Click-to-place: resolve the ACTIVE tool's source spec. Fixed `bytes` place
@@ -2637,8 +2503,62 @@ function createAnnotationCapability(ctx, config = {}) {
2637
2503
  });
2638
2504
  });
2639
2505
  } else if (fx.fx === "syncLink") scheduleLinkSync(fx.id, { target: fx.target });
2640
- else doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
2506
+ else {
2507
+ refKey(fx.ref);
2508
+ doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
2509
+ }
2641
2510
  }
2511
+ const reloadPageNow = async (pon) => {
2512
+ const doc = ctx.doc;
2513
+ const crop = cropOf(pon);
2514
+ if (!doc || !crop) return;
2515
+ try {
2516
+ const snap = await doc.page(pon).annotations.list();
2517
+ const m = model();
2518
+ const stale = m.order.filter((id) => m.byId[id]?.pon === pon);
2519
+ if (stale.length) apply({
2520
+ t: "remove",
2521
+ ids: stale
2522
+ });
2523
+ apply({
2524
+ t: "loaded",
2525
+ annots: snap.annotations.map((d) => ingestDTO(d, crop))
2526
+ });
2527
+ } catch {}
2528
+ };
2529
+ /** Script patch → the engine's per-kind patch vocabulary. Colors cross the
2530
+ * Acrobat-array → engine {r,g,b}/255 boundary here; everything else maps
2531
+ * one-to-one (the VM's validity matrix already scoped keys per kind). */
2532
+ const engineScriptPatch = (subtype, patch) => {
2533
+ const out = { subtype };
2534
+ const toEngineColor = (color) => {
2535
+ const rgb = scriptColorToRgb(color);
2536
+ return rgb ? {
2537
+ r: Math.round(rgb.r * 255),
2538
+ g: Math.round(rgb.g * 255),
2539
+ b: Math.round(rgb.b * 255)
2540
+ } : null;
2541
+ };
2542
+ if (patch.strokeColor) {
2543
+ const color = toEngineColor(patch.strokeColor);
2544
+ if (!color) return /* @__PURE__ */ new Error("transparent stroke colours are not supported");
2545
+ out.color = color;
2546
+ }
2547
+ if (patch.fillColor) out.interiorColor = toEngineColor(patch.fillColor);
2548
+ if (patch.opacity !== void 0) out.opacity = patch.opacity;
2549
+ if (patch.width !== void 0) out.strokeWidth = patch.width;
2550
+ if (patch.borderStyle) out.borderStyle = patch.borderStyle === "D" ? "dashed" : "solid";
2551
+ if (patch.dash) out.dashArray = patch.dash;
2552
+ if (patch.rect) out.rect = {
2553
+ left: patch.rect[0],
2554
+ bottom: patch.rect[1],
2555
+ right: patch.rect[2],
2556
+ top: patch.rect[3]
2557
+ };
2558
+ if (patch.contents !== void 0) out.contents = patch.contents;
2559
+ if (patch.flags) out.flags = patch.flags;
2560
+ return out;
2561
+ };
2642
2562
  return {
2643
2563
  create: async (pon, draft) => {
2644
2564
  const doc = ctx.doc;
@@ -2652,6 +2572,7 @@ function createAnnotationCapability(ctx, config = {}) {
2652
2572
  return res.created.ref;
2653
2573
  },
2654
2574
  armStamp,
2575
+ placeStamp,
2655
2576
  disarmStamp,
2656
2577
  placeArmedStamp,
2657
2578
  requestStampAt,
@@ -2665,7 +2586,20 @@ function createAnnotationCapability(ctx, config = {}) {
2665
2586
  const g = ctx.getState().toolGhost;
2666
2587
  return g && g.pon === pon ? g : null;
2667
2588
  },
2668
- armedStampPreview: () => armedStamp?.preview ?? null,
2589
+ armedStampPreview: (devicePixelWidth) => {
2590
+ const armed = armedStamp;
2591
+ if (!armed?.preview) return Promise.resolve(null);
2592
+ const bucket = previewBucket(devicePixelWidth ?? 0);
2593
+ let pending = armed.previewCache.get(bucket);
2594
+ if (!pending) {
2595
+ pending = armed.preview(bucket).catch((error) => {
2596
+ console.error("[annotation] stamp ghost preview failed:", error);
2597
+ return null;
2598
+ });
2599
+ armed.previewCache.set(bucket, pending);
2600
+ }
2601
+ return pending;
2602
+ },
2669
2603
  stampArmEpoch: () => ctx.getState().stampArmEpoch,
2670
2604
  setFilePickerProvider: (provider) => {
2671
2605
  filePickerProvider = provider;
@@ -2779,10 +2713,13 @@ function createAnnotationCapability(ctx, config = {}) {
2779
2713
  const h = hitTest(m, at.pon, at.point, chromeGeomAt(at.scale), m.hitMargin, pageBoxOf(at.pon), inertIdsAt(at.pon), viewEnv(at.zoom, at.rotation));
2780
2714
  if (h.t === "annot") id = h.id;
2781
2715
  }
2782
- if (m.hovered !== id) apply({
2783
- t: "hover",
2784
- id
2785
- });
2716
+ if (m.hovered !== id) {
2717
+ hoverFeed?.hover(id);
2718
+ apply({
2719
+ t: "hover",
2720
+ id
2721
+ });
2722
+ }
2786
2723
  },
2787
2724
  behaviorFor: (a) => behaviors.find((b) => b.matches(a) && b.engaged()) ?? null,
2788
2725
  linkItemsOn: (pon) => memoLinkItems(pon),
@@ -3002,24 +2939,7 @@ function createAnnotationCapability(ctx, config = {}) {
3002
2939
  return subs.length > 0 && subs.every((a) => a.ref != null && allowsMutation("update", a.ref));
3003
2940
  },
3004
2941
  ensurePage: () => {},
3005
- reloadPage: async (pon) => {
3006
- const doc = ctx.doc;
3007
- const crop = cropOf(pon);
3008
- if (!doc || !crop) return;
3009
- try {
3010
- const snap = await doc.page(pon).annotations.list();
3011
- const m = model();
3012
- const stale = m.order.filter((id) => m.byId[id]?.pon === pon);
3013
- if (stale.length) apply({
3014
- t: "remove",
3015
- ids: stale
3016
- });
3017
- apply({
3018
- t: "loaded",
3019
- annots: snap.annotations.map((d) => ingestDTO(d, crop))
3020
- });
3021
- } catch {}
3022
- },
2942
+ reloadPage: (pon) => reloadPageNow(pon),
3023
2943
  hydration: () => ctx.getState().hydration,
3024
2944
  ensureHydrated: () => {
3025
2945
  if (hydrationStarted) return;
@@ -3085,6 +3005,72 @@ function createAnnotationCapability(ctx, config = {}) {
3085
3005
  textTimers.clear();
3086
3006
  apply({ t: "endTextEdit" });
3087
3007
  },
3008
+ commitScriptEffects: async (entries) => {
3009
+ const doc = ctx.doc;
3010
+ if (!doc) return { results: entries.map((entry) => ({
3011
+ annotObjectNumber: entry.annotObjectNumber,
3012
+ status: "failed",
3013
+ error: "no document"
3014
+ })) };
3015
+ const results = [];
3016
+ const touchedPons = /* @__PURE__ */ new Set();
3017
+ let failed = false;
3018
+ for (const entry of entries) {
3019
+ if (failed) {
3020
+ results.push({
3021
+ annotObjectNumber: entry.annotObjectNumber,
3022
+ status: "skipped"
3023
+ });
3024
+ continue;
3025
+ }
3026
+ const loaded = model().byId[`obj:${entry.annotObjectNumber}`];
3027
+ const pon = loaded?.pon ?? entry.pageObjectNumber;
3028
+ let ref = loaded?.ref ?? null;
3029
+ let subtype = loaded?.subtype;
3030
+ if ((!ref || !subtype) && pon !== void 0) try {
3031
+ const { annotations } = await doc.page(pon).annotations.list();
3032
+ const dto = annotations.find((candidate) => candidate.ref.kind === "objectNumber" && candidate.ref.annotObjectNumber === entry.annotObjectNumber);
3033
+ if (dto) {
3034
+ ref = dto.ref;
3035
+ subtype = dto.subtype;
3036
+ }
3037
+ } catch {}
3038
+ if (pon === void 0 || !ref || !subtype) {
3039
+ results.push({
3040
+ annotObjectNumber: entry.annotObjectNumber,
3041
+ status: "failed",
3042
+ error: "annotation not resolved (page not loaded)"
3043
+ });
3044
+ continue;
3045
+ }
3046
+ const patch = engineScriptPatch(subtype, entry.patch);
3047
+ if (patch instanceof Error) {
3048
+ results.push({
3049
+ annotObjectNumber: entry.annotObjectNumber,
3050
+ status: "failed",
3051
+ error: patch.message
3052
+ });
3053
+ continue;
3054
+ }
3055
+ try {
3056
+ await doc.page(pon).annotations.update(ref, patch);
3057
+ touchedPons.add(pon);
3058
+ results.push({
3059
+ annotObjectNumber: entry.annotObjectNumber,
3060
+ status: "applied"
3061
+ });
3062
+ } catch (error) {
3063
+ failed = true;
3064
+ results.push({
3065
+ annotObjectNumber: entry.annotObjectNumber,
3066
+ status: "failed",
3067
+ error: error instanceof Error ? error.message : String(error)
3068
+ });
3069
+ }
3070
+ }
3071
+ for (const touched of touchedPons) await reloadPageNow(touched);
3072
+ return { results };
3073
+ },
3088
3074
  registerBehavior: (b) => {
3089
3075
  behaviors.push(b);
3090
3076
  return () => {
@@ -3095,6 +3081,6 @@ function createAnnotationCapability(ctx, config = {}) {
3095
3081
  };
3096
3082
  }
3097
3083
  //#endregion
3098
- export { fromDTO as a, refKey as c, AnnotationToken as d, isTouchDirect as i, styleFromDTO as l, DEFAULT_TOOLS as n, toCreateDraft as o, buildToolRegistry as r, toPatch as s, createAnnotationCapability as t, widgetAppearanceFromProps as u };
3084
+ export { isTouchDirect as a, toPatch as c, buildToolRegistry as i, ARMED_STAMP_TOOL_ID as n, fromDTO as o, DEFAULT_TOOLS as r, toCreateDraft as s, createAnnotationCapability as t };
3099
3085
 
3100
- //# sourceMappingURL=capability-BRf0HrOi.js.map
3086
+ //# sourceMappingURL=capability-Bh_1PL5v.js.map