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