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