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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,14 +1,43 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_contract = require("./contract-DmQ4peF2.cjs");
2
+ const require_contract = require("./contract-C6CgKD69.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");
6
8
  let _embedpdf_plugin_form_scripting = require("@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
7
39
  //#region src/capability.ts
8
40
  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;
12
41
  /** Session-unique ids. Assets are session-scoped for now (no persistence),
13
42
  * so a timestamp + counter is enough — durable ids come with the store port. */
14
43
  let seq = 0;
@@ -25,7 +54,6 @@ const entryStringArray = (entries, key) => {
25
54
  const entry = entries[key];
26
55
  return entry?.type === "string-array" ? [...entry.value] : void 0;
27
56
  };
28
- const kindToPdfName = (kind) => kind === "signature" ? "Signature" : kind === "initials" ? "Initials" : "Stamp";
29
57
  const kindFromPdfName = (name) => {
30
58
  switch (name?.toLowerCase()) {
31
59
  case "stamp": return "stamp";
@@ -34,21 +62,6 @@ const kindFromPdfName = (name) => {
34
62
  default: return;
35
63
  }
36
64
  };
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
65
  function createStampCapability(ctx, config = {}) {
53
66
  /** Binary sidecar of the serializable store: asset bytes + cached preview,
54
67
  * keyed by asset id. The reducer never sees these (kernel rule 1). */
@@ -75,16 +88,13 @@ function createStampCapability(ctx, config = {}) {
75
88
  }
76
89
  return assetEngineRef;
77
90
  };
78
- const openAssetDocument = async (bytes, identity) => {
91
+ const openAssetDocument = async (bytes) => {
79
92
  try {
80
93
  return await (await assetEngine()).open({
81
94
  kind: "bytes",
82
95
  id: uid("stamp-import"),
83
96
  bytes
84
- }, {
85
- scope: ["*"],
86
- ...identity ? { identity } : {}
87
- });
97
+ }, { scope: ["*"] });
88
98
  } catch (err) {
89
99
  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
100
  throw err;
@@ -107,94 +117,151 @@ function createStampCapability(ctx, config = {}) {
107
117
  taken.add(id);
108
118
  return id;
109
119
  };
110
- const createLooseLibrary = (name) => {
111
- const id = uid("stamp-lib");
120
+ const libraryChanged = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[stamp] onLibraryChanged observer failed:", error));
121
+ /** Thumbnail render of one library page (the small picker image). */
122
+ const renderThumbnail = async (handle) => imageToPreview(await handle.render.image({
123
+ viewport: {
124
+ kind: "width",
125
+ width: config.previewWidth ?? DEFAULT_PREVIEW_WIDTH
126
+ },
127
+ background: "transparent",
128
+ includeAnnotations: true,
129
+ format: "png"
130
+ }));
131
+ const createLibrary = async (name, opts) => {
132
+ const taken = new Set(Object.keys(ctx.getState().libraries));
133
+ const id = allocateId(opts?.id, "stamp-lib", taken);
134
+ const doc = await openAssetDocument(blankLibraryPdf());
135
+ let bytes;
136
+ try {
137
+ requireCanonicalServices(doc);
138
+ await doc.metadata.update({ title: name });
139
+ await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(id, { categories: opts?.categories }));
140
+ bytes = await doc.download();
141
+ } finally {
142
+ await doc.close();
143
+ }
144
+ libraryBinaries.set(id, bytes);
112
145
  ctx.dispatch({
113
146
  type: "LIBRARY_ADDED",
114
147
  library: {
115
148
  id,
116
149
  name,
117
- storage: "loose",
118
- assetIds: []
150
+ assetIds: [],
151
+ ...opts?.categories ? { categories: opts.categories } : {}
119
152
  }
120
153
  });
154
+ libraryChanged.emit({
155
+ libraryId: id,
156
+ reason: "created"
157
+ });
121
158
  return id;
122
159
  };
160
+ const requireCanonicalServices = (doc) => {
161
+ 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");
162
+ 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)");
163
+ };
123
164
  const importLibraryPdf = async (source, opts) => {
124
165
  const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(source);
125
166
  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
167
  const doc = await openAssetDocument(new Uint8Array(resolved.bytes));
127
168
  let imported;
128
169
  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);
170
+ requireCanonicalServices(doc);
171
+ const layout = await doc.pages.list();
172
+ 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");
173
+ const byPon = new Map(layout.pages.map((page) => [page.pageObjectNumber, page]));
174
+ const catalogEntries = (await doc.pieceInfo.read("EMBD_StampLibrary"))?.entries ?? {};
143
175
  const state = ctx.getState();
144
176
  const takenLibraryIds = new Set(Object.keys(state.libraries));
145
- const takenAssetIds = new Set(Object.keys(state.assets));
146
- const catalogEntries = catalogMetadata?.entries ?? {};
147
177
  const libraryId = allocateId(entryString(catalogEntries, "Id"), "stamp-lib", takenLibraryIds);
148
- const libraryName = opts?.name ?? entryString(catalogEntries, "Name") ?? "Stamps";
178
+ const docMeta = await doc.metadata.read();
179
+ const libraryName = (docMeta.title && docMeta.title.length > 0 ? docMeta.title : void 0) ?? entryString(catalogEntries, "Name") ?? opts?.name ?? "Stamps";
180
+ if (!docMeta.title) await doc.metadata.update({ title: libraryName });
149
181
  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));
182
+ const libraryLocale = entryString(catalogEntries, "Locale");
183
+ const registry = (layout.namedPages ?? []).filter((entry) => entry.target.kind === "page");
184
+ const hidden = (layout.namedPages ?? []).filter((entry) => entry.target.kind !== "page");
185
+ 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`);
186
+ let descriptors;
187
+ if (registry.length > 0) {
188
+ const seen = /* @__PURE__ */ new Set();
189
+ descriptors = registry.map((entry) => {
190
+ const { name, label } = require_contract.parseStampKey(entry.name);
191
+ 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}'`);
192
+ if (seen.has(name)) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.InvalidArg, `[stamp] library '${libraryName}': duplicate stamp identifier '${name}'`);
193
+ seen.add(name);
194
+ const page = byPon.get(entry.target.pageObjectNumber);
195
+ return {
196
+ pageObjectNumber: page.pageObjectNumber,
197
+ index: page.index,
198
+ name,
199
+ label
200
+ };
201
+ });
202
+ descriptors.sort((a, b) => a.index - b.index);
203
+ } 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");
204
+ else {
205
+ descriptors = [];
206
+ for (const page of layout.pages) {
207
+ const v1 = (await doc.page(page.pageObjectNumber).pieceInfo?.read(require_contract.STAMP_PIECEINFO_APP))?.entries;
208
+ const name = (v1 && entryString(v1, "Name")) ?? `Stamp${page.index + 1}`;
209
+ const label = (v1 && entryString(v1, "Subject")) ?? opts?.assetName?.(page.index) ?? name;
210
+ descriptors.push({
211
+ pageObjectNumber: page.pageObjectNumber,
212
+ index: page.index,
213
+ name,
214
+ label
215
+ });
216
+ }
217
+ for (const d of descriptors) await doc.pages.setName({
218
+ name: require_contract.stampKey(d.name, d.label),
219
+ pageObjectNumber: d.pageObjectNumber
220
+ });
221
+ }
222
+ await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(libraryId, {
223
+ categories: libraryCategories,
224
+ locale: libraryLocale
225
+ }));
173
226
  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
227
+ for (const d of descriptors) {
228
+ const handle = doc.page(d.pageObjectNumber);
229
+ if (!handle.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need page pieceInfo support");
230
+ const entries = (await handle.pieceInfo.read("EMBD_Stamp"))?.entries ?? {};
231
+ const kind = opts?.kind ?? kindFromPdfName(entryName(entries, "Kind")) ?? "stamp";
232
+ const subject = entryString(entries, "SubjectOverride");
233
+ const categories = entryStringArray(entries, "Categories");
234
+ const page = byPon.get(d.pageObjectNumber);
235
+ const asset = {
236
+ id: require_contract.assetIdFor(libraryId, d.name),
237
+ libraryId,
238
+ kind,
239
+ name: d.name,
240
+ label: d.label,
241
+ size: {
242
+ width: page.size.width,
243
+ height: page.size.height
182
244
  },
183
- background: "transparent",
184
- includeAnnotations: true,
185
- format: "png"
186
- });
245
+ pageObjectNumber: d.pageObjectNumber,
246
+ ...subject !== void 0 ? { subject } : {},
247
+ ...categories !== void 0 ? { categories } : {}
248
+ };
249
+ await handle.pieceInfo.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(kind, {
250
+ subject,
251
+ categories
252
+ }));
253
+ const bytes = await doc.pages.extract([d.pageObjectNumber]);
187
254
  assets.push({
188
255
  asset,
189
256
  bytes,
190
- preview: imageToPreview(image)
257
+ preview: await renderThumbnail(handle)
191
258
  });
192
259
  }
193
260
  imported = {
194
261
  library: {
195
262
  id: libraryId,
196
263
  name: libraryName,
197
- storage: "canonical-pdf",
264
+ ...libraryLocale ? { locale: libraryLocale } : {},
198
265
  categories: libraryCategories,
199
266
  assetIds: []
200
267
  },
@@ -219,15 +286,26 @@ function createStampCapability(ctx, config = {}) {
219
286
  asset
220
287
  });
221
288
  }
289
+ libraryChanged.emit({
290
+ libraryId: imported.library.id,
291
+ reason: "imported"
292
+ });
222
293
  return imported.library.id;
223
294
  };
224
295
  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}'`);
296
+ 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}'`);
227
297
  const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source);
228
298
  const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes);
229
299
  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
300
  const isPdf = meta.mimeType === "application/pdf";
301
+ const name = input.name ?? require_contract.customStampName();
302
+ const label = input.label ?? name;
303
+ 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 '='`);
304
+ const rasterSize = input.size ?? ("width" in meta ? {
305
+ width: meta.width,
306
+ height: meta.height
307
+ } : null);
308
+ 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
309
  let suppliedPreview = null;
232
310
  if (input.preview) {
233
311
  const preview = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.preview);
@@ -236,111 +314,106 @@ function createStampCapability(ctx, config = {}) {
236
314
  mimeType: preview.mimeType ?? "image/png"
237
315
  };
238
316
  }
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 {
317
+ const libraryId = input.libraryId ?? await createLibrary(label);
318
+ return mutateLibrary(libraryId, async () => {
319
+ const liveLibrary = ctx.getState().libraries[libraryId];
320
+ if (!liveLibrary) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] library '${libraryId}' no longer exists`);
321
+ const assetId = require_contract.assetIdFor(liveLibrary.id, name);
322
+ 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}'`);
323
+ const canonicalBytes = libraryBinaries.get(liveLibrary.id);
324
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${liveLibrary.id}'`);
325
+ const doc = await openAssetDocument(canonicalBytes);
326
+ let appended;
327
+ try {
328
+ requireCanonicalServices(doc);
329
+ let pageObjectNumber;
330
+ if (isPdf) {
250
331
  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
264
- },
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
332
+ 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");
333
+ pageObjectNumber = result.insertedPageObjectNumbers[0];
334
+ } else {
335
+ 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");
336
+ pageObjectNumber = (await doc.pages.insertBlank({ size: rasterSize })).insertedPageObjectNumbers[0];
337
+ await doc.page(pageObjectNumber).annotations.create({
338
+ subtype: "stamp",
339
+ rect: {
340
+ left: 0,
341
+ bottom: 0,
342
+ right: rasterSize.width,
343
+ top: rasterSize.height
277
344
  },
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();
345
+ source: new Uint8Array(resolved.bytes),
346
+ fit: "fill"
347
+ });
348
+ 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
349
  }
291
- libraryBinaries.set(liveLibrary.id, appended.canonical);
292
- binaries.set(appended.asset.id, {
293
- bytes: appended.bytes,
294
- preview: appended.preview
295
- });
296
- ctx.dispatch({
297
- type: "ASSET_ADDED",
298
- asset: appended.asset
350
+ const layout = (await doc.pages.list()).pages.find((candidate) => candidate.pageObjectNumber === pageObjectNumber);
351
+ const page = doc.page(pageObjectNumber);
352
+ 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");
353
+ await doc.pages.setName({
354
+ name: require_contract.stampKey(name, label),
355
+ pageObjectNumber
299
356
  });
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
357
+ const asset = {
358
+ id: assetId,
359
+ libraryId: liveLibrary.id,
360
+ kind: input.kind ?? "stamp",
361
+ name,
362
+ label,
363
+ size: {
364
+ width: layout.size.width,
365
+ height: layout.size.height
366
+ },
367
+ pageObjectNumber,
368
+ ...input.subject !== void 0 ? { subject: input.subject } : {},
369
+ ...input.categories !== void 0 ? { categories: input.categories } : {}
370
+ };
371
+ await page.pieceInfo.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(asset.kind, {
372
+ subject: asset.subject,
373
+ categories: asset.categories
374
+ }));
375
+ appended = {
376
+ asset,
377
+ bytes: await doc.pages.extract([pageObjectNumber]),
378
+ preview: suppliedPreview ?? await renderThumbnail(page),
379
+ canonical: await doc.download()
380
+ };
381
+ } finally {
382
+ await doc.close();
383
+ }
384
+ libraryBinaries.set(liveLibrary.id, appended.canonical);
385
+ binaries.set(appended.asset.id, {
386
+ bytes: appended.bytes,
387
+ preview: appended.preview
327
388
  });
328
389
  ctx.dispatch({
329
390
  type: "ASSET_ADDED",
330
- asset
391
+ asset: appended.asset
331
392
  });
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);
393
+ libraryChanged.emit({
394
+ libraryId: liveLibrary.id,
395
+ reason: "asset-added"
396
+ });
397
+ return appended.asset.id;
398
+ });
399
+ };
400
+ const addAssetFromAnnotations = async (documentId, pageObjectNumber, refs, input) => {
401
+ const doc = ctx.documentHandle(documentId);
402
+ if (!doc) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] target document '${documentId}' is not open`);
403
+ const page = doc.page(pageObjectNumber);
404
+ 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");
405
+ const source = await page.annotations.exportAppearance(refs);
406
+ return addAsset({
407
+ ...input,
408
+ source
339
409
  });
340
410
  };
341
411
  const dropLibrary = (id) => {
342
412
  const library = ctx.getState().libraries[id];
343
- if (library) for (const assetId of library.assetIds) binaries.delete(assetId);
413
+ if (library) for (const assetId of library.assetIds) {
414
+ binaries.delete(assetId);
415
+ ghostRenders.delete(assetId);
416
+ }
344
417
  libraryBinaries.delete(id);
345
418
  ctx.dispatch({
346
419
  type: "LIBRARY_REMOVED",
@@ -354,54 +427,103 @@ function createStampCapability(ctx, config = {}) {
354
427
  const asset = ctx.getState().assets[id];
355
428
  if (!asset) return;
356
429
  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);
430
+ if (!library) return;
431
+ const canonicalBytes = libraryBinaries.get(library.id);
432
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${library.id}'`);
433
+ const doc = await openAssetDocument(canonicalBytes);
434
+ let rewritten;
435
+ try {
436
+ await doc.pages.delete([asset.pageObjectNumber]);
437
+ rewritten = await doc.download();
438
+ } finally {
439
+ await doc.close();
374
440
  }
441
+ libraryBinaries.set(library.id, rewritten);
375
442
  binaries.delete(id);
443
+ ghostRenders.delete(id);
376
444
  ctx.dispatch({
377
445
  type: "ASSET_REMOVED",
378
446
  assetId: id
379
447
  });
448
+ libraryChanged.emit({
449
+ libraryId: library.id,
450
+ reason: "asset-removed"
451
+ });
380
452
  });
381
453
  };
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
- }
454
+ const removeLibrary = (id) => mutateLibrary(id, async () => {
455
+ const existed = ctx.getState().libraries[id] !== void 0;
456
+ dropLibrary(id);
457
+ if (existed) libraryChanged.emit({
458
+ libraryId: id,
459
+ reason: "removed"
460
+ });
461
+ });
462
+ const updateAsset = async (id, patch) => {
463
+ const initial = ctx.getState().assets[id];
464
+ if (!initial) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${id}'`);
465
+ return mutateLibrary(initial.libraryId, async () => {
466
+ const asset = ctx.getState().assets[id];
467
+ const library = asset ? ctx.getState().libraries[asset.libraryId] : void 0;
468
+ if (!asset || !library) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${id}'`);
469
+ const next = {
470
+ ...asset,
471
+ ...patch.label !== void 0 ? { label: patch.label } : {},
472
+ ...patch.categories !== void 0 ? { categories: patch.categories } : {}
473
+ };
474
+ if (patch.subject === null) delete next.subject;
475
+ else if (patch.subject !== void 0) next.subject = patch.subject;
476
+ const canonicalBytes = libraryBinaries.get(library.id);
477
+ if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical bytes are missing for library '${library.id}'`);
478
+ const doc = await openAssetDocument(canonicalBytes);
479
+ let rewritten;
480
+ try {
481
+ requireCanonicalServices(doc);
482
+ if (next.label !== asset.label) await doc.pages.setName({
483
+ name: require_contract.stampKey(asset.name, next.label),
484
+ pageObjectNumber: asset.pageObjectNumber,
485
+ replace: require_contract.stampKey(asset.name, asset.label)
486
+ });
487
+ await doc.page(asset.pageObjectNumber).pieceInfo?.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(next.kind, {
488
+ subject: next.subject,
489
+ categories: next.categories
490
+ }));
491
+ rewritten = await doc.download();
492
+ } finally {
493
+ await doc.close();
494
+ }
495
+ libraryBinaries.set(library.id, rewritten);
496
+ ctx.dispatch({
497
+ type: "ASSET_UPDATED",
498
+ asset: next
499
+ });
500
+ libraryChanged.emit({
501
+ libraryId: library.id,
502
+ reason: "asset-updated"
503
+ });
504
+ });
391
505
  };
392
506
  /**
393
507
  * Form-backed stamps are executable templates only until placement. Work
394
508
  * on an isolated copy so neither the canonical library nor its reusable
395
509
  * per-page extraction becomes target/user/time specific.
510
+ *
511
+ * The realm comes from the TARGET document's actions plugin — a detached
512
+ * realm under that document's policy and environment (WP4: its own
513
+ * sandbox, none of the viewer realm's globals). No actions plugin, or
514
+ * scripting off, or `dynamic: false` → the template is armed as-is.
396
515
  */
397
516
  const materializeForPlacement = async (documentId, asset, bin) => {
398
- if (asset.format !== "pdf" || !config.scripting?.enabled) return bin;
399
- const targetDoc = ctx.documentHandle(documentId);
517
+ if (config.dynamic === false) return bin;
518
+ const actions = ctx.tryForDocument(_embedpdf_plugin_actions_contract_host.ActionsToken, documentId);
519
+ const mintRealm = actions?.createDetachedScriptRealm;
520
+ if (!actions || !mintRealm) return bin;
400
521
  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`);
522
+ if (!targetMeta) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] target document '${documentId}' is not open`);
402
523
  const canonicalBytes = libraryBinaries.get(asset.libraryId);
403
524
  const sourceBytes = canonicalBytes ?? bin.bytes;
404
- const doc = await openAssetDocument(new Uint8Array(sourceBytes), targetDoc.security.identity);
525
+ const doc = await openAssetDocument(new Uint8Array(sourceBytes));
526
+ let realm = null;
405
527
  let scripting = null;
406
528
  try {
407
529
  const layout = await doc.pages.list();
@@ -409,13 +531,24 @@ function createStampCapability(ctx, config = {}) {
409
531
  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
532
  if (!(await doc.forms.list()).fields.some((field) => field.widgets.some(({ pageObjectNumber }) => pageObjectNumber === selectedPage.pageObjectNumber))) return bin;
411
533
  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");
534
+ realm = mintRealm.call(actions, {
535
+ doc,
536
+ document: () => targetMeta,
537
+ bootSources: async () => {
538
+ return (doc.actions ? await doc.actions.read() : null)?.nameTreeScripts.map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action)) ?? [];
539
+ }
540
+ });
412
541
  scripting = (0, _embedpdf_plugin_form_scripting.createFormScriptingController)({
413
542
  doc,
414
543
  document: () => targetMeta,
415
- config: config.scripting
544
+ transaction: realm.transaction.bind(realm),
545
+ budget: realm.budget
416
546
  });
417
547
  const result = await scripting.recalculate();
418
- surfaceScriptingResult(result);
548
+ actions.surfaceScriptCommit(result, {
549
+ origin: "user",
550
+ realm: "detached"
551
+ });
419
552
  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
553
  const pageObjectNumber = selectedPage.pageObjectNumber;
421
554
  const failed = (await doc.pages.flatten([pageObjectNumber], "display")).results.find(({ status }) => status === "failed" || status === "skipped");
@@ -436,6 +569,7 @@ function createStampCapability(ctx, config = {}) {
436
569
  };
437
570
  } finally {
438
571
  scripting?.dispose();
572
+ realm?.dispose();
439
573
  await doc.close();
440
574
  }
441
575
  };
@@ -445,17 +579,72 @@ function createStampCapability(ctx, config = {}) {
445
579
  if (!asset || !bin) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${assetId}'`);
446
580
  const placement = await materializeForPlacement(documentId, asset, bin);
447
581
  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,
582
+ ...stampPayload(asset, placement, placement !== bin),
454
583
  targetWidth: opts?.targetWidth
455
584
  });
456
585
  };
586
+ /**
587
+ * Ghost renders, one per size bucket per asset, rendered lazily on the
588
+ * asset engine the first time the ghost is shown at that size and kept
589
+ * until the asset goes away. Materialized (dynamic) placements are
590
+ * user/time specific and cache only for their own arm (`cacheKey` null).
591
+ */
592
+ const ghostRenders = /* @__PURE__ */ new Map();
593
+ const renderGhost = async (bytes, devicePixelWidth) => {
594
+ const doc = await openAssetDocument(new Uint8Array(bytes));
595
+ try {
596
+ const page = (await doc.pages.list()).pages[0];
597
+ if (!page) return null;
598
+ return imageToPreview(await doc.page(page.pageObjectNumber).render.image({
599
+ viewport: {
600
+ kind: "width",
601
+ width: devicePixelWidth
602
+ },
603
+ background: "transparent",
604
+ includeAnnotations: true,
605
+ format: "png"
606
+ }));
607
+ } finally {
608
+ await doc.close();
609
+ }
610
+ };
611
+ /** A resolution-aware ghost source for placement bytes: the page renders
612
+ * at the requested width, cached per bucket under `cacheKey` when given. */
613
+ const ghostProvider = (bytes, fallback, cacheKey) => {
614
+ const local = cacheKey ? ghostRenders.get(cacheKey) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map();
615
+ if (cacheKey) ghostRenders.set(cacheKey, local);
616
+ return (devicePixelWidth) => {
617
+ let pending = local.get(devicePixelWidth);
618
+ if (!pending) {
619
+ pending = renderGhost(bytes, devicePixelWidth).catch((error) => {
620
+ globalThis.console?.warn("[stamp] ghost render failed, using the thumbnail:", error);
621
+ return fallback;
622
+ });
623
+ local.set(devicePixelWidth, pending);
624
+ }
625
+ return pending;
626
+ };
627
+ };
628
+ /** The one payload both entry points hand the annotation plugin: artwork,
629
+ * a resolution-aware ghost, true size, and the identity a placement
630
+ * writes (`/Name` = identifier, `/Subj` = the subject override or label). */
631
+ const stampPayload = (asset, placement, materialized) => ({
632
+ source: placement.bytes,
633
+ preview: ghostProvider(placement.bytes, placement.preview, materialized ? null : asset.id),
634
+ intrinsicSize: asset.size,
635
+ name: asset.name,
636
+ subject: asset.subject ?? asset.label
637
+ });
638
+ const placeAsset = async (documentId, assetId, placement) => {
639
+ const asset = ctx.getState().assets[assetId];
640
+ const bin = binaries.get(assetId);
641
+ if (!asset || !bin) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${assetId}'`);
642
+ const materialized = await materializeForPlacement(documentId, asset, bin);
643
+ return ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).placeStamp(stampPayload(asset, materialized, materialized !== bin), placement);
644
+ };
457
645
  ctx.cleanup(() => {
458
646
  binaries.clear();
647
+ ghostRenders.clear();
459
648
  libraryBinaries.clear();
460
649
  libraryMutationTails.clear();
461
650
  const ownedAssetEngine = typeof config.assetEngine === "function" ? assetEngineRef : null;
@@ -479,12 +668,17 @@ function createStampCapability(ctx, config = {}) {
479
668
  asset: (id) => ctx.getState().assets[id] ?? null,
480
669
  assetPreview: (id) => binaries.get(id)?.preview ?? null,
481
670
  assetBytes: (id) => binaries.get(id)?.bytes ?? null,
482
- libraryBytes: (id) => libraryBinaries.get(id) ?? null,
671
+ exportLibrary: (id) => libraryBinaries.get(id) ?? null,
672
+ onLibraryChanged: libraryChanged.on,
673
+ createLibrary,
483
674
  importLibraryPdf,
484
675
  addAsset,
676
+ addAssetFromAnnotations,
677
+ updateAsset,
485
678
  removeAsset,
486
679
  removeLibrary,
487
680
  armAsset,
681
+ placeAsset,
488
682
  disarm: (documentId) => ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).disarmStamp()
489
683
  };
490
684
  }
@@ -539,6 +733,13 @@ const stampReducer = (state, action) => {
539
733
  }
540
734
  };
541
735
  }
736
+ case "ASSET_UPDATED": return state.assets[action.asset.id] ? {
737
+ ...state,
738
+ assets: {
739
+ ...state.assets,
740
+ [action.asset.id]: action.asset
741
+ }
742
+ } : state;
542
743
  case "ASSET_REMOVED": {
543
744
  const asset = state.assets[action.assetId];
544
745
  if (!asset) return state;
@@ -579,7 +780,91 @@ const stampPlugin = (config = {}) => (0, _embedpdf_core.definePlugin)({
579
780
  capability: (ctx) => createStampCapability(ctx, config)
580
781
  });
581
782
  //#endregion
783
+ //#region src/persistence.ts
784
+ /** An in-memory store — tests and SSR. */
785
+ function memoryStampStore() {
786
+ const rows = /* @__PURE__ */ new Map();
787
+ return {
788
+ list: async () => [...rows.entries()].map(([id, bytes]) => ({
789
+ id,
790
+ bytes
791
+ })),
792
+ put: async (id, bytes) => {
793
+ rows.set(id, new Uint8Array(bytes));
794
+ },
795
+ delete: async (id) => {
796
+ rows.delete(id);
797
+ }
798
+ };
799
+ }
800
+ /**
801
+ * Bring every stored library back: each PDF is imported as-is (its title,
802
+ * registry, and PieceInfo id come from the file — the store's key is only a
803
+ * hint). Call once at boot, before seeding defaults, so "already has
804
+ * libraries" means the user's own.
805
+ */
806
+ async function restoreStampLibraries(stamp, store) {
807
+ const restored = [];
808
+ for (const { id, bytes } of await store.list()) try {
809
+ restored.push(await stamp.importLibraryPdf(bytes));
810
+ } catch (error) {
811
+ globalThis.console?.warn(`[stamp] stored library '${id}' could not be restored:`, error);
812
+ }
813
+ return restored;
814
+ }
815
+ /**
816
+ * Keep the store in sync from now on: every canonical change writes the
817
+ * library's PDF (coalesced per library so a burst of edits saves once),
818
+ * a removal deletes it. `except` names libraries never to persist — the
819
+ * bundled default set, typically. Returns the unsubscribe.
820
+ */
821
+ function persistStampLibraries(stamp, store, opts = {}) {
822
+ const except = new Set(opts.except ?? []);
823
+ const timers = /* @__PURE__ */ new Map();
824
+ const write = (libraryId) => {
825
+ timers.delete(libraryId);
826
+ const bytes = stamp.exportLibrary(libraryId);
827
+ if (!bytes) return;
828
+ store.put(libraryId, bytes).catch((error) => {
829
+ globalThis.console?.warn(`[stamp] persisting library '${libraryId}' failed:`, error);
830
+ });
831
+ };
832
+ const off = stamp.onLibraryChanged(({ libraryId, reason }) => {
833
+ if (except.has(libraryId)) return;
834
+ const pending = timers.get(libraryId);
835
+ if (pending) clearTimeout(pending);
836
+ if (reason === "removed") {
837
+ timers.delete(libraryId);
838
+ store.delete(libraryId).catch((error) => {
839
+ globalThis.console?.warn(`[stamp] deleting stored library '${libraryId}' failed:`, error);
840
+ });
841
+ return;
842
+ }
843
+ timers.set(libraryId, setTimeout(() => write(libraryId), opts.debounceMs ?? 250));
844
+ });
845
+ return () => {
846
+ off();
847
+ for (const [libraryId, timer] of timers) {
848
+ clearTimeout(timer);
849
+ write(libraryId);
850
+ }
851
+ };
852
+ }
853
+ //#endregion
854
+ exports.STAMP_LIBRARY_PIECEINFO_APP = require_contract.STAMP_LIBRARY_PIECEINFO_APP;
855
+ exports.STAMP_PIECEINFO_APP = require_contract.STAMP_PIECEINFO_APP;
856
+ exports.STAMP_PIECEINFO_VERSION = require_contract.STAMP_PIECEINFO_VERSION;
582
857
  exports.StampToken = require_contract.StampToken;
858
+ exports.assetIdFor = require_contract.assetIdFor;
859
+ exports.customStampName = require_contract.customStampName;
860
+ exports.memoryStampStore = memoryStampStore;
861
+ exports.parseStampKey = require_contract.parseStampKey;
862
+ exports.persistStampLibraries = persistStampLibraries;
863
+ exports.restoreStampLibraries = restoreStampLibraries;
864
+ exports.stampKey = require_contract.stampKey;
865
+ exports.stampKindToPdfName = require_contract.stampKindToPdfName;
866
+ exports.stampLibraryPieceInfo = require_contract.stampLibraryPieceInfo;
867
+ exports.stampPieceInfo = require_contract.stampPieceInfo;
583
868
  exports.stampPlugin = stampPlugin;
584
869
 
585
870
  //# sourceMappingURL=index.cjs.map