@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/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 +13 -0
- package/dist/contract.d.cts +2 -0
- package/dist/contract.d.ts +2 -0
- package/dist/contract.js +2 -0
- package/dist/index.cjs +511 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -175
- package/dist/index.d.ts +2 -175
- package/dist/index.js +495 -228
- package/dist/index.js.map +1 -1
- package/package.json +15 -8
package/dist/index.js
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
3
|
import { EngineError, EngineErrorCode, resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { javaScriptProgramFromActionTree } from "@embedpdf/core-acrojs";
|
|
5
|
+
import { ActionsToken } from "@embedpdf/plugin-actions/contract/host";
|
|
6
|
+
import { AnnotationToken } from "@embedpdf/plugin-annotation/contract";
|
|
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
|
|
5
38
|
//#region src/capability.ts
|
|
6
39
|
const DEFAULT_PREVIEW_WIDTH = 256;
|
|
7
|
-
const LIBRARY_PIECEINFO_APP = "EMBD_StampLibrary";
|
|
8
|
-
const STAMP_PIECEINFO_APP = "EMBD_Stamp";
|
|
9
|
-
const STAMP_SCHEMA_VERSION = 1;
|
|
10
40
|
/** Session-unique ids. Assets are session-scoped for now (no persistence),
|
|
11
41
|
* so a timestamp + counter is enough — durable ids come with the store port. */
|
|
12
42
|
let seq = 0;
|
|
@@ -23,7 +53,6 @@ const entryStringArray = (entries, key) => {
|
|
|
23
53
|
const entry = entries[key];
|
|
24
54
|
return entry?.type === "string-array" ? [...entry.value] : void 0;
|
|
25
55
|
};
|
|
26
|
-
const kindToPdfName = (kind) => kind === "signature" ? "Signature" : kind === "initials" ? "Initials" : "Stamp";
|
|
27
56
|
const kindFromPdfName = (name) => {
|
|
28
57
|
switch (name?.toLowerCase()) {
|
|
29
58
|
case "stamp": return "stamp";
|
|
@@ -32,21 +61,6 @@ const kindFromPdfName = (name) => {
|
|
|
32
61
|
default: return;
|
|
33
62
|
}
|
|
34
63
|
};
|
|
35
|
-
const metadataPatch = (id, name, kind, subject, categories) => ({
|
|
36
|
-
Version: STAMP_SCHEMA_VERSION,
|
|
37
|
-
Id: id,
|
|
38
|
-
Name: name,
|
|
39
|
-
Kind: { name: kindToPdfName(kind) },
|
|
40
|
-
Subject: subject ?? null,
|
|
41
|
-
Categories: categories ?? null
|
|
42
|
-
});
|
|
43
|
-
const libraryMetadataPatch = (id, name, categories) => ({
|
|
44
|
-
Version: STAMP_SCHEMA_VERSION,
|
|
45
|
-
Id: id,
|
|
46
|
-
Name: name,
|
|
47
|
-
Kind: { name: "StampLibrary" },
|
|
48
|
-
Categories: categories ?? null
|
|
49
|
-
});
|
|
50
64
|
function createStampCapability(ctx, config = {}) {
|
|
51
65
|
/** Binary sidecar of the serializable store: asset bytes + cached preview,
|
|
52
66
|
* keyed by asset id. The reducer never sees these (kernel rule 1). */
|
|
@@ -73,16 +87,13 @@ function createStampCapability(ctx, config = {}) {
|
|
|
73
87
|
}
|
|
74
88
|
return assetEngineRef;
|
|
75
89
|
};
|
|
76
|
-
const openAssetDocument = async (bytes
|
|
90
|
+
const openAssetDocument = async (bytes) => {
|
|
77
91
|
try {
|
|
78
92
|
return await (await assetEngine()).open({
|
|
79
93
|
kind: "bytes",
|
|
80
94
|
id: uid("stamp-import"),
|
|
81
95
|
bytes
|
|
82
|
-
}, {
|
|
83
|
-
scope: ["*"],
|
|
84
|
-
...identity ? { identity } : {}
|
|
85
|
-
});
|
|
96
|
+
}, { scope: ["*"] });
|
|
86
97
|
} catch (err) {
|
|
87
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.");
|
|
88
99
|
throw err;
|
|
@@ -105,94 +116,151 @@ function createStampCapability(ctx, config = {}) {
|
|
|
105
116
|
taken.add(id);
|
|
106
117
|
return id;
|
|
107
118
|
};
|
|
108
|
-
const
|
|
109
|
-
|
|
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);
|
|
110
144
|
ctx.dispatch({
|
|
111
145
|
type: "LIBRARY_ADDED",
|
|
112
146
|
library: {
|
|
113
147
|
id,
|
|
114
148
|
name,
|
|
115
|
-
|
|
116
|
-
|
|
149
|
+
assetIds: [],
|
|
150
|
+
...opts?.categories ? { categories: opts.categories } : {}
|
|
117
151
|
}
|
|
118
152
|
});
|
|
153
|
+
libraryChanged.emit({
|
|
154
|
+
libraryId: id,
|
|
155
|
+
reason: "created"
|
|
156
|
+
});
|
|
119
157
|
return id;
|
|
120
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
|
+
};
|
|
121
163
|
const importLibraryPdf = async (source, opts) => {
|
|
122
164
|
const resolved = await resolveBinarySource(source);
|
|
123
165
|
if (sniffBinaryMetadata(resolved.bytes)?.mimeType !== "application/pdf") throw new EngineError(EngineErrorCode.InvalidArg, "[stamp] importLibraryPdf needs PDF bytes (use addAsset for a raster image)");
|
|
124
166
|
const doc = await openAssetDocument(new Uint8Array(resolved.bytes));
|
|
125
167
|
let imported;
|
|
126
168
|
try {
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
if (!handle.pieceInfo) throw new EngineError(EngineErrorCode.NotImplemented, "[stamp] canonical PDF libraries need page pieceInfo support");
|
|
133
|
-
return {
|
|
134
|
-
layout,
|
|
135
|
-
handle,
|
|
136
|
-
metadata: null
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
const catalogMetadata = await doc.pieceInfo.read(LIBRARY_PIECEINFO_APP);
|
|
140
|
-
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 ?? {};
|
|
141
174
|
const state = ctx.getState();
|
|
142
175
|
const takenLibraryIds = new Set(Object.keys(state.libraries));
|
|
143
|
-
const takenAssetIds = new Set(Object.keys(state.assets));
|
|
144
|
-
const catalogEntries = catalogMetadata?.entries ?? {};
|
|
145
176
|
const libraryId = allocateId(entryString(catalogEntries, "Id"), "stamp-lib", takenLibraryIds);
|
|
146
|
-
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 });
|
|
147
180
|
const libraryCategories = opts?.categories ?? entryStringArray(catalogEntries, "Categories");
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
|
|
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
|
+
}));
|
|
171
225
|
const assets = [];
|
|
172
|
-
for (const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
180
243
|
},
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
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]);
|
|
185
253
|
assets.push({
|
|
186
254
|
asset,
|
|
187
255
|
bytes,
|
|
188
|
-
preview:
|
|
256
|
+
preview: await renderThumbnail(handle)
|
|
189
257
|
});
|
|
190
258
|
}
|
|
191
259
|
imported = {
|
|
192
260
|
library: {
|
|
193
261
|
id: libraryId,
|
|
194
262
|
name: libraryName,
|
|
195
|
-
|
|
263
|
+
...libraryLocale ? { locale: libraryLocale } : {},
|
|
196
264
|
categories: libraryCategories,
|
|
197
265
|
assetIds: []
|
|
198
266
|
},
|
|
@@ -217,15 +285,26 @@ function createStampCapability(ctx, config = {}) {
|
|
|
217
285
|
asset
|
|
218
286
|
});
|
|
219
287
|
}
|
|
288
|
+
libraryChanged.emit({
|
|
289
|
+
libraryId: imported.library.id,
|
|
290
|
+
reason: "imported"
|
|
291
|
+
});
|
|
220
292
|
return imported.library.id;
|
|
221
293
|
};
|
|
222
294
|
const addAsset = async (input) => {
|
|
223
|
-
|
|
224
|
-
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}'`);
|
|
225
296
|
const resolved = await resolveBinarySource(input.source);
|
|
226
297
|
const meta = sniffBinaryMetadata(resolved.bytes);
|
|
227
298
|
if (!meta) throw new EngineError(EngineErrorCode.InvalidArg, "[stamp] asset source must be PNG, JPEG, or single-page PDF bytes");
|
|
228
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)");
|
|
229
308
|
let suppliedPreview = null;
|
|
230
309
|
if (input.preview) {
|
|
231
310
|
const preview = await resolveBinarySource(input.preview);
|
|
@@ -234,111 +313,106 @@ function createStampCapability(ctx, config = {}) {
|
|
|
234
313
|
mimeType: preview.mimeType ?? "image/png"
|
|
235
314
|
};
|
|
236
315
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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) {
|
|
248
330
|
const result = await doc.pages.insert(new Uint8Array(resolved.bytes));
|
|
249
|
-
if (result.insertedPageObjectNumbers.length !== 1) throw new EngineError(EngineErrorCode.InvalidArg, "[stamp] a
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
height: layout.size.height
|
|
262
|
-
},
|
|
263
|
-
format: "pdf",
|
|
264
|
-
pageObjectNumber,
|
|
265
|
-
subject: input.subject,
|
|
266
|
-
categories: input.categories
|
|
267
|
-
};
|
|
268
|
-
await page.pieceInfo.update(STAMP_PIECEINFO_APP, metadataPatch(asset.id, asset.name, asset.kind, asset.subject, asset.categories));
|
|
269
|
-
const bytes = await doc.pages.extract([pageObjectNumber]);
|
|
270
|
-
let preview = suppliedPreview;
|
|
271
|
-
if (!preview) preview = imageToPreview(await page.render.image({
|
|
272
|
-
viewport: {
|
|
273
|
-
kind: "width",
|
|
274
|
-
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
|
|
275
343
|
},
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}));
|
|
280
|
-
appended = {
|
|
281
|
-
asset,
|
|
282
|
-
bytes,
|
|
283
|
-
preview,
|
|
284
|
-
canonical: await doc.download()
|
|
285
|
-
};
|
|
286
|
-
} finally {
|
|
287
|
-
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");
|
|
288
348
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
293
355
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
binaries.set(asset.id, {
|
|
323
|
-
bytes:
|
|
324
|
-
preview
|
|
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
|
|
325
387
|
});
|
|
326
388
|
ctx.dispatch({
|
|
327
389
|
type: "ASSET_ADDED",
|
|
328
|
-
asset
|
|
390
|
+
asset: appended.asset
|
|
329
391
|
});
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
337
408
|
});
|
|
338
409
|
};
|
|
339
410
|
const dropLibrary = (id) => {
|
|
340
411
|
const library = ctx.getState().libraries[id];
|
|
341
|
-
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
|
+
}
|
|
342
416
|
libraryBinaries.delete(id);
|
|
343
417
|
ctx.dispatch({
|
|
344
418
|
type: "LIBRARY_REMOVED",
|
|
@@ -352,71 +426,128 @@ function createStampCapability(ctx, config = {}) {
|
|
|
352
426
|
const asset = ctx.getState().assets[id];
|
|
353
427
|
if (!asset) return;
|
|
354
428
|
const library = ctx.getState().libraries[asset.libraryId];
|
|
355
|
-
if (library
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
await doc.pages.delete([asset.pageObjectNumber]);
|
|
367
|
-
rewritten = await doc.download();
|
|
368
|
-
} finally {
|
|
369
|
-
await doc.close();
|
|
370
|
-
}
|
|
371
|
-
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();
|
|
372
439
|
}
|
|
440
|
+
libraryBinaries.set(library.id, rewritten);
|
|
373
441
|
binaries.delete(id);
|
|
442
|
+
ghostRenders.delete(id);
|
|
374
443
|
ctx.dispatch({
|
|
375
444
|
type: "ASSET_REMOVED",
|
|
376
445
|
assetId: id
|
|
377
446
|
});
|
|
447
|
+
libraryChanged.emit({
|
|
448
|
+
libraryId: library.id,
|
|
449
|
+
reason: "asset-removed"
|
|
450
|
+
});
|
|
378
451
|
});
|
|
379
452
|
};
|
|
380
|
-
const removeLibrary = (id) => mutateLibrary(id, async () =>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
});
|
|
389
504
|
};
|
|
390
505
|
/**
|
|
391
506
|
* Form-backed stamps are executable templates only until placement. Work
|
|
392
507
|
* on an isolated copy so neither the canonical library nor its reusable
|
|
393
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.
|
|
394
514
|
*/
|
|
395
515
|
const materializeForPlacement = async (documentId, asset, bin) => {
|
|
396
|
-
if (
|
|
397
|
-
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;
|
|
398
520
|
const targetMeta = ctx.core().documents[documentId] ?? null;
|
|
399
|
-
if (!
|
|
521
|
+
if (!targetMeta) throw new EngineError(EngineErrorCode.NotFound, `[stamp] target document '${documentId}' is not open`);
|
|
400
522
|
const canonicalBytes = libraryBinaries.get(asset.libraryId);
|
|
401
523
|
const sourceBytes = canonicalBytes ?? bin.bytes;
|
|
402
|
-
const doc = await openAssetDocument(new Uint8Array(sourceBytes)
|
|
524
|
+
const doc = await openAssetDocument(new Uint8Array(sourceBytes));
|
|
525
|
+
let realm = null;
|
|
403
526
|
let scripting = null;
|
|
404
527
|
try {
|
|
405
528
|
const layout = await doc.pages.list();
|
|
406
529
|
const selectedPage = asset.pageObjectNumber === void 0 ? layout.pageCount === 1 ? layout.pages[0] : void 0 : layout.pages.find(({ pageObjectNumber }) => pageObjectNumber === asset.pageObjectNumber);
|
|
407
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");
|
|
408
|
-
|
|
409
|
-
if (!snapshot.fields.some((field) => field.widgets.some(({ pageObjectNumber }) => pageObjectNumber === selectedPage.pageObjectNumber))) return bin;
|
|
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");
|
|
411
|
-
|
|
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
|
+
});
|
|
412
540
|
scripting = createFormScriptingController({
|
|
413
541
|
doc,
|
|
414
542
|
document: () => targetMeta,
|
|
415
|
-
|
|
416
|
-
|
|
543
|
+
transaction: realm.transaction.bind(realm),
|
|
544
|
+
budget: realm.budget
|
|
545
|
+
});
|
|
546
|
+
const result = await scripting.recalculate();
|
|
547
|
+
actions.surfaceScriptCommit(result, {
|
|
548
|
+
origin: "user",
|
|
549
|
+
realm: "detached"
|
|
417
550
|
});
|
|
418
|
-
const result = await scripting.recalculate(snapshot);
|
|
419
|
-
surfaceScriptingResult(result);
|
|
420
551
|
if (result.status === "failed") throw new EngineError(EngineErrorCode.Unknown, `[stamp] dynamic stamp scripting failed: ${result.error?.message ?? "native form effect failed"}`);
|
|
421
552
|
const pageObjectNumber = selectedPage.pageObjectNumber;
|
|
422
553
|
const failed = (await doc.pages.flatten([pageObjectNumber], "display")).results.find(({ status }) => status === "failed" || status === "skipped");
|
|
@@ -437,6 +568,7 @@ function createStampCapability(ctx, config = {}) {
|
|
|
437
568
|
};
|
|
438
569
|
} finally {
|
|
439
570
|
scripting?.dispose();
|
|
571
|
+
realm?.dispose();
|
|
440
572
|
await doc.close();
|
|
441
573
|
}
|
|
442
574
|
};
|
|
@@ -446,17 +578,72 @@ function createStampCapability(ctx, config = {}) {
|
|
|
446
578
|
if (!asset || !bin) throw new EngineError(EngineErrorCode.NotFound, `[stamp] unknown asset '${assetId}'`);
|
|
447
579
|
const placement = await materializeForPlacement(documentId, asset, bin);
|
|
448
580
|
await ctx.forDocument(AnnotationToken, documentId).armStamp({
|
|
449
|
-
|
|
450
|
-
preview: placement.preview ? {
|
|
451
|
-
data: placement.preview.bytes,
|
|
452
|
-
mimeType: placement.preview.mimeType
|
|
453
|
-
} : void 0,
|
|
454
|
-
intrinsicSize: asset.size,
|
|
581
|
+
...stampPayload(asset, placement, placement !== bin),
|
|
455
582
|
targetWidth: opts?.targetWidth
|
|
456
583
|
});
|
|
457
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
|
+
};
|
|
458
644
|
ctx.cleanup(() => {
|
|
459
645
|
binaries.clear();
|
|
646
|
+
ghostRenders.clear();
|
|
460
647
|
libraryBinaries.clear();
|
|
461
648
|
libraryMutationTails.clear();
|
|
462
649
|
const ownedAssetEngine = typeof config.assetEngine === "function" ? assetEngineRef : null;
|
|
@@ -480,12 +667,17 @@ function createStampCapability(ctx, config = {}) {
|
|
|
480
667
|
asset: (id) => ctx.getState().assets[id] ?? null,
|
|
481
668
|
assetPreview: (id) => binaries.get(id)?.preview ?? null,
|
|
482
669
|
assetBytes: (id) => binaries.get(id)?.bytes ?? null,
|
|
483
|
-
|
|
670
|
+
exportLibrary: (id) => libraryBinaries.get(id) ?? null,
|
|
671
|
+
onLibraryChanged: libraryChanged.on,
|
|
672
|
+
createLibrary,
|
|
484
673
|
importLibraryPdf,
|
|
485
674
|
addAsset,
|
|
675
|
+
addAssetFromAnnotations,
|
|
676
|
+
updateAsset,
|
|
486
677
|
removeAsset,
|
|
487
678
|
removeLibrary,
|
|
488
679
|
armAsset,
|
|
680
|
+
placeAsset,
|
|
489
681
|
disarm: (documentId) => ctx.forDocument(AnnotationToken, documentId).disarmStamp()
|
|
490
682
|
};
|
|
491
683
|
}
|
|
@@ -540,6 +732,13 @@ const stampReducer = (state, action) => {
|
|
|
540
732
|
}
|
|
541
733
|
};
|
|
542
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;
|
|
543
742
|
case "ASSET_REMOVED": {
|
|
544
743
|
const asset = state.assets[action.assetId];
|
|
545
744
|
if (!asset) return state;
|
|
@@ -562,9 +761,6 @@ const stampReducer = (state, action) => {
|
|
|
562
761
|
}
|
|
563
762
|
};
|
|
564
763
|
//#endregion
|
|
565
|
-
//#region src/types.ts
|
|
566
|
-
const StampToken = createCapabilityToken("stamp");
|
|
567
|
-
//#endregion
|
|
568
764
|
//#region src/stamp.plugin.ts
|
|
569
765
|
/**
|
|
570
766
|
* The stamp plugin. WORKSPACE-scoped: libraries are shared across every open
|
|
@@ -583,6 +779,77 @@ const stampPlugin = (config = {}) => definePlugin({
|
|
|
583
779
|
capability: (ctx) => createStampCapability(ctx, config)
|
|
584
780
|
});
|
|
585
781
|
//#endregion
|
|
586
|
-
|
|
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 };
|
|
587
854
|
|
|
588
855
|
//# sourceMappingURL=index.js.map
|