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