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