@embedpdf/plugin-stamp 3.0.0-next.11 → 3.0.0-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,14 +1,89 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_contract = require("./contract-DmQ4peF2.cjs");
2
+ const require_contract = require("./contract-T-v5KaYd.cjs");
3
3
  let _embedpdf_core = require("@embedpdf/core");
4
4
  let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
5
+ let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
6
+ let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
5
7
  let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
8
+ let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
6
9
  let _embedpdf_plugin_form_scripting = require("@embedpdf/plugin-form/scripting");
10
+ //#region src/blank-library.ts
11
+ /**
12
+ * The seed of a new library: a one-page PDF whose page is UNREGISTERED — the
13
+ * shape Acrobat itself gives a fresh stamp library (its first page is a
14
+ * blank starter that no `/Names /Pages` key points at). A library must be a
15
+ * PDF from the moment it exists, and a PDF must have a page; the unregistered
16
+ * blank is that page. It is never an asset, never placed, never listed.
17
+ */
18
+ const BLANK_OBJECTS = [
19
+ "<< /Type /Catalog /Pages 2 0 R >>",
20
+ "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
21
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>"
22
+ ];
23
+ let cached = null;
24
+ function blankLibraryPdf() {
25
+ if (cached) return new Uint8Array(cached);
26
+ let body = "%PDF-1.7\n";
27
+ const offsets = [];
28
+ BLANK_OBJECTS.forEach((object, index) => {
29
+ offsets.push(body.length);
30
+ body += `${index + 1} 0 obj\n${object}\nendobj\n`;
31
+ });
32
+ const xref = body.length;
33
+ body += `xref\n0 ${BLANK_OBJECTS.length + 1}\n0000000000 65535 f \n`;
34
+ for (const offset of offsets) body += `${String(offset).padStart(10, "0")} 00000 n \n`;
35
+ body += `trailer\n<< /Size ${BLANK_OBJECTS.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
36
+ cached = new TextEncoder().encode(body);
37
+ return new Uint8Array(cached);
38
+ }
39
+ //#endregion
7
40
  //#region src/capability.ts
8
41
  const DEFAULT_PREVIEW_WIDTH = 256;
9
- const LIBRARY_PIECEINFO_APP = "EMBD_StampLibrary";
10
- const STAMP_PIECEINFO_APP = "EMBD_Stamp";
11
- const STAMP_SCHEMA_VERSION = 1;
42
+ /** `#rrggbb` → the engine's sRGB triplet. */
43
+ const hexColor = (hex) => {
44
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
45
+ if (!m) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] mark color must be #rrggbb, got '${hex}'`);
46
+ const n = parseInt(m[1], 16);
47
+ return {
48
+ r: n >> 16 & 255,
49
+ g: n >> 8 & 255,
50
+ b: n & 255
51
+ };
52
+ };
53
+ /** An exact ArrayBuffer over a view (the engine's binary idiom). */
54
+ const toArrayBuffer = (bytes) => {
55
+ const out = new ArrayBuffer(bytes.byteLength);
56
+ new Uint8Array(out).set(bytes);
57
+ return out;
58
+ };
59
+ /** The rect an ink mark occupies, padded by its stroke. */
60
+ const inkBounds = (strokes, strokeWidth) => {
61
+ let left = Infinity;
62
+ let bottom = Infinity;
63
+ let right = -Infinity;
64
+ let top = -Infinity;
65
+ for (const stroke of strokes) for (const { x, y } of stroke) {
66
+ if (x < left) left = x;
67
+ if (x > right) right = x;
68
+ if (y < bottom) bottom = y;
69
+ if (y > top) top = y;
70
+ }
71
+ if (!Number.isFinite(left)) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] an ink mark needs at least one point");
72
+ const pad = strokeWidth;
73
+ return {
74
+ left: left - pad,
75
+ bottom: bottom - pad,
76
+ right: right + pad,
77
+ top: top + pad
78
+ };
79
+ };
80
+ /** A generous box for typed text; the export crops to the glyphs. */
81
+ const textBounds = (text, fontSize) => ({
82
+ left: 0,
83
+ bottom: 0,
84
+ right: Math.max(1, text.length) * fontSize * .75 + fontSize,
85
+ top: fontSize * 1.6
86
+ });
12
87
  /** Session-unique ids. Assets are session-scoped for now (no persistence),
13
88
  * so a timestamp + counter is enough — durable ids come with the store port. */
14
89
  let seq = 0;
@@ -25,7 +100,6 @@ const entryStringArray = (entries, key) => {
25
100
  const entry = entries[key];
26
101
  return entry?.type === "string-array" ? [...entry.value] : void 0;
27
102
  };
28
- const kindToPdfName = (kind) => kind === "signature" ? "Signature" : kind === "initials" ? "Initials" : "Stamp";
29
103
  const kindFromPdfName = (name) => {
30
104
  switch (name?.toLowerCase()) {
31
105
  case "stamp": return "stamp";
@@ -34,21 +108,6 @@ const kindFromPdfName = (name) => {
34
108
  default: return;
35
109
  }
36
110
  };
37
- const metadataPatch = (id, name, kind, subject, categories) => ({
38
- Version: STAMP_SCHEMA_VERSION,
39
- Id: id,
40
- Name: name,
41
- Kind: { name: kindToPdfName(kind) },
42
- Subject: subject ?? null,
43
- Categories: categories ?? null
44
- });
45
- const libraryMetadataPatch = (id, name, categories) => ({
46
- Version: STAMP_SCHEMA_VERSION,
47
- Id: id,
48
- Name: name,
49
- Kind: { name: "StampLibrary" },
50
- Categories: categories ?? null
51
- });
52
111
  function createStampCapability(ctx, config = {}) {
53
112
  /** Binary sidecar of the serializable store: asset bytes + cached preview,
54
113
  * keyed by asset id. The reducer never sees these (kernel rule 1). */
@@ -75,16 +134,13 @@ function createStampCapability(ctx, config = {}) {
75
134
  }
76
135
  return assetEngineRef;
77
136
  };
78
- const openAssetDocument = async (bytes, identity) => {
137
+ const openAssetDocument = async (bytes) => {
79
138
  try {
80
139
  return await (await assetEngine()).open({
81
140
  kind: "bytes",
82
141
  id: uid("stamp-import"),
83
142
  bytes
84
- }, {
85
- scope: ["*"],
86
- ...identity ? { identity } : {}
87
- });
143
+ }, { scope: ["*"] });
88
144
  } catch (err) {
89
145
  if (!config.assetEngine && _embedpdf_engine_core_runtime.EngineError.is(err, _embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg)) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] importing a library PDF needs an engine that can open local bytes, and this viewer's engine cannot (cloud). Pass stampPlugin({ assetEngine: () => import('@embedpdf/engine').then((m) => m.createLocalEngine()) }) — it loads lazily, on first import.");
90
146
  throw err;
@@ -107,94 +163,159 @@ function createStampCapability(ctx, config = {}) {
107
163
  taken.add(id);
108
164
  return id;
109
165
  };
110
- const createLooseLibrary = (name) => {
111
- const id = uid("stamp-lib");
166
+ const libraryChanged = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[stamp] onLibraryChanged observer failed:", error));
167
+ /** Thumbnail render of one library page (the small picker image). */
168
+ const renderThumbnail = async (handle) => imageToPreview(await handle.render.image({
169
+ viewport: {
170
+ kind: "width",
171
+ width: config.previewWidth ?? DEFAULT_PREVIEW_WIDTH
172
+ },
173
+ background: "transparent",
174
+ includeAnnotations: true,
175
+ format: "png"
176
+ }));
177
+ const createLibrary = async (name, opts) => {
178
+ const kind = opts?.kind ?? "stamps";
179
+ const taken = new Set(Object.keys(ctx.getState().libraries));
180
+ const id = allocateId(opts?.id, "stamp-lib", taken);
181
+ const doc = await openAssetDocument(blankLibraryPdf());
182
+ let bytes;
183
+ try {
184
+ requireCanonicalServices(doc);
185
+ await doc.metadata.update({ title: name });
186
+ await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(id, {
187
+ kind,
188
+ categories: opts?.categories
189
+ }));
190
+ bytes = await doc.download();
191
+ } finally {
192
+ await doc.close();
193
+ }
194
+ libraryBinaries.set(id, bytes);
112
195
  ctx.dispatch({
113
196
  type: "LIBRARY_ADDED",
114
197
  library: {
115
198
  id,
116
199
  name,
117
- storage: "loose",
118
- assetIds: []
200
+ kind,
201
+ assetIds: [],
202
+ ...opts?.categories ? { categories: opts.categories } : {}
119
203
  }
120
204
  });
205
+ libraryChanged.emit({
206
+ libraryId: id,
207
+ reason: "created"
208
+ });
121
209
  return id;
122
210
  };
211
+ const requireCanonicalServices = (doc) => {
212
+ if (!doc.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need an asset engine with pieceInfo");
213
+ if (!doc.pages.setName || !doc.pages.removeName) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need an asset engine with named pages (pages.setName)");
214
+ };
123
215
  const importLibraryPdf = async (source, opts) => {
124
216
  const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(source);
125
217
  if ((0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes)?.mimeType !== "application/pdf") throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] importLibraryPdf needs PDF bytes (use addAsset for a raster image)");
126
218
  const doc = await openAssetDocument(new Uint8Array(resolved.bytes));
127
219
  let imported;
128
220
  try {
129
- if (!doc.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need an asset engine with pieceInfo");
130
- const snapshot = await doc.pages.list();
131
- if (snapshot.pageCount === 0) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] a canonical stamp library PDF must contain at least one page");
132
- const pages = snapshot.pages.map((layout) => {
133
- const handle = doc.page(layout.pageObjectNumber);
134
- if (!handle.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need page pieceInfo support");
135
- return {
136
- layout,
137
- handle,
138
- metadata: null
139
- };
140
- });
141
- const catalogMetadata = await doc.pieceInfo.read(LIBRARY_PIECEINFO_APP);
142
- for (const page of pages) page.metadata = await page.handle.pieceInfo.read(STAMP_PIECEINFO_APP);
221
+ requireCanonicalServices(doc);
222
+ const layout = await doc.pages.list();
223
+ if (layout.pageCount === 0) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] a canonical stamp library PDF must contain at least one page");
224
+ const byPon = new Map(layout.pages.map((page) => [page.pageObjectNumber, page]));
225
+ const catalogEntries = (await doc.pieceInfo.read("EMBD_StampLibrary"))?.entries ?? {};
143
226
  const state = ctx.getState();
144
227
  const takenLibraryIds = new Set(Object.keys(state.libraries));
145
- const takenAssetIds = new Set(Object.keys(state.assets));
146
- const catalogEntries = catalogMetadata?.entries ?? {};
147
228
  const libraryId = allocateId(entryString(catalogEntries, "Id"), "stamp-lib", takenLibraryIds);
148
- const libraryName = opts?.name ?? entryString(catalogEntries, "Name") ?? "Stamps";
229
+ const docMeta = await doc.metadata.read();
230
+ const libraryName = (docMeta.title && docMeta.title.length > 0 ? docMeta.title : void 0) ?? entryString(catalogEntries, "Name") ?? opts?.name ?? "Stamps";
231
+ if (!docMeta.title) await doc.metadata.update({ title: libraryName });
149
232
  const libraryCategories = opts?.categories ?? entryStringArray(catalogEntries, "Categories");
150
- const descriptors = pages.map(({ layout, handle, metadata: pageMetadata }) => {
151
- const entries = pageMetadata?.entries ?? {};
152
- const id = allocateId(entryString(entries, "Id"), "stamp", takenAssetIds);
153
- const kind = opts?.kind ?? kindFromPdfName(entryName(entries, "Kind")) ?? "stamp";
154
- return {
155
- handle,
156
- asset: {
157
- id,
158
- libraryId,
159
- kind,
160
- name: opts?.assetName?.(layout.index) ?? entryString(entries, "Name") ?? `Stamp ${layout.index + 1}`,
161
- size: {
162
- width: layout.size.width,
163
- height: layout.size.height
164
- },
165
- format: "pdf",
166
- pageObjectNumber: layout.pageObjectNumber,
167
- subject: entryString(entries, "Subject"),
168
- categories: entryStringArray(entries, "Categories")
169
- }
170
- };
171
- });
172
- await doc.pieceInfo.update(LIBRARY_PIECEINFO_APP, libraryMetadataPatch(libraryId, libraryName, libraryCategories));
233
+ const libraryLocale = entryString(catalogEntries, "Locale");
234
+ const libraryKind = opts?.libraryKind ?? require_contract.libraryKindFromPdfName(entryName(catalogEntries, "Kind"));
235
+ const registry = (layout.namedPages ?? []).filter((entry) => entry.target.kind === "page");
236
+ const hidden = (layout.namedPages ?? []).filter((entry) => entry.target.kind !== "page");
237
+ if (hidden.length > 0) globalThis.console?.warn(`[stamp] library '${libraryName}' carries ${hidden.length} hidden-template or dangling registration(s); they are not stamps and were ignored`);
238
+ let descriptors;
239
+ if (registry.length > 0) {
240
+ const seen = /* @__PURE__ */ new Set();
241
+ descriptors = registry.map((entry) => {
242
+ const { name, label } = require_contract.parseStampKey(entry.name);
243
+ if (!name) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] library '${libraryName}': empty stamp identifier in key '${entry.name}'`);
244
+ if (seen.has(name)) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] library '${libraryName}': duplicate stamp identifier '${name}'`);
245
+ seen.add(name);
246
+ const page = byPon.get(entry.target.pageObjectNumber);
247
+ return {
248
+ pageObjectNumber: page.pageObjectNumber,
249
+ index: page.index,
250
+ name,
251
+ label
252
+ };
253
+ });
254
+ descriptors.sort((a, b) => a.index - b.index);
255
+ } else if (layout.namedPages === void 0) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] the asset engine reports no named-page registry (an older engine); upgrade it to import libraries");
256
+ else {
257
+ descriptors = [];
258
+ for (const page of layout.pages) {
259
+ const v1 = (await doc.page(page.pageObjectNumber).pieceInfo?.read(require_contract.STAMP_PIECEINFO_APP))?.entries;
260
+ const name = (v1 && entryString(v1, "Name")) ?? `Stamp${page.index + 1}`;
261
+ const label = (v1 && entryString(v1, "Subject")) ?? opts?.assetName?.(page.index) ?? name;
262
+ descriptors.push({
263
+ pageObjectNumber: page.pageObjectNumber,
264
+ index: page.index,
265
+ name,
266
+ label
267
+ });
268
+ }
269
+ for (const d of descriptors) await doc.pages.setName({
270
+ name: require_contract.stampKey(d.name, d.label),
271
+ pageObjectNumber: d.pageObjectNumber
272
+ });
273
+ }
274
+ await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(libraryId, {
275
+ kind: libraryKind,
276
+ categories: libraryCategories,
277
+ locale: libraryLocale
278
+ }));
173
279
  const assets = [];
174
- for (const descriptor of descriptors) {
175
- const { asset, handle } = descriptor;
176
- await handle.pieceInfo.update(STAMP_PIECEINFO_APP, metadataPatch(asset.id, asset.name, asset.kind, asset.subject, asset.categories));
177
- const bytes = await doc.pages.extract([asset.pageObjectNumber]);
178
- const image = await handle.render.image({
179
- viewport: {
180
- kind: "width",
181
- width: config.previewWidth ?? DEFAULT_PREVIEW_WIDTH
280
+ for (const d of descriptors) {
281
+ const handle = doc.page(d.pageObjectNumber);
282
+ if (!handle.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need page pieceInfo support");
283
+ const entries = (await handle.pieceInfo.read("EMBD_Stamp"))?.entries ?? {};
284
+ const kind = opts?.kind ?? kindFromPdfName(entryName(entries, "Kind")) ?? "stamp";
285
+ const subject = entryString(entries, "SubjectOverride");
286
+ const categories = entryStringArray(entries, "Categories");
287
+ const page = byPon.get(d.pageObjectNumber);
288
+ const asset = {
289
+ id: require_contract.assetIdFor(libraryId, d.name),
290
+ libraryId,
291
+ kind,
292
+ name: d.name,
293
+ label: d.label,
294
+ size: {
295
+ width: page.size.width,
296
+ height: page.size.height
182
297
  },
183
- background: "transparent",
184
- includeAnnotations: true,
185
- format: "png"
186
- });
298
+ pageObjectNumber: d.pageObjectNumber,
299
+ ...subject !== void 0 ? { subject } : {},
300
+ ...categories !== void 0 ? { categories } : {}
301
+ };
302
+ await handle.pieceInfo.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(kind, {
303
+ subject,
304
+ categories
305
+ }));
306
+ const bytes = await doc.pages.extract([d.pageObjectNumber]);
187
307
  assets.push({
188
308
  asset,
189
309
  bytes,
190
- preview: imageToPreview(image)
310
+ preview: await renderThumbnail(handle)
191
311
  });
192
312
  }
193
313
  imported = {
194
314
  library: {
195
315
  id: libraryId,
196
316
  name: libraryName,
197
- storage: "canonical-pdf",
317
+ kind: libraryKind,
318
+ ...libraryLocale ? { locale: libraryLocale } : {},
198
319
  categories: libraryCategories,
199
320
  assetIds: []
200
321
  },
@@ -219,15 +340,93 @@ function createStampCapability(ctx, config = {}) {
219
340
  asset
220
341
  });
221
342
  }
343
+ libraryChanged.emit({
344
+ libraryId: imported.library.id,
345
+ reason: "imported"
346
+ });
222
347
  return imported.library.id;
223
348
  };
349
+ /**
350
+ * A drawn or typed mark rendered to a one-page PDF: the annotation is
351
+ * authored on a scratch page and exported the way "stamp from selection"
352
+ * exports — a fresh page the size of the mark, paths kept as paths, the
353
+ * font subset-embedded. Self-contained and Acrobat-readable.
354
+ */
355
+ const renderMark = async (mark) => {
356
+ const doc = await openAssetDocument(blankLibraryPdf());
357
+ try {
358
+ const scratch = (await doc.pages.list()).pages[0];
359
+ if (!scratch) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, "[stamp] no scratch page");
360
+ const page = doc.page(scratch.pageObjectNumber);
361
+ if (!page.annotations.exportAppearance) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] authoring a mark needs an asset engine that can export annotation appearances");
362
+ const color = hexColor(mark.color ?? "#1d2b53");
363
+ const created = mark.kind === "ink" ? await page.annotations.create({
364
+ subtype: "ink",
365
+ inkList: mark.strokes.map((stroke) => stroke.map(({ x, y }) => ({
366
+ x,
367
+ y
368
+ }))),
369
+ rect: inkBounds(mark.strokes, mark.strokeWidth ?? 2),
370
+ color,
371
+ strokeWidth: mark.strokeWidth ?? 2
372
+ }) : await page.annotations.create({
373
+ subtype: "free-text",
374
+ intent: "free-text",
375
+ contents: mark.text,
376
+ fontFamily: mark.fontFamily,
377
+ fontSize: mark.fontSize ?? 36,
378
+ textAlign: "left",
379
+ rect: textBounds(mark.text, mark.fontSize ?? 36),
380
+ fontColor: color,
381
+ strokeWidth: 0
382
+ });
383
+ return new Uint8Array(await page.annotations.exportAppearance([created.created.ref]));
384
+ } finally {
385
+ await doc.close();
386
+ }
387
+ };
388
+ /** One page of a PDF as a single-page PDF (a `pdf` mark with `pageIndex`). */
389
+ const extractPage = async (bytes, pageIndex) => {
390
+ const doc = await openAssetDocument(bytes);
391
+ try {
392
+ const layout = await doc.pages.list();
393
+ const page = layout.pages[pageIndex];
394
+ if (!page) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] the PDF has no page ${pageIndex} (${layout.pageCount} pages)`);
395
+ return await doc.pages.extract([page.pageObjectNumber]);
396
+ } finally {
397
+ await doc.close();
398
+ }
399
+ };
400
+ /** The bytes an asset is made from: the caller's `source`, or its `mark` authored into one. */
401
+ const resolveAssetSource = async (input) => {
402
+ if (input.source === void 0 === (input.mark === void 0)) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] addAsset takes exactly one of `source` / `mark`");
403
+ if (input.source !== void 0) return (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source);
404
+ const mark = input.mark;
405
+ switch (mark.kind) {
406
+ case "image": return (0, _embedpdf_engine_core_runtime.resolveBinarySource)(mark.source);
407
+ case "pdf": {
408
+ const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(mark.source);
409
+ if (mark.pageIndex === void 0) return resolved;
410
+ return { bytes: toArrayBuffer(await extractPage(new Uint8Array(resolved.bytes), mark.pageIndex)) };
411
+ }
412
+ case "ink":
413
+ case "text": return { bytes: toArrayBuffer(await renderMark(mark)) };
414
+ }
415
+ };
224
416
  const addAsset = async (input) => {
225
- const targetLibrary = input.libraryId ? ctx.getState().libraries[input.libraryId] ?? null : null;
226
- if (input.libraryId && !targetLibrary) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown library '${input.libraryId}'`);
227
- const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source);
417
+ if (input.libraryId && !ctx.getState().libraries[input.libraryId]) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown library '${input.libraryId}'`);
418
+ const resolved = await resolveAssetSource(input);
228
419
  const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes);
229
420
  if (!meta) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] asset source must be PNG, JPEG, or single-page PDF bytes");
230
421
  const isPdf = meta.mimeType === "application/pdf";
422
+ const name = input.name ?? require_contract.customStampName();
423
+ const label = input.label ?? name;
424
+ if (name.length === 0 || name.includes("=")) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] invalid stamp identifier '${name}': must be non-empty and contain no '='`);
425
+ const rasterSize = input.size ?? ("width" in meta ? {
426
+ width: meta.width,
427
+ height: meta.height
428
+ } : null);
429
+ if (!isPdf && (!rasterSize || rasterSize.width <= 0 || rasterSize.height <= 0)) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] a raster asset needs a positive `size` (its page size in points)");
231
430
  let suppliedPreview = null;
232
431
  if (input.preview) {
233
432
  const preview = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.preview);
@@ -236,111 +435,106 @@ function createStampCapability(ctx, config = {}) {
236
435
  mimeType: preview.mimeType ?? "image/png"
237
436
  };
238
437
  }
239
- if (targetLibrary?.storage === "canonical-pdf") {
240
- if (!isPdf) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] raster assets cannot be appended to a canonical PDF library");
241
- return mutateLibrary(targetLibrary.id, async () => {
242
- const liveLibrary = ctx.getState().libraries[targetLibrary.id];
243
- if (liveLibrary?.storage !== "canonical-pdf") throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] canonical library '${targetLibrary.id}' no longer exists`);
244
- const canonicalBytes = libraryBinaries.get(liveLibrary.id);
245
- if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${liveLibrary.id}'`);
246
- const assetId = uid("stamp");
247
- const doc = await openAssetDocument(canonicalBytes);
248
- let appended;
249
- try {
438
+ const libraryId = input.libraryId ?? await createLibrary(label);
439
+ return mutateLibrary(libraryId, async () => {
440
+ const liveLibrary = ctx.getState().libraries[libraryId];
441
+ if (!liveLibrary) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] library '${libraryId}' no longer exists`);
442
+ const assetId = require_contract.assetIdFor(liveLibrary.id, name);
443
+ if (ctx.getState().assets[assetId]) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] library '${liveLibrary.name}' already has a stamp named '${name}'`);
444
+ const canonicalBytes = libraryBinaries.get(liveLibrary.id);
445
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${liveLibrary.id}'`);
446
+ const doc = await openAssetDocument(canonicalBytes);
447
+ let appended;
448
+ try {
449
+ requireCanonicalServices(doc);
450
+ let pageObjectNumber;
451
+ if (isPdf) {
250
452
  const result = await doc.pages.insert(new Uint8Array(resolved.bytes));
251
- if (result.insertedPageObjectNumbers.length !== 1) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] a canonical library asset must be a single-page PDF");
252
- const pageObjectNumber = result.insertedPageObjectNumbers[0];
253
- const layout = result.layout.pages.find((candidate) => candidate.pageObjectNumber === pageObjectNumber);
254
- const page = doc.page(pageObjectNumber);
255
- if (!layout || !page.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need page layout and pieceInfo support");
256
- const asset = {
257
- id: assetId,
258
- libraryId: liveLibrary.id,
259
- kind: input.kind ?? "stamp",
260
- name: input.name,
261
- size: {
262
- width: layout.size.width,
263
- height: layout.size.height
453
+ if (result.insertedPageObjectNumbers.length !== 1) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] a library asset must be a single-page PDF");
454
+ pageObjectNumber = result.insertedPageObjectNumbers[0];
455
+ } else {
456
+ if (!doc.pages.flatten) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] raster assets need an asset engine with pages.flatten");
457
+ pageObjectNumber = (await doc.pages.insertBlank({ size: rasterSize })).insertedPageObjectNumbers[0];
458
+ await doc.page(pageObjectNumber).annotations.create({
459
+ subtype: "stamp",
460
+ rect: {
461
+ left: 0,
462
+ bottom: 0,
463
+ right: rasterSize.width,
464
+ top: rasterSize.height
264
465
  },
265
- format: "pdf",
266
- pageObjectNumber,
267
- subject: input.subject,
268
- categories: input.categories
269
- };
270
- await page.pieceInfo.update(STAMP_PIECEINFO_APP, metadataPatch(asset.id, asset.name, asset.kind, asset.subject, asset.categories));
271
- const bytes = await doc.pages.extract([pageObjectNumber]);
272
- let preview = suppliedPreview;
273
- if (!preview) preview = imageToPreview(await page.render.image({
274
- viewport: {
275
- kind: "width",
276
- width: config.previewWidth ?? DEFAULT_PREVIEW_WIDTH
277
- },
278
- background: "transparent",
279
- includeAnnotations: true,
280
- format: "png"
281
- }));
282
- appended = {
283
- asset,
284
- bytes,
285
- preview,
286
- canonical: await doc.download()
287
- };
288
- } finally {
289
- await doc.close();
466
+ source: new Uint8Array(resolved.bytes),
467
+ fit: "fill"
468
+ });
469
+ if ((await doc.pages.flatten([pageObjectNumber], "display")).results.some(({ status }) => status !== "applied")) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, "[stamp] flattening the raster into its page failed");
290
470
  }
291
- libraryBinaries.set(liveLibrary.id, appended.canonical);
292
- binaries.set(appended.asset.id, {
293
- bytes: appended.bytes,
294
- preview: appended.preview
471
+ const layout = (await doc.pages.list()).pages.find((candidate) => candidate.pageObjectNumber === pageObjectNumber);
472
+ const page = doc.page(pageObjectNumber);
473
+ if (!layout || !page.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need page layout and pieceInfo support");
474
+ await doc.pages.setName({
475
+ name: require_contract.stampKey(name, label),
476
+ pageObjectNumber
295
477
  });
296
- ctx.dispatch({
297
- type: "ASSET_ADDED",
298
- asset: appended.asset
299
- });
300
- return appended.asset.id;
301
- });
302
- }
303
- const size = input.size ?? ("width" in meta ? {
304
- width: meta.width,
305
- height: meta.height
306
- } : null);
307
- if (!size) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, "[stamp] a PDF asset needs `size` (its page size in points) — PDF bytes carry no sniffable dimensions");
308
- let preview = suppliedPreview;
309
- if (!preview && !isPdf) preview = {
310
- bytes: new Uint8Array(resolved.bytes),
311
- mimeType: meta.mimeType
312
- };
313
- const commitLooseAsset = (libraryId) => {
314
- const asset = {
315
- id: uid("stamp"),
316
- libraryId,
317
- kind: input.kind ?? "stamp",
318
- name: input.name,
319
- size,
320
- format: isPdf ? "pdf" : meta.mimeType === "image/png" ? "png" : "jpeg",
321
- subject: input.subject,
322
- categories: input.categories
323
- };
324
- binaries.set(asset.id, {
325
- bytes: new Uint8Array(resolved.bytes),
326
- preview
478
+ const asset = {
479
+ id: assetId,
480
+ libraryId: liveLibrary.id,
481
+ kind: input.kind ?? "stamp",
482
+ name,
483
+ label,
484
+ size: {
485
+ width: layout.size.width,
486
+ height: layout.size.height
487
+ },
488
+ pageObjectNumber,
489
+ ...input.subject !== void 0 ? { subject: input.subject } : {},
490
+ ...input.categories !== void 0 ? { categories: input.categories } : {}
491
+ };
492
+ await page.pieceInfo.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(asset.kind, {
493
+ subject: asset.subject,
494
+ categories: asset.categories
495
+ }));
496
+ appended = {
497
+ asset,
498
+ bytes: await doc.pages.extract([pageObjectNumber]),
499
+ preview: suppliedPreview ?? await renderThumbnail(page),
500
+ canonical: await doc.download()
501
+ };
502
+ } finally {
503
+ await doc.close();
504
+ }
505
+ libraryBinaries.set(liveLibrary.id, appended.canonical);
506
+ binaries.set(appended.asset.id, {
507
+ bytes: appended.bytes,
508
+ preview: appended.preview
327
509
  });
328
510
  ctx.dispatch({
329
511
  type: "ASSET_ADDED",
330
- asset
512
+ asset: appended.asset
331
513
  });
332
- return asset.id;
333
- };
334
- if (!targetLibrary) return commitLooseAsset(createLooseLibrary(input.name));
335
- return mutateLibrary(targetLibrary.id, async () => {
336
- const liveLibrary = ctx.getState().libraries[targetLibrary.id];
337
- if (liveLibrary?.storage !== "loose") throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] loose library '${targetLibrary.id}' no longer exists`);
338
- return commitLooseAsset(liveLibrary.id);
514
+ libraryChanged.emit({
515
+ libraryId: liveLibrary.id,
516
+ reason: "asset-added"
517
+ });
518
+ return appended.asset.id;
519
+ });
520
+ };
521
+ const addAssetFromAnnotations = async (documentId, pageObjectNumber, refs, input) => {
522
+ const doc = ctx.documentHandle(documentId);
523
+ if (!doc) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] target document '${documentId}' is not open`);
524
+ const page = doc.page(pageObjectNumber);
525
+ if (!page.annotations.exportAppearance) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] this document's engine cannot export annotation appearances");
526
+ const source = await page.annotations.exportAppearance(refs);
527
+ return addAsset({
528
+ ...input,
529
+ source
339
530
  });
340
531
  };
341
532
  const dropLibrary = (id) => {
342
533
  const library = ctx.getState().libraries[id];
343
- if (library) for (const assetId of library.assetIds) binaries.delete(assetId);
534
+ if (library) for (const assetId of library.assetIds) {
535
+ binaries.delete(assetId);
536
+ ghostRenders.delete(assetId);
537
+ }
344
538
  libraryBinaries.delete(id);
345
539
  ctx.dispatch({
346
540
  type: "LIBRARY_REMOVED",
@@ -354,54 +548,140 @@ function createStampCapability(ctx, config = {}) {
354
548
  const asset = ctx.getState().assets[id];
355
549
  if (!asset) return;
356
550
  const library = ctx.getState().libraries[asset.libraryId];
357
- if (library?.storage === "canonical-pdf") {
358
- if (library.assetIds.length === 1) {
359
- dropLibrary(library.id);
360
- return;
361
- }
362
- if (asset.pageObjectNumber === void 0) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical asset '${id}' has no page object number`);
363
- const canonicalBytes = libraryBinaries.get(library.id);
364
- if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${library.id}'`);
365
- const doc = await openAssetDocument(canonicalBytes);
366
- let rewritten;
367
- try {
368
- await doc.pages.delete([asset.pageObjectNumber]);
369
- rewritten = await doc.download();
370
- } finally {
371
- await doc.close();
372
- }
373
- libraryBinaries.set(library.id, rewritten);
551
+ if (!library) return;
552
+ const canonicalBytes = libraryBinaries.get(library.id);
553
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${library.id}'`);
554
+ const doc = await openAssetDocument(canonicalBytes);
555
+ let rewritten;
556
+ try {
557
+ await doc.pages.delete([asset.pageObjectNumber]);
558
+ rewritten = await doc.download();
559
+ } finally {
560
+ await doc.close();
374
561
  }
562
+ libraryBinaries.set(library.id, rewritten);
375
563
  binaries.delete(id);
564
+ ghostRenders.delete(id);
376
565
  ctx.dispatch({
377
566
  type: "ASSET_REMOVED",
378
567
  assetId: id
379
568
  });
569
+ libraryChanged.emit({
570
+ libraryId: library.id,
571
+ reason: "asset-removed"
572
+ });
380
573
  });
381
574
  };
382
- const removeLibrary = (id) => mutateLibrary(id, async () => dropLibrary(id));
383
- const surfaceScriptingResult = (result) => {
384
- try {
385
- for (const effect of result.uiEffects) config.scripting?.onUiEffect?.(effect);
386
- for (const diagnostic of result.diagnostics) config.scripting?.onDiagnostic?.(diagnostic);
387
- if (result.error) config.scripting?.onError?.(result.error);
388
- } catch (error) {
389
- globalThis.console?.error("[stamp] scripting observer failed:", error);
390
- }
575
+ const removeLibrary = (id) => mutateLibrary(id, async () => {
576
+ const existed = ctx.getState().libraries[id] !== void 0;
577
+ dropLibrary(id);
578
+ if (existed) libraryChanged.emit({
579
+ libraryId: id,
580
+ reason: "removed"
581
+ });
582
+ });
583
+ const updateAsset = async (id, patch) => {
584
+ const initial = ctx.getState().assets[id];
585
+ if (!initial) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${id}'`);
586
+ return mutateLibrary(initial.libraryId, async () => {
587
+ const asset = ctx.getState().assets[id];
588
+ const library = asset ? ctx.getState().libraries[asset.libraryId] : void 0;
589
+ if (!asset || !library) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${id}'`);
590
+ const next = {
591
+ ...asset,
592
+ ...patch.label !== void 0 ? { label: patch.label } : {},
593
+ ...patch.categories !== void 0 ? { categories: patch.categories } : {}
594
+ };
595
+ if (patch.subject === null) delete next.subject;
596
+ else if (patch.subject !== void 0) next.subject = patch.subject;
597
+ const canonicalBytes = libraryBinaries.get(library.id);
598
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${library.id}'`);
599
+ const doc = await openAssetDocument(canonicalBytes);
600
+ let rewritten;
601
+ try {
602
+ requireCanonicalServices(doc);
603
+ if (next.label !== asset.label) await doc.pages.setName({
604
+ name: require_contract.stampKey(asset.name, next.label),
605
+ pageObjectNumber: asset.pageObjectNumber,
606
+ replace: require_contract.stampKey(asset.name, asset.label)
607
+ });
608
+ await doc.page(asset.pageObjectNumber).pieceInfo?.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(next.kind, {
609
+ subject: next.subject,
610
+ categories: next.categories
611
+ }));
612
+ rewritten = await doc.download();
613
+ } finally {
614
+ await doc.close();
615
+ }
616
+ libraryBinaries.set(library.id, rewritten);
617
+ ctx.dispatch({
618
+ type: "ASSET_UPDATED",
619
+ asset: next
620
+ });
621
+ libraryChanged.emit({
622
+ libraryId: library.id,
623
+ reason: "asset-updated"
624
+ });
625
+ });
626
+ };
627
+ const updateLibrary = async (id, patch) => {
628
+ if (!ctx.getState().libraries[id]) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown library '${id}'`);
629
+ return mutateLibrary(id, async () => {
630
+ const library = ctx.getState().libraries[id];
631
+ if (!library) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown library '${id}'`);
632
+ const canonicalBytes = libraryBinaries.get(id);
633
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${id}'`);
634
+ const next = {
635
+ ...library,
636
+ ...patch.name !== void 0 ? { name: patch.name } : {},
637
+ ...patch.categories !== void 0 ? { categories: patch.categories } : {}
638
+ };
639
+ const doc = await openAssetDocument(canonicalBytes);
640
+ let rewritten;
641
+ try {
642
+ requireCanonicalServices(doc);
643
+ if (next.name !== library.name) await doc.metadata.update({ title: next.name });
644
+ await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(id, {
645
+ kind: next.kind,
646
+ categories: next.categories,
647
+ locale: next.locale
648
+ }));
649
+ rewritten = await doc.download();
650
+ } finally {
651
+ await doc.close();
652
+ }
653
+ libraryBinaries.set(id, rewritten);
654
+ ctx.dispatch({
655
+ type: "LIBRARY_UPDATED",
656
+ library: next
657
+ });
658
+ libraryChanged.emit({
659
+ libraryId: id,
660
+ reason: "updated"
661
+ });
662
+ });
391
663
  };
392
664
  /**
393
665
  * Form-backed stamps are executable templates only until placement. Work
394
666
  * on an isolated copy so neither the canonical library nor its reusable
395
667
  * per-page extraction becomes target/user/time specific.
668
+ *
669
+ * The realm comes from the TARGET document's actions plugin — a detached
670
+ * realm under that document's policy and environment (WP4: its own
671
+ * sandbox, none of the viewer realm's globals). No actions plugin, or
672
+ * scripting off, or `dynamic: false` → the template is armed as-is.
396
673
  */
397
674
  const materializeForPlacement = async (documentId, asset, bin) => {
398
- if (asset.format !== "pdf" || !config.scripting?.enabled) return bin;
399
- const targetDoc = ctx.documentHandle(documentId);
675
+ if (config.dynamic === false) return bin;
676
+ const actions = ctx.tryForDocument(_embedpdf_plugin_actions_contract_host.ActionsToken, documentId);
677
+ const mintRealm = actions?.createDetachedScriptRealm;
678
+ if (!actions || !mintRealm) return bin;
400
679
  const targetMeta = ctx.core().documents[documentId] ?? null;
401
- if (!targetDoc || !targetMeta) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] target document '${documentId}' is not open`);
680
+ if (!targetMeta) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] target document '${documentId}' is not open`);
402
681
  const canonicalBytes = libraryBinaries.get(asset.libraryId);
403
682
  const sourceBytes = canonicalBytes ?? bin.bytes;
404
- const doc = await openAssetDocument(new Uint8Array(sourceBytes), targetDoc.security.identity);
683
+ const doc = await openAssetDocument(new Uint8Array(sourceBytes));
684
+ let realm = null;
405
685
  let scripting = null;
406
686
  try {
407
687
  const layout = await doc.pages.list();
@@ -409,13 +689,24 @@ function createStampCapability(ctx, config = {}) {
409
689
  if (!selectedPage) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, canonicalBytes ? `[stamp] canonical page ${asset.pageObjectNumber ?? "unknown"} no longer exists` : "[stamp] a loose dynamic stamp asset must contain exactly one page");
410
690
  if (!(await doc.forms.list()).fields.some((field) => field.widgets.some(({ pageObjectNumber }) => pageObjectNumber === selectedPage.pageObjectNumber))) return bin;
411
691
  if (!doc.pages.flatten || !doc.pages.extract) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] dynamic PDF stamps need an asset engine with pages.flatten and pages.extract");
692
+ realm = mintRealm.call(actions, {
693
+ doc,
694
+ document: () => targetMeta,
695
+ bootSources: async () => {
696
+ return (doc.actions ? await doc.actions.read() : null)?.nameTreeScripts.map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action)) ?? [];
697
+ }
698
+ });
412
699
  scripting = (0, _embedpdf_plugin_form_scripting.createFormScriptingController)({
413
700
  doc,
414
701
  document: () => targetMeta,
415
- config: config.scripting
702
+ transaction: realm.transaction.bind(realm),
703
+ budget: realm.budget
416
704
  });
417
705
  const result = await scripting.recalculate();
418
- surfaceScriptingResult(result);
706
+ actions.surfaceScriptCommit(result, {
707
+ origin: "user",
708
+ realm: "detached"
709
+ });
419
710
  if (result.status === "failed") throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] dynamic stamp scripting failed: ${result.error?.message ?? "native form effect failed"}`);
420
711
  const pageObjectNumber = selectedPage.pageObjectNumber;
421
712
  const failed = (await doc.pages.flatten([pageObjectNumber], "display")).results.find(({ status }) => status === "failed" || status === "skipped");
@@ -436,6 +727,7 @@ function createStampCapability(ctx, config = {}) {
436
727
  };
437
728
  } finally {
438
729
  scripting?.dispose();
730
+ realm?.dispose();
439
731
  await doc.close();
440
732
  }
441
733
  };
@@ -445,17 +737,86 @@ function createStampCapability(ctx, config = {}) {
445
737
  if (!asset || !bin) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${assetId}'`);
446
738
  const placement = await materializeForPlacement(documentId, asset, bin);
447
739
  await ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).armStamp({
448
- source: placement.bytes,
449
- preview: placement.preview ? {
450
- data: placement.preview.bytes,
451
- mimeType: placement.preview.mimeType
452
- } : void 0,
453
- intrinsicSize: asset.size,
740
+ ...stampPayload(asset, placement, placement !== bin),
454
741
  targetWidth: opts?.targetWidth
455
742
  });
743
+ armedByDocument.set(documentId, assetId);
744
+ };
745
+ /** Which asset this plugin armed per document. The annotation plugin owns
746
+ * the arm itself (a tool switch disarms it), so the answer is checked
747
+ * against it and forgotten once it no longer holds one. */
748
+ const armedByDocument = /* @__PURE__ */ new Map();
749
+ const armedAsset = (documentId) => {
750
+ const assetId = armedByDocument.get(documentId);
751
+ if (assetId === void 0) return null;
752
+ if (!ctx.tryForDocument(_embedpdf_plugin_annotation_contract_host.AnnotationToken, documentId)?.hasArmedStamp()) {
753
+ armedByDocument.delete(documentId);
754
+ return null;
755
+ }
756
+ return ctx.getState().assets[assetId] ?? null;
757
+ };
758
+ /**
759
+ * Ghost renders, one per size bucket per asset, rendered lazily on the
760
+ * asset engine the first time the ghost is shown at that size and kept
761
+ * until the asset goes away. Materialized (dynamic) placements are
762
+ * user/time specific and cache only for their own arm (`cacheKey` null).
763
+ */
764
+ const ghostRenders = /* @__PURE__ */ new Map();
765
+ const renderGhost = async (bytes, devicePixelWidth) => {
766
+ const doc = await openAssetDocument(new Uint8Array(bytes));
767
+ try {
768
+ const page = (await doc.pages.list()).pages[0];
769
+ if (!page) return null;
770
+ return imageToPreview(await doc.page(page.pageObjectNumber).render.image({
771
+ viewport: {
772
+ kind: "width",
773
+ width: devicePixelWidth
774
+ },
775
+ background: "transparent",
776
+ includeAnnotations: true,
777
+ format: "png"
778
+ }));
779
+ } finally {
780
+ await doc.close();
781
+ }
782
+ };
783
+ /** A resolution-aware ghost source for placement bytes: the page renders
784
+ * at the requested width, cached per bucket under `cacheKey` when given. */
785
+ const ghostProvider = (bytes, fallback, cacheKey) => {
786
+ const local = cacheKey ? ghostRenders.get(cacheKey) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map();
787
+ if (cacheKey) ghostRenders.set(cacheKey, local);
788
+ return (devicePixelWidth) => {
789
+ let pending = local.get(devicePixelWidth);
790
+ if (!pending) {
791
+ pending = renderGhost(bytes, devicePixelWidth).catch((error) => {
792
+ globalThis.console?.warn("[stamp] ghost render failed, using the thumbnail:", error);
793
+ return fallback;
794
+ });
795
+ local.set(devicePixelWidth, pending);
796
+ }
797
+ return pending;
798
+ };
799
+ };
800
+ /** The one payload both entry points hand the annotation plugin: artwork,
801
+ * a resolution-aware ghost, true size, and the identity a placement
802
+ * writes (`/Name` = identifier, `/Subj` = the subject override or label). */
803
+ const stampPayload = (asset, placement, materialized) => ({
804
+ source: placement.bytes,
805
+ preview: ghostProvider(placement.bytes, placement.preview, materialized ? null : asset.id),
806
+ intrinsicSize: asset.size,
807
+ name: asset.name,
808
+ subject: asset.subject ?? asset.label
809
+ });
810
+ const placeAsset = async (documentId, assetId, placement) => {
811
+ const asset = ctx.getState().assets[assetId];
812
+ const bin = binaries.get(assetId);
813
+ if (!asset || !bin) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${assetId}'`);
814
+ const materialized = await materializeForPlacement(documentId, asset, bin);
815
+ return ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).placeStamp(stampPayload(asset, materialized, materialized !== bin), placement);
456
816
  };
457
817
  ctx.cleanup(() => {
458
818
  binaries.clear();
819
+ ghostRenders.clear();
459
820
  libraryBinaries.clear();
460
821
  libraryMutationTails.clear();
461
822
  const ownedAssetEngine = typeof config.assetEngine === "function" ? assetEngineRef : null;
@@ -463,9 +824,10 @@ function createStampCapability(ctx, config = {}) {
463
824
  if (ownedAssetEngine) Promise.resolve(ownedAssetEngine).then((engine) => engine.destroy()).catch(() => {});
464
825
  });
465
826
  return {
466
- libraries: () => {
827
+ libraries: (query) => {
467
828
  const s = ctx.getState();
468
- return s.libraryOrder.map((id) => s.libraries[id]).filter((l) => l != null);
829
+ const kinds = query?.kind === void 0 ? null : new Set(typeof query.kind === "string" ? [query.kind] : query.kind);
830
+ return s.libraryOrder.map((id) => s.libraries[id]).filter((l) => l != null && (kinds === null || kinds.has(l.kind)));
469
831
  },
470
832
  library: (id) => ctx.getState().libraries[id] ?? null,
471
833
  assets: (libraryId) => {
@@ -479,13 +841,23 @@ function createStampCapability(ctx, config = {}) {
479
841
  asset: (id) => ctx.getState().assets[id] ?? null,
480
842
  assetPreview: (id) => binaries.get(id)?.preview ?? null,
481
843
  assetBytes: (id) => binaries.get(id)?.bytes ?? null,
482
- libraryBytes: (id) => libraryBinaries.get(id) ?? null,
844
+ exportLibrary: (id) => libraryBinaries.get(id) ?? null,
845
+ onLibraryChanged: libraryChanged.on,
846
+ createLibrary,
847
+ updateLibrary,
483
848
  importLibraryPdf,
484
849
  addAsset,
850
+ addAssetFromAnnotations,
851
+ updateAsset,
485
852
  removeAsset,
486
853
  removeLibrary,
487
854
  armAsset,
488
- disarm: (documentId) => ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).disarmStamp()
855
+ placeAsset,
856
+ disarm: (documentId) => {
857
+ armedByDocument.delete(documentId);
858
+ ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).disarmStamp();
859
+ },
860
+ armedAsset
489
861
  };
490
862
  }
491
863
  //#endregion
@@ -507,6 +879,13 @@ const stampReducer = (state, action) => {
507
879
  },
508
880
  libraryOrder: [...state.libraryOrder, action.library.id]
509
881
  };
882
+ case "LIBRARY_UPDATED": return state.libraries[action.library.id] ? {
883
+ ...state,
884
+ libraries: {
885
+ ...state.libraries,
886
+ [action.library.id]: action.library
887
+ }
888
+ } : state;
510
889
  case "LIBRARY_REMOVED": {
511
890
  const library = state.libraries[action.libraryId];
512
891
  if (!library) return state;
@@ -539,6 +918,13 @@ const stampReducer = (state, action) => {
539
918
  }
540
919
  };
541
920
  }
921
+ case "ASSET_UPDATED": return state.assets[action.asset.id] ? {
922
+ ...state,
923
+ assets: {
924
+ ...state.assets,
925
+ [action.asset.id]: action.asset
926
+ }
927
+ } : state;
542
928
  case "ASSET_REMOVED": {
543
929
  const asset = state.assets[action.assetId];
544
930
  if (!asset) return state;
@@ -579,7 +965,94 @@ const stampPlugin = (config = {}) => (0, _embedpdf_core.definePlugin)({
579
965
  capability: (ctx) => createStampCapability(ctx, config)
580
966
  });
581
967
  //#endregion
968
+ //#region src/persistence.ts
969
+ /** An in-memory store — tests and SSR. */
970
+ function memoryStampStore() {
971
+ const rows = /* @__PURE__ */ new Map();
972
+ return {
973
+ list: async () => [...rows.entries()].map(([id, bytes]) => ({
974
+ id,
975
+ bytes
976
+ })),
977
+ put: async (id, bytes) => {
978
+ rows.set(id, new Uint8Array(bytes));
979
+ },
980
+ delete: async (id) => {
981
+ rows.delete(id);
982
+ }
983
+ };
984
+ }
985
+ /**
986
+ * Bring every stored library back: each PDF is imported as-is (its title,
987
+ * registry, and PieceInfo id come from the file — the store's key is only a
988
+ * hint). Call once at boot, before seeding defaults, so "already has
989
+ * libraries" means the user's own.
990
+ */
991
+ async function restoreStampLibraries(stamp, store) {
992
+ const restored = [];
993
+ for (const { id, bytes } of await store.list()) try {
994
+ restored.push(await stamp.importLibraryPdf(bytes));
995
+ } catch (error) {
996
+ globalThis.console?.warn(`[stamp] stored library '${id}' could not be restored:`, error);
997
+ }
998
+ return restored;
999
+ }
1000
+ /**
1001
+ * Keep the store in sync from now on: every canonical change writes the
1002
+ * library's PDF (coalesced per library so a burst of edits saves once),
1003
+ * a removal deletes it. `except` names libraries never to persist — the
1004
+ * bundled default set, typically. Returns the unsubscribe.
1005
+ */
1006
+ function persistStampLibraries(stamp, store, opts = {}) {
1007
+ const except = new Set(opts.except ?? []);
1008
+ const timers = /* @__PURE__ */ new Map();
1009
+ const write = (libraryId) => {
1010
+ timers.delete(libraryId);
1011
+ const bytes = stamp.exportLibrary(libraryId);
1012
+ if (!bytes) return;
1013
+ store.put(libraryId, bytes).catch((error) => {
1014
+ globalThis.console?.warn(`[stamp] persisting library '${libraryId}' failed:`, error);
1015
+ });
1016
+ };
1017
+ const off = stamp.onLibraryChanged(({ libraryId, reason }) => {
1018
+ if (except.has(libraryId)) return;
1019
+ const pending = timers.get(libraryId);
1020
+ if (pending) clearTimeout(pending);
1021
+ if (reason === "removed") {
1022
+ timers.delete(libraryId);
1023
+ store.delete(libraryId).catch((error) => {
1024
+ globalThis.console?.warn(`[stamp] deleting stored library '${libraryId}' failed:`, error);
1025
+ });
1026
+ return;
1027
+ }
1028
+ timers.set(libraryId, setTimeout(() => write(libraryId), opts.debounceMs ?? 250));
1029
+ });
1030
+ return () => {
1031
+ off();
1032
+ for (const [libraryId, timer] of timers) {
1033
+ clearTimeout(timer);
1034
+ write(libraryId);
1035
+ }
1036
+ };
1037
+ }
1038
+ //#endregion
1039
+ exports.DEFAULT_LIBRARY_KIND = require_contract.DEFAULT_LIBRARY_KIND;
1040
+ exports.STAMP_LIBRARY_PIECEINFO_APP = require_contract.STAMP_LIBRARY_PIECEINFO_APP;
1041
+ exports.STAMP_PIECEINFO_APP = require_contract.STAMP_PIECEINFO_APP;
1042
+ exports.STAMP_PIECEINFO_VERSION = require_contract.STAMP_PIECEINFO_VERSION;
582
1043
  exports.StampToken = require_contract.StampToken;
1044
+ exports.assetIdFor = require_contract.assetIdFor;
1045
+ exports.customStampName = require_contract.customStampName;
1046
+ exports.libraryKindFromPdfName = require_contract.libraryKindFromPdfName;
1047
+ exports.libraryKindToPdfName = require_contract.libraryKindToPdfName;
1048
+ exports.memoryStampStore = memoryStampStore;
1049
+ exports.parseStampKey = require_contract.parseStampKey;
1050
+ exports.persistStampLibraries = persistStampLibraries;
1051
+ exports.restoreStampLibraries = restoreStampLibraries;
1052
+ exports.stampKey = require_contract.stampKey;
1053
+ exports.stampKindToPdfName = require_contract.stampKindToPdfName;
1054
+ exports.stampLibraryPieceInfo = require_contract.stampLibraryPieceInfo;
1055
+ exports.stampPieceInfo = require_contract.stampPieceInfo;
583
1056
  exports.stampPlugin = stampPlugin;
584
1057
 
585
1058
  //# sourceMappingURL=index.cjs.map