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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,13 +1,43 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_contract = require("./contract-C6CgKD69.cjs");
2
3
  let _embedpdf_core = require("@embedpdf/core");
3
4
  let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
4
- let _embedpdf_plugin_annotation = require("@embedpdf/plugin-annotation");
5
- let _embedpdf_plugin_form = require("@embedpdf/plugin-form");
5
+ let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
6
+ let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
7
+ let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
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
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;
11
41
  /** Session-unique ids. Assets are session-scoped for now (no persistence),
12
42
  * so a timestamp + counter is enough — durable ids come with the store port. */
13
43
  let seq = 0;
@@ -24,7 +54,6 @@ const entryStringArray = (entries, key) => {
24
54
  const entry = entries[key];
25
55
  return entry?.type === "string-array" ? [...entry.value] : void 0;
26
56
  };
27
- const kindToPdfName = (kind) => kind === "signature" ? "Signature" : kind === "initials" ? "Initials" : "Stamp";
28
57
  const kindFromPdfName = (name) => {
29
58
  switch (name?.toLowerCase()) {
30
59
  case "stamp": return "stamp";
@@ -33,21 +62,6 @@ const kindFromPdfName = (name) => {
33
62
  default: return;
34
63
  }
35
64
  };
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
65
  function createStampCapability(ctx, config = {}) {
52
66
  /** Binary sidecar of the serializable store: asset bytes + cached preview,
53
67
  * keyed by asset id. The reducer never sees these (kernel rule 1). */
@@ -74,16 +88,13 @@ function createStampCapability(ctx, config = {}) {
74
88
  }
75
89
  return assetEngineRef;
76
90
  };
77
- const openAssetDocument = async (bytes, identity) => {
91
+ const openAssetDocument = async (bytes) => {
78
92
  try {
79
93
  return await (await assetEngine()).open({
80
94
  kind: "bytes",
81
95
  id: uid("stamp-import"),
82
96
  bytes
83
- }, {
84
- scope: ["*"],
85
- ...identity ? { identity } : {}
86
- });
97
+ }, { scope: ["*"] });
87
98
  } catch (err) {
88
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.");
89
100
  throw err;
@@ -106,94 +117,151 @@ function createStampCapability(ctx, config = {}) {
106
117
  taken.add(id);
107
118
  return id;
108
119
  };
109
- const createLooseLibrary = (name) => {
110
- 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);
111
145
  ctx.dispatch({
112
146
  type: "LIBRARY_ADDED",
113
147
  library: {
114
148
  id,
115
149
  name,
116
- storage: "loose",
117
- assetIds: []
150
+ assetIds: [],
151
+ ...opts?.categories ? { categories: opts.categories } : {}
118
152
  }
119
153
  });
154
+ libraryChanged.emit({
155
+ libraryId: id,
156
+ reason: "created"
157
+ });
120
158
  return id;
121
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
+ };
122
164
  const importLibraryPdf = async (source, opts) => {
123
165
  const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(source);
124
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)");
125
167
  const doc = await openAssetDocument(new Uint8Array(resolved.bytes));
126
168
  let imported;
127
169
  try {
128
- 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");
129
- const snapshot = await doc.pages.list();
130
- 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");
131
- const pages = snapshot.pages.map((layout) => {
132
- const handle = doc.page(layout.pageObjectNumber);
133
- if (!handle.pieceInfo) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.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);
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 ?? {};
142
175
  const state = ctx.getState();
143
176
  const takenLibraryIds = new Set(Object.keys(state.libraries));
144
- const takenAssetIds = new Set(Object.keys(state.assets));
145
- const catalogEntries = catalogMetadata?.entries ?? {};
146
177
  const libraryId = allocateId(entryString(catalogEntries, "Id"), "stamp-lib", takenLibraryIds);
147
- 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 });
148
181
  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));
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
+ }));
172
226
  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
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
181
244
  },
182
- background: "transparent",
183
- includeAnnotations: true,
184
- format: "png"
185
- });
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]);
186
254
  assets.push({
187
255
  asset,
188
256
  bytes,
189
- preview: imageToPreview(image)
257
+ preview: await renderThumbnail(handle)
190
258
  });
191
259
  }
192
260
  imported = {
193
261
  library: {
194
262
  id: libraryId,
195
263
  name: libraryName,
196
- storage: "canonical-pdf",
264
+ ...libraryLocale ? { locale: libraryLocale } : {},
197
265
  categories: libraryCategories,
198
266
  assetIds: []
199
267
  },
@@ -218,15 +286,26 @@ function createStampCapability(ctx, config = {}) {
218
286
  asset
219
287
  });
220
288
  }
289
+ libraryChanged.emit({
290
+ libraryId: imported.library.id,
291
+ reason: "imported"
292
+ });
221
293
  return imported.library.id;
222
294
  };
223
295
  const addAsset = async (input) => {
224
- const targetLibrary = input.libraryId ? ctx.getState().libraries[input.libraryId] ?? null : null;
225
- 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}'`);
226
297
  const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source);
227
298
  const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes);
228
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");
229
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)");
230
309
  let suppliedPreview = null;
231
310
  if (input.preview) {
232
311
  const preview = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.preview);
@@ -235,111 +314,106 @@ function createStampCapability(ctx, config = {}) {
235
314
  mimeType: preview.mimeType ?? "image/png"
236
315
  };
237
316
  }
238
- if (targetLibrary?.storage === "canonical-pdf") {
239
- 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");
240
- return mutateLibrary(targetLibrary.id, async () => {
241
- const liveLibrary = ctx.getState().libraries[targetLibrary.id];
242
- 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`);
243
- const canonicalBytes = libraryBinaries.get(liveLibrary.id);
244
- if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.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 {
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) {
249
331
  const result = await doc.pages.insert(new Uint8Array(resolved.bytes));
250
- 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");
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 _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.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
263
- },
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
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
276
344
  },
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();
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");
289
349
  }
290
- libraryBinaries.set(liveLibrary.id, appended.canonical);
291
- binaries.set(appended.asset.id, {
292
- bytes: appended.bytes,
293
- preview: appended.preview
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
294
356
  });
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 _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");
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
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
326
388
  });
327
389
  ctx.dispatch({
328
390
  type: "ASSET_ADDED",
329
- asset
391
+ asset: appended.asset
330
392
  });
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 _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] loose library '${targetLibrary.id}' no longer exists`);
337
- 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
338
409
  });
339
410
  };
340
411
  const dropLibrary = (id) => {
341
412
  const library = ctx.getState().libraries[id];
342
- 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
+ }
343
417
  libraryBinaries.delete(id);
344
418
  ctx.dispatch({
345
419
  type: "LIBRARY_REMOVED",
@@ -353,71 +427,128 @@ function createStampCapability(ctx, config = {}) {
353
427
  const asset = ctx.getState().assets[id];
354
428
  if (!asset) return;
355
429
  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 _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.Unknown, `[stamp] canonical asset '${id}' has no page object number`);
362
- const canonicalBytes = libraryBinaries.get(library.id);
363
- if (!canonicalBytes) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.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);
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();
373
440
  }
441
+ libraryBinaries.set(library.id, rewritten);
374
442
  binaries.delete(id);
443
+ ghostRenders.delete(id);
375
444
  ctx.dispatch({
376
445
  type: "ASSET_REMOVED",
377
446
  assetId: id
378
447
  });
448
+ libraryChanged.emit({
449
+ libraryId: library.id,
450
+ reason: "asset-removed"
451
+ });
379
452
  });
380
453
  };
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
- }
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
+ });
390
505
  };
391
506
  /**
392
507
  * Form-backed stamps are executable templates only until placement. Work
393
508
  * on an isolated copy so neither the canonical library nor its reusable
394
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.
395
515
  */
396
516
  const materializeForPlacement = async (documentId, asset, bin) => {
397
- if (asset.format !== "pdf" || !config.scripting?.enabled) return bin;
398
- 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;
399
521
  const targetMeta = ctx.core().documents[documentId] ?? null;
400
- 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`);
401
523
  const canonicalBytes = libraryBinaries.get(asset.libraryId);
402
524
  const sourceBytes = canonicalBytes ?? bin.bytes;
403
- const doc = await openAssetDocument(new Uint8Array(sourceBytes), targetDoc.security.identity);
525
+ const doc = await openAssetDocument(new Uint8Array(sourceBytes));
526
+ let realm = null;
404
527
  let scripting = null;
405
528
  try {
406
529
  const layout = await doc.pages.list();
407
530
  const selectedPage = asset.pageObjectNumber === void 0 ? layout.pageCount === 1 ? layout.pages[0] : void 0 : layout.pages.find(({ pageObjectNumber }) => pageObjectNumber === asset.pageObjectNumber);
408
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");
409
- const snapshot = await doc.forms.list();
410
- if (!snapshot.fields.some((field) => field.widgets.some(({ pageObjectNumber }) => pageObjectNumber === selectedPage.pageObjectNumber))) return bin;
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");
412
- const scriptingConfig = config.scripting;
413
- scripting = (0, _embedpdf_plugin_form.createFormScriptingController)({
534
+ realm = mintRealm.call(actions, {
414
535
  doc,
415
536
  document: () => targetMeta,
416
- config: scriptingConfig,
417
- sandboxFactory: scriptingConfig.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox()))
537
+ bootSources: async () => {
538
+ return (doc.actions ? await doc.actions.read() : null)?.nameTreeScripts.map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action)) ?? [];
539
+ }
540
+ });
541
+ scripting = (0, _embedpdf_plugin_form_scripting.createFormScriptingController)({
542
+ doc,
543
+ document: () => targetMeta,
544
+ transaction: realm.transaction.bind(realm),
545
+ budget: realm.budget
546
+ });
547
+ const result = await scripting.recalculate();
548
+ actions.surfaceScriptCommit(result, {
549
+ origin: "user",
550
+ realm: "detached"
418
551
  });
419
- const result = await scripting.recalculate(snapshot);
420
- surfaceScriptingResult(result);
421
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"}`);
422
553
  const pageObjectNumber = selectedPage.pageObjectNumber;
423
554
  const failed = (await doc.pages.flatten([pageObjectNumber], "display")).results.find(({ status }) => status === "failed" || status === "skipped");
@@ -438,6 +569,7 @@ function createStampCapability(ctx, config = {}) {
438
569
  };
439
570
  } finally {
440
571
  scripting?.dispose();
572
+ realm?.dispose();
441
573
  await doc.close();
442
574
  }
443
575
  };
@@ -446,18 +578,73 @@ function createStampCapability(ctx, config = {}) {
446
578
  const bin = binaries.get(assetId);
447
579
  if (!asset || !bin) throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotFound, `[stamp] unknown asset '${assetId}'`);
448
580
  const placement = await materializeForPlacement(documentId, asset, bin);
449
- await ctx.forDocument(_embedpdf_plugin_annotation.AnnotationToken, documentId).armStamp({
450
- source: placement.bytes,
451
- preview: placement.preview ? {
452
- data: placement.preview.bytes,
453
- mimeType: placement.preview.mimeType
454
- } : void 0,
455
- intrinsicSize: asset.size,
581
+ await ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).armStamp({
582
+ ...stampPayload(asset, placement, placement !== bin),
456
583
  targetWidth: opts?.targetWidth
457
584
  });
458
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
+ };
459
645
  ctx.cleanup(() => {
460
646
  binaries.clear();
647
+ ghostRenders.clear();
461
648
  libraryBinaries.clear();
462
649
  libraryMutationTails.clear();
463
650
  const ownedAssetEngine = typeof config.assetEngine === "function" ? assetEngineRef : null;
@@ -481,13 +668,18 @@ function createStampCapability(ctx, config = {}) {
481
668
  asset: (id) => ctx.getState().assets[id] ?? null,
482
669
  assetPreview: (id) => binaries.get(id)?.preview ?? null,
483
670
  assetBytes: (id) => binaries.get(id)?.bytes ?? null,
484
- libraryBytes: (id) => libraryBinaries.get(id) ?? null,
671
+ exportLibrary: (id) => libraryBinaries.get(id) ?? null,
672
+ onLibraryChanged: libraryChanged.on,
673
+ createLibrary,
485
674
  importLibraryPdf,
486
675
  addAsset,
676
+ addAssetFromAnnotations,
677
+ updateAsset,
487
678
  removeAsset,
488
679
  removeLibrary,
489
680
  armAsset,
490
- disarm: (documentId) => ctx.forDocument(_embedpdf_plugin_annotation.AnnotationToken, documentId).disarmStamp()
681
+ placeAsset,
682
+ disarm: (documentId) => ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).disarmStamp()
491
683
  };
492
684
  }
493
685
  //#endregion
@@ -541,6 +733,13 @@ const stampReducer = (state, action) => {
541
733
  }
542
734
  };
543
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;
544
743
  case "ASSET_REMOVED": {
545
744
  const asset = state.assets[action.assetId];
546
745
  if (!asset) return state;
@@ -563,9 +762,6 @@ const stampReducer = (state, action) => {
563
762
  }
564
763
  };
565
764
  //#endregion
566
- //#region src/types.ts
567
- const StampToken = (0, _embedpdf_core.createCapabilityToken)("stamp");
568
- //#endregion
569
765
  //#region src/stamp.plugin.ts
570
766
  /**
571
767
  * The stamp plugin. WORKSPACE-scoped: libraries are shared across every open
@@ -577,14 +773,98 @@ const StampToken = (0, _embedpdf_core.createCapabilityToken)("stamp");
577
773
  */
578
774
  const stampPlugin = (config = {}) => (0, _embedpdf_core.definePlugin)({
579
775
  id: "stamp",
580
- token: StampToken,
776
+ token: require_contract.StampToken,
581
777
  scope: "workspace",
582
778
  initialState: initialStampState,
583
779
  reduce: stampReducer,
584
780
  capability: (ctx) => createStampCapability(ctx, config)
585
781
  });
586
782
  //#endregion
587
- exports.StampToken = StampToken;
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;
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;
588
868
  exports.stampPlugin = stampPlugin;
589
869
 
590
870
  //# sourceMappingURL=index.cjs.map