@embedpdf/plugin-stamp 3.0.0-next.13 → 3.0.0-next.15
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-CEeBOug9.d.cts +431 -0
- package/dist/{contract-B79Onij2.js → contract-CJBvneyw.js} +23 -16
- package/dist/contract-CJBvneyw.js.map +1 -0
- package/dist/contract-CjGXUUDJ.d.ts +431 -0
- package/dist/{contract-T-v5KaYd.cjs → contract-lDLkRDYV.cjs} +28 -34
- package/dist/contract-lDLkRDYV.cjs.map +1 -0
- package/dist/contract.cjs +2 -13
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.ts +2 -2
- package/dist/contract.js +2 -2
- package/dist/index.cjs +1200 -765
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -9
- package/dist/index.d.ts +21 -9
- package/dist/index.js +1203 -758
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/dist/contract-B79Onij2.js.map +0 -1
- package/dist/contract-BS3m51Au.d.cts +0 -384
- package/dist/contract-T-v5KaYd.cjs.map +0 -1
- package/dist/contract-VuCLK5Sr.d.ts +0 -384
package/dist/index.cjs
CHANGED
|
@@ -1,15 +1,687 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_contract = require("./contract-
|
|
2
|
+
const require_contract = require("./contract-lDLkRDYV.cjs");
|
|
3
3
|
let _embedpdf_core = require("@embedpdf/core");
|
|
4
|
+
let _embedpdf_plugin_actions_contract = require("@embedpdf/plugin-actions/contract");
|
|
5
|
+
let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
|
|
4
6
|
let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
|
|
5
7
|
let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
|
|
6
8
|
let _embedpdf_plugin_actions_contract_host = require("@embedpdf/plugin-actions/contract/host");
|
|
7
|
-
let _embedpdf_plugin_annotation_contract = require("@embedpdf/plugin-annotation/contract");
|
|
8
9
|
let _embedpdf_plugin_annotation_contract_host = require("@embedpdf/plugin-annotation/contract/host");
|
|
9
10
|
let _embedpdf_plugin_form_scripting = require("@embedpdf/plugin-form/scripting");
|
|
11
|
+
//#region src/services/errors.ts
|
|
12
|
+
/** A `PluginError` raised by the stamp plugin. */
|
|
13
|
+
const stampError = (code, message) => new _embedpdf_core.PluginError(code, "stamp", message);
|
|
14
|
+
const notFound = (what, id) => stampError("not-found", `unknown ${what} '${id}'`);
|
|
15
|
+
/**
|
|
16
|
+
* A refusal that names the missing permission. The stamp plugin acts on a
|
|
17
|
+
* document it isn't bound to, so it asks that document's checks instead of
|
|
18
|
+
* `ctx.assertAllowed`, and refuses with the same error.
|
|
19
|
+
*/
|
|
20
|
+
const permissionDenied = (permission, operation) => new _embedpdf_core.PluginError("permission-denied", "stamp", `${operation} requires ${permission}`, { permission });
|
|
21
|
+
/** Stop before the next step, or before anything is kept, once the caller cancelled. */
|
|
22
|
+
const throwIfCancelled = (signal) => {
|
|
23
|
+
if (signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "stamp", "operation cancelled", { cause: signal.reason });
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A capability verb: whatever the asset engine, a sibling plugin or a binary
|
|
27
|
+
* source rejects with reaches the caller as a `PluginError`.
|
|
28
|
+
*/
|
|
29
|
+
const verb = (run) => async (...args) => {
|
|
30
|
+
try {
|
|
31
|
+
return await run(...args);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw (0, _embedpdf_core.toPluginError)("stamp", error);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/read/catalog.ts
|
|
38
|
+
/** Reads over the state and the binaries resource: libraries, assets,
|
|
39
|
+
* previews, the library's PDF, and the checks. */
|
|
40
|
+
function createCatalog(ctx, { binaries, assetEngine, ghosts, targets }) {
|
|
41
|
+
const { binaries: assetBinaries, libraryBinaries } = binaries;
|
|
42
|
+
const { ghostProvider } = ghosts;
|
|
43
|
+
const { documentIdOf } = targets;
|
|
44
|
+
const listAssets = (filter) => {
|
|
45
|
+
const state = ctx.state.get();
|
|
46
|
+
return (filter?.libraryId ? [filter.libraryId] : state.libraryOrder).flatMap((libraryId) => (state.libraries[libraryId]?.assetIds ?? []).map((id) => state.assets[id])).filter((asset) => asset != null && (!filter?.kind || asset.kind === filter.kind) && (!filter?.category || (asset.categories ?? []).includes(filter.category)));
|
|
47
|
+
};
|
|
48
|
+
/** Placing is annotation work: the document's annotation plugin answers. */
|
|
49
|
+
const canPlace = (documentId) => {
|
|
50
|
+
const id = documentIdOf(documentId);
|
|
51
|
+
if (!id) return false;
|
|
52
|
+
return ctx.tryForDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, id)?.canCreate() ?? false;
|
|
53
|
+
};
|
|
54
|
+
/** A stamp made from annotations copies them out of the document, as a download does. */
|
|
55
|
+
const canCreateFromAnnotations = (documentId) => {
|
|
56
|
+
const id = documentIdOf(documentId);
|
|
57
|
+
return id !== null && ctx.get(_embedpdf_core.DocumentsToken).canDownload(id);
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
canPlace,
|
|
61
|
+
canCreateFromAnnotations,
|
|
62
|
+
api: {
|
|
63
|
+
listLibraries: (filter) => {
|
|
64
|
+
const state = ctx.state.get();
|
|
65
|
+
const kinds = filter?.kind === void 0 ? null : new Set(typeof filter.kind === "string" ? [filter.kind] : filter.kind);
|
|
66
|
+
return state.libraryOrder.map((id) => state.libraries[id]).filter((library) => library != null && (kinds === null || kinds.has(library.kind)));
|
|
67
|
+
},
|
|
68
|
+
getLibrary: (libraryId) => ctx.state.get().libraries[libraryId] ?? null,
|
|
69
|
+
listAssets,
|
|
70
|
+
getAsset: (assetId) => ctx.state.get().assets[assetId] ?? null,
|
|
71
|
+
getAssetPreview: (assetId) => assetBinaries.get(assetId)?.preview ?? null,
|
|
72
|
+
renderAssetPreview: verb(async (assetId, { width, signal }) => {
|
|
73
|
+
throwIfCancelled(signal);
|
|
74
|
+
const binary = assetBinaries.get(assetId);
|
|
75
|
+
if (!binary) throw notFound("asset", assetId);
|
|
76
|
+
const preview = await ctx.cancellable(signal, ghostProvider(binary.bytes, binary.preview, assetId)(width));
|
|
77
|
+
return preview ? {
|
|
78
|
+
bytes: preview.bytes,
|
|
79
|
+
mimeType: preview.mimeType ?? "image/png"
|
|
80
|
+
} : null;
|
|
81
|
+
}),
|
|
82
|
+
readAssetBytes: (assetId) => {
|
|
83
|
+
const bytes = assetBinaries.get(assetId)?.bytes;
|
|
84
|
+
return bytes ? new Uint8Array(bytes) : null;
|
|
85
|
+
},
|
|
86
|
+
exportLibrary: async (libraryId, options) => {
|
|
87
|
+
throwIfCancelled(options?.signal);
|
|
88
|
+
const bytes = libraryBinaries.get(libraryId);
|
|
89
|
+
if (!bytes) throw notFound("library", libraryId);
|
|
90
|
+
return new Uint8Array(bytes);
|
|
91
|
+
},
|
|
92
|
+
canPlace,
|
|
93
|
+
canCreateFromAnnotations,
|
|
94
|
+
canImport: assetEngine.canImport
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/services/asset-engine.ts
|
|
100
|
+
/**
|
|
101
|
+
* The asset engine port and the canonical-library services every write
|
|
102
|
+
* needs: opening library bytes as a document on a local engine, import-time
|
|
103
|
+
* previews, and the import-support fact learned from the first import.
|
|
104
|
+
*/
|
|
105
|
+
/** Session-unique ids (scratch documents, library ids a file does not carry):
|
|
106
|
+
* a timestamp plus a counter. */
|
|
107
|
+
let idCounter = 0;
|
|
108
|
+
const uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${(idCounter++).toString(36)}`;
|
|
109
|
+
function createAssetEngine(ctx) {
|
|
110
|
+
const settings = ctx.settings();
|
|
111
|
+
/** Set once an import finds the asset engine is not local (cloud). */
|
|
112
|
+
let importSupported = null;
|
|
113
|
+
let resolved = null;
|
|
114
|
+
/** Engines this plugin made through a configured function: destroyed with it. */
|
|
115
|
+
const owned = [];
|
|
116
|
+
const assetEngine = () => {
|
|
117
|
+
const configured = settings.get().assetEngine;
|
|
118
|
+
if (resolved?.from !== configured) {
|
|
119
|
+
const engine = !configured ? ctx.engine : typeof configured === "function" ? configured() : configured;
|
|
120
|
+
if (typeof configured === "function") owned.push(engine);
|
|
121
|
+
resolved = {
|
|
122
|
+
from: configured,
|
|
123
|
+
engine
|
|
124
|
+
};
|
|
125
|
+
importSupported = null;
|
|
126
|
+
}
|
|
127
|
+
return resolved.engine;
|
|
128
|
+
};
|
|
129
|
+
/** Library PDFs are sliced and tagged (`/PieceInfo`) on a local engine only. */
|
|
130
|
+
const openAssetDocument = async (bytes) => {
|
|
131
|
+
const engine = await assetEngine();
|
|
132
|
+
if (!(0, _embedpdf_engine_core_runtime.isLocalEngine)(engine)) {
|
|
133
|
+
if (importSupported !== false) {
|
|
134
|
+
importSupported = false;
|
|
135
|
+
ctx.notify();
|
|
136
|
+
}
|
|
137
|
+
throw stampError("unsupported", settings.get().assetEngine ? "the stamp plugin's assetEngine must be a local engine (createLocalEngine())" : "importing a library PDF needs a local engine, and this viewer's engine is a cloud engine. Pass stampPlugin({ assetEngine: () => import('@embedpdf/engine').then((module) => module.createLocalEngine()) }): it loads lazily, on first import.");
|
|
138
|
+
}
|
|
139
|
+
return engine.open({
|
|
140
|
+
kind: "bytes",
|
|
141
|
+
id: uid("stamp-import"),
|
|
142
|
+
bytes
|
|
143
|
+
}, { scope: ["*"] });
|
|
144
|
+
};
|
|
145
|
+
/** Import-time preview render to bytes. The asset engine is local by
|
|
146
|
+
* definition, so the image source is always inline bytes. */
|
|
147
|
+
const imageToPreview = (image) => {
|
|
148
|
+
if (image.source.kind !== "bytes") throw stampError("unsupported", "the asset engine returned a URL-sourced render; asset engines must be local");
|
|
149
|
+
return {
|
|
150
|
+
bytes: image.source.bytes,
|
|
151
|
+
mimeType: image.contentType
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
/** Thumbnail render of one library page (the small picker image). */
|
|
155
|
+
const renderThumbnail = async (handle) => imageToPreview(await handle.render.image({
|
|
156
|
+
viewport: {
|
|
157
|
+
kind: "width",
|
|
158
|
+
width: settings.get().previewWidth
|
|
159
|
+
},
|
|
160
|
+
background: "transparent",
|
|
161
|
+
includeAnnotations: true,
|
|
162
|
+
format: "png"
|
|
163
|
+
}));
|
|
164
|
+
ctx.cleanup(() => {
|
|
165
|
+
resolved = null;
|
|
166
|
+
for (const engine of owned.splice(0)) Promise.resolve(engine).then((made) => made.destroy()).catch(() => {});
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
openAssetDocument,
|
|
170
|
+
imageToPreview,
|
|
171
|
+
renderThumbnail,
|
|
172
|
+
/** Optimistic until an import finds the asset engine is not local (cloud). */
|
|
173
|
+
canImport: () => {
|
|
174
|
+
if (resolved && resolved.from !== settings.get().assetEngine) return true;
|
|
175
|
+
return importSupported !== false;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/services/binaries.ts
|
|
181
|
+
function createBinaries(ctx) {
|
|
182
|
+
/** Asset bytes (a one-page placement PDF) and the cached preview, by asset id. */
|
|
183
|
+
const binaries = /* @__PURE__ */ new Map();
|
|
184
|
+
/** The durable source of truth for a canonical library. Asset PDFs above
|
|
185
|
+
* are derived page extractions; this map owns the rewritten whole PDF. */
|
|
186
|
+
const libraryBinaries = /* @__PURE__ */ new Map();
|
|
187
|
+
/** Whole-PDF rewrites are serialized per library, or two concurrent
|
|
188
|
+
* appends could both start from the same bytes and lose one page. A
|
|
189
|
+
* mutation whose signal fired while it waited never starts. */
|
|
190
|
+
const mutateLibrary = (libraryId, mutation, signal) => ctx.serialQueue(`library:${libraryId}`)(mutation, { signal });
|
|
191
|
+
/**
|
|
192
|
+
* Ghost renders, one per size bucket per asset, rendered lazily on the
|
|
193
|
+
* asset engine the first time the ghost is shown at that size and kept
|
|
194
|
+
* until the asset goes away. Materialized (dynamic) placements are
|
|
195
|
+
* user/time specific and cache only for their own arm (`cacheKey` null).
|
|
196
|
+
*/
|
|
197
|
+
const ghostRenders = /* @__PURE__ */ new Map();
|
|
198
|
+
ctx.cleanup(() => {
|
|
199
|
+
binaries.clear();
|
|
200
|
+
ghostRenders.clear();
|
|
201
|
+
libraryBinaries.clear();
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
binaries,
|
|
205
|
+
libraryBinaries,
|
|
206
|
+
ghostRenders,
|
|
207
|
+
mutateLibrary
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/services/documents.ts
|
|
212
|
+
/**
|
|
213
|
+
* The document a placing verb acts on: the one its options name, else the
|
|
214
|
+
* active one (a document's scope fills `documentId` in through the plugin's
|
|
215
|
+
* `inScope`), and the page arguments resolved in it.
|
|
216
|
+
*/
|
|
217
|
+
function createTargets(ctx) {
|
|
218
|
+
const documents = () => ctx.get(_embedpdf_core.DocumentsToken);
|
|
219
|
+
/** The document's id, or null with no document open. */
|
|
220
|
+
const documentIdOf = (documentId) => documentId ?? documents().getActiveId();
|
|
221
|
+
/** The document a verb acts on. Rejects `not-ready` with no document open. */
|
|
222
|
+
const targetOf = (options) => {
|
|
223
|
+
const documentId = documentIdOf(options?.documentId);
|
|
224
|
+
if (!documentId) throw stampError("not-ready", "no document is open");
|
|
225
|
+
return documentId;
|
|
226
|
+
};
|
|
227
|
+
/** A page argument of the document, as a ref or an index. Rejects `not-found`. */
|
|
228
|
+
const pageOf = (documentId, page) => {
|
|
229
|
+
const found = documents().getPage(page, documentId);
|
|
230
|
+
if (!found) throw stampError("not-found", `no page ${typeof page === "number" ? page : page.objectNumber} in document '${documentId}'`);
|
|
231
|
+
return found;
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
documentIdOf,
|
|
235
|
+
targetOf,
|
|
236
|
+
pageOf
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/services/events.ts
|
|
241
|
+
function createEvents(ctx) {
|
|
242
|
+
return {
|
|
243
|
+
libraryChanged: ctx.events.source(),
|
|
244
|
+
libraryCreated: ctx.events.source(),
|
|
245
|
+
libraryUpdated: ctx.events.source(),
|
|
246
|
+
libraryDeleted: ctx.events.source(),
|
|
247
|
+
assetCreated: ctx.events.source(),
|
|
248
|
+
assetUpdated: ctx.events.source(),
|
|
249
|
+
assetDeleted: ctx.events.source(),
|
|
250
|
+
armChanged: ctx.events.source()
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/services/ghosts.ts
|
|
255
|
+
function createGhosts({ openAssetDocument, imageToPreview }, { ghostRenders }) {
|
|
256
|
+
const renderGhost = async (bytes, devicePixelWidth) => {
|
|
257
|
+
const doc = await openAssetDocument(new Uint8Array(bytes));
|
|
258
|
+
try {
|
|
259
|
+
const page = (await doc.pages.list()).pages[0];
|
|
260
|
+
if (!page) return null;
|
|
261
|
+
return imageToPreview(await doc.page(page.ref).render.image({
|
|
262
|
+
viewport: {
|
|
263
|
+
kind: "width",
|
|
264
|
+
width: devicePixelWidth
|
|
265
|
+
},
|
|
266
|
+
background: "transparent",
|
|
267
|
+
includeAnnotations: true,
|
|
268
|
+
format: "png"
|
|
269
|
+
}));
|
|
270
|
+
} finally {
|
|
271
|
+
await doc.close();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
/** A resolution-aware ghost source for placement bytes: the page renders
|
|
275
|
+
* at the requested width, cached per bucket under `cacheKey` when given. */
|
|
276
|
+
const ghostProvider = (bytes, fallback, cacheKey) => {
|
|
277
|
+
const local = cacheKey ? ghostRenders.get(cacheKey) ?? /* @__PURE__ */ new Map() : /* @__PURE__ */ new Map();
|
|
278
|
+
if (cacheKey) ghostRenders.set(cacheKey, local);
|
|
279
|
+
return (devicePixelWidth) => {
|
|
280
|
+
let pending = local.get(devicePixelWidth);
|
|
281
|
+
if (!pending) {
|
|
282
|
+
pending = renderGhost(bytes, devicePixelWidth).catch((error) => {
|
|
283
|
+
globalThis.console?.warn("[stamp] ghost render failed, using the thumbnail:", error);
|
|
284
|
+
return fallback;
|
|
285
|
+
});
|
|
286
|
+
local.set(devicePixelWidth, pending);
|
|
287
|
+
}
|
|
288
|
+
return pending;
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
return { ghostProvider };
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/services/index.ts
|
|
295
|
+
/**
|
|
296
|
+
* Plugin-private services every area is built on (not the kernel): the
|
|
297
|
+
* events, the binaries resource, the asset engine port, the ghost renderer
|
|
298
|
+
* and the document a placing verb acts on.
|
|
299
|
+
*/
|
|
300
|
+
function createServices(ctx) {
|
|
301
|
+
const binaries = createBinaries(ctx);
|
|
302
|
+
const assetEngine = createAssetEngine(ctx);
|
|
303
|
+
return {
|
|
304
|
+
events: createEvents(ctx),
|
|
305
|
+
binaries,
|
|
306
|
+
assetEngine,
|
|
307
|
+
ghosts: createGhosts(assetEngine, binaries),
|
|
308
|
+
targets: createTargets(ctx)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/model.ts
|
|
313
|
+
const initialStampState = () => ({
|
|
314
|
+
libraries: {},
|
|
315
|
+
libraryOrder: [],
|
|
316
|
+
assets: {},
|
|
317
|
+
armed: {}
|
|
318
|
+
});
|
|
319
|
+
/** Remember the asset armed on a document. */
|
|
320
|
+
function armAsset(state, documentId, assetId) {
|
|
321
|
+
if (state.armed[documentId] === assetId) return state;
|
|
322
|
+
return {
|
|
323
|
+
...state,
|
|
324
|
+
armed: {
|
|
325
|
+
...state.armed,
|
|
326
|
+
[documentId]: assetId
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/** Forget what was armed on these documents. */
|
|
331
|
+
function disarmDocuments(state, documentIds) {
|
|
332
|
+
if (!documentIds.some((documentId) => documentId in state.armed)) return state;
|
|
333
|
+
const armed = { ...state.armed };
|
|
334
|
+
for (const documentId of documentIds) delete armed[documentId];
|
|
335
|
+
return {
|
|
336
|
+
...state,
|
|
337
|
+
armed
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** Append a library to the display order. */
|
|
341
|
+
function addLibrary(state, library) {
|
|
342
|
+
return {
|
|
343
|
+
...state,
|
|
344
|
+
libraries: {
|
|
345
|
+
...state.libraries,
|
|
346
|
+
[library.id]: library
|
|
347
|
+
},
|
|
348
|
+
libraryOrder: [...state.libraryOrder, library.id]
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/** Replace a known library's descriptor; an unknown id changes nothing. */
|
|
352
|
+
function setLibrary(state, library) {
|
|
353
|
+
if (!state.libraries[library.id]) return state;
|
|
354
|
+
return {
|
|
355
|
+
...state,
|
|
356
|
+
libraries: {
|
|
357
|
+
...state.libraries,
|
|
358
|
+
[library.id]: library
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/** Remove a library together with its assets. */
|
|
363
|
+
function removeLibrary(state, libraryId) {
|
|
364
|
+
const library = state.libraries[libraryId];
|
|
365
|
+
if (!library) return state;
|
|
366
|
+
const { [libraryId]: _removed, ...libraries } = state.libraries;
|
|
367
|
+
const assets = { ...state.assets };
|
|
368
|
+
for (const id of library.assetIds) delete assets[id];
|
|
369
|
+
return {
|
|
370
|
+
...state,
|
|
371
|
+
libraries,
|
|
372
|
+
libraryOrder: state.libraryOrder.filter((id) => id !== libraryId),
|
|
373
|
+
assets
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/** Add an asset to the end of its library; an asset of an unknown library changes nothing. */
|
|
377
|
+
function addAsset(state, asset) {
|
|
378
|
+
const library = state.libraries[asset.libraryId];
|
|
379
|
+
if (!library) return state;
|
|
380
|
+
return {
|
|
381
|
+
...state,
|
|
382
|
+
assets: {
|
|
383
|
+
...state.assets,
|
|
384
|
+
[asset.id]: asset
|
|
385
|
+
},
|
|
386
|
+
libraries: {
|
|
387
|
+
...state.libraries,
|
|
388
|
+
[library.id]: {
|
|
389
|
+
...library,
|
|
390
|
+
assetIds: [...library.assetIds, asset.id]
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** Replace a known asset's descriptor; an unknown id changes nothing. */
|
|
396
|
+
function setAsset(state, asset) {
|
|
397
|
+
if (!state.assets[asset.id]) return state;
|
|
398
|
+
return {
|
|
399
|
+
...state,
|
|
400
|
+
assets: {
|
|
401
|
+
...state.assets,
|
|
402
|
+
[asset.id]: asset
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/** Remove an asset and its entry in its library's order. */
|
|
407
|
+
function removeAsset(state, assetId) {
|
|
408
|
+
const asset = state.assets[assetId];
|
|
409
|
+
if (!asset) return state;
|
|
410
|
+
const { [assetId]: _removed, ...assets } = state.assets;
|
|
411
|
+
const library = state.libraries[asset.libraryId];
|
|
412
|
+
return {
|
|
413
|
+
...state,
|
|
414
|
+
assets,
|
|
415
|
+
libraries: library ? {
|
|
416
|
+
...state.libraries,
|
|
417
|
+
[library.id]: {
|
|
418
|
+
...library,
|
|
419
|
+
assetIds: library.assetIds.filter((id) => id !== assetId)
|
|
420
|
+
}
|
|
421
|
+
} : state.libraries
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/write/assets.ts
|
|
426
|
+
function createAssetWrites(ctx, { events, binaries, assetEngine, targets }, { resolveAssetSource }, { createLibrary }, { canCreateFromAnnotations }) {
|
|
427
|
+
const { libraryChanged, assetCreated, assetUpdated, assetDeleted } = events;
|
|
428
|
+
const { binaries: assetBinaries, libraryBinaries, ghostRenders, mutateLibrary } = binaries;
|
|
429
|
+
const { openAssetDocument, renderThumbnail } = assetEngine;
|
|
430
|
+
const { targetOf, pageOf } = targets;
|
|
431
|
+
const createAsset = async (input, options) => {
|
|
432
|
+
const signal = options?.signal;
|
|
433
|
+
throwIfCancelled(signal);
|
|
434
|
+
if (input.libraryId && !ctx.state.get().libraries[input.libraryId]) throw notFound("library", input.libraryId);
|
|
435
|
+
const resolved = await ctx.cancellable(signal, resolveAssetSource(input));
|
|
436
|
+
const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes);
|
|
437
|
+
if (!meta) throw stampError("invalid-input", "asset source must be PNG, JPEG, or single-page PDF bytes");
|
|
438
|
+
const isPdf = meta.mimeType === "application/pdf";
|
|
439
|
+
const name = input.name ?? require_contract.customStampName();
|
|
440
|
+
const label = input.label ?? name;
|
|
441
|
+
if (name.length === 0 || name.includes("=")) throw stampError("invalid-input", `invalid stamp identifier '${name}': must be non-empty and contain no '='`);
|
|
442
|
+
const rasterSize = input.size ?? ("width" in meta ? {
|
|
443
|
+
width: meta.width,
|
|
444
|
+
height: meta.height
|
|
445
|
+
} : null);
|
|
446
|
+
if (!isPdf && (!rasterSize || rasterSize.width <= 0 || rasterSize.height <= 0)) throw stampError("invalid-input", "a raster asset needs a positive `size` (its page size in points)");
|
|
447
|
+
let suppliedPreview = null;
|
|
448
|
+
if (input.preview) {
|
|
449
|
+
const preview = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.preview);
|
|
450
|
+
suppliedPreview = {
|
|
451
|
+
bytes: new Uint8Array(preview.bytes),
|
|
452
|
+
mimeType: preview.mimeType ?? "image/png"
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const libraryId = input.libraryId ?? (await createLibrary(label, { signal })).library.id;
|
|
456
|
+
return mutateLibrary(libraryId, async () => {
|
|
457
|
+
const liveLibrary = ctx.state.get().libraries[libraryId];
|
|
458
|
+
if (!liveLibrary) throw stampError("not-found", `library '${libraryId}' no longer exists`);
|
|
459
|
+
const assetId = require_contract.assetIdFor(liveLibrary.id, name);
|
|
460
|
+
if (ctx.state.get().assets[assetId]) throw stampError("invalid-input", `library '${liveLibrary.name}' already has a stamp named '${name}'`);
|
|
461
|
+
const canonicalBytes = libraryBinaries.get(liveLibrary.id);
|
|
462
|
+
if (!canonicalBytes) throw stampError("operation-failed", `canonical bytes are missing for library '${liveLibrary.id}'`);
|
|
463
|
+
const doc = await ctx.cancellable(signal, openAssetDocument(canonicalBytes));
|
|
464
|
+
let appended;
|
|
465
|
+
try {
|
|
466
|
+
let page;
|
|
467
|
+
if (isPdf) {
|
|
468
|
+
const result = await doc.pages.insert(new Uint8Array(resolved.bytes));
|
|
469
|
+
if (result.insertedPages.length !== 1) throw stampError("invalid-input", "a library asset must be a single-page PDF");
|
|
470
|
+
page = result.insertedPages[0];
|
|
471
|
+
} else {
|
|
472
|
+
if (!doc.pages.flatten) throw stampError("unsupported", "raster assets need an asset engine with pages.flatten");
|
|
473
|
+
page = (await doc.pages.insertBlank({ size: rasterSize })).insertedPages[0];
|
|
474
|
+
await doc.page(page).annotations.create({
|
|
475
|
+
subtype: "stamp",
|
|
476
|
+
box: {
|
|
477
|
+
x: 0,
|
|
478
|
+
y: 0,
|
|
479
|
+
width: rasterSize.width,
|
|
480
|
+
height: rasterSize.height
|
|
481
|
+
},
|
|
482
|
+
fit: "fill"
|
|
483
|
+
}, { resources: { appearance: new Uint8Array(resolved.bytes) } });
|
|
484
|
+
if ((await doc.pages.flatten([page], { usage: "display" })).results.some(({ status }) => status !== "applied")) throw stampError("operation-failed", "flattening the raster into its page failed");
|
|
485
|
+
}
|
|
486
|
+
const layout = (await doc.pages.list()).pages.find((candidate) => candidate.ref.objectNumber === page.objectNumber);
|
|
487
|
+
const handle = doc.page(page);
|
|
488
|
+
if (!layout) throw stampError("operation-failed", "the new page is missing from the library layout");
|
|
489
|
+
await doc.pages.setName({
|
|
490
|
+
name: require_contract.stampKey(name, label),
|
|
491
|
+
page
|
|
492
|
+
});
|
|
493
|
+
const asset = {
|
|
494
|
+
id: assetId,
|
|
495
|
+
libraryId: liveLibrary.id,
|
|
496
|
+
kind: input.kind ?? "stamp",
|
|
497
|
+
name,
|
|
498
|
+
label,
|
|
499
|
+
size: {
|
|
500
|
+
width: layout.size.width,
|
|
501
|
+
height: layout.size.height
|
|
502
|
+
},
|
|
503
|
+
page,
|
|
504
|
+
...input.subject !== void 0 ? { subject: input.subject } : {},
|
|
505
|
+
...input.categories !== void 0 ? { categories: input.categories } : {}
|
|
506
|
+
};
|
|
507
|
+
await handle.pieceInfo.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(asset.kind, {
|
|
508
|
+
subject: asset.subject,
|
|
509
|
+
categories: asset.categories
|
|
510
|
+
}));
|
|
511
|
+
appended = {
|
|
512
|
+
asset,
|
|
513
|
+
bytes: await doc.pages.extract([page]),
|
|
514
|
+
preview: suppliedPreview ?? await renderThumbnail(handle),
|
|
515
|
+
canonical: await doc.download()
|
|
516
|
+
};
|
|
517
|
+
} finally {
|
|
518
|
+
await doc.close();
|
|
519
|
+
}
|
|
520
|
+
throwIfCancelled(signal);
|
|
521
|
+
libraryBinaries.set(liveLibrary.id, appended.canonical);
|
|
522
|
+
assetBinaries.set(appended.asset.id, {
|
|
523
|
+
bytes: appended.bytes,
|
|
524
|
+
preview: appended.preview
|
|
525
|
+
});
|
|
526
|
+
ctx.state.update(addAsset, appended.asset);
|
|
527
|
+
libraryChanged.emit({
|
|
528
|
+
libraryId: liveLibrary.id,
|
|
529
|
+
reason: "asset-added"
|
|
530
|
+
});
|
|
531
|
+
assetCreated.emit({
|
|
532
|
+
assetId: appended.asset.id,
|
|
533
|
+
libraryId: liveLibrary.id,
|
|
534
|
+
asset: appended.asset
|
|
535
|
+
});
|
|
536
|
+
return { asset: appended.asset };
|
|
537
|
+
}, signal);
|
|
538
|
+
};
|
|
539
|
+
const createAssetFromAnnotations = async (page, refs, input, options) => {
|
|
540
|
+
const signal = options?.signal;
|
|
541
|
+
throwIfCancelled(signal);
|
|
542
|
+
const documentId = targetOf(options);
|
|
543
|
+
if (!canCreateFromAnnotations(documentId)) throw permissionDenied("doc.download", "stamp.createAssetFromAnnotations");
|
|
544
|
+
const doc = ctx.documentHandle(documentId);
|
|
545
|
+
if (!doc) throw stampError("not-found", `document '${documentId}' is not open`);
|
|
546
|
+
const handle = doc.page(pageOf(documentId, page).ref);
|
|
547
|
+
const source = await ctx.cancellable(signal, handle.annotations.exportAppearance([...refs]));
|
|
548
|
+
return createAsset({
|
|
549
|
+
...input,
|
|
550
|
+
source
|
|
551
|
+
}, { signal });
|
|
552
|
+
};
|
|
553
|
+
const deleteAsset = async (id, options) => {
|
|
554
|
+
const signal = options?.signal;
|
|
555
|
+
throwIfCancelled(signal);
|
|
556
|
+
const initialAsset = ctx.state.get().assets[id];
|
|
557
|
+
if (!initialAsset) return;
|
|
558
|
+
return mutateLibrary(initialAsset.libraryId, async () => {
|
|
559
|
+
const asset = ctx.state.get().assets[id];
|
|
560
|
+
if (!asset) return;
|
|
561
|
+
const library = ctx.state.get().libraries[asset.libraryId];
|
|
562
|
+
if (!library) return;
|
|
563
|
+
const canonicalBytes = libraryBinaries.get(library.id);
|
|
564
|
+
if (!canonicalBytes) throw stampError("operation-failed", `canonical bytes are missing for library '${library.id}'`);
|
|
565
|
+
const doc = await ctx.cancellable(signal, openAssetDocument(canonicalBytes));
|
|
566
|
+
let rewritten;
|
|
567
|
+
try {
|
|
568
|
+
await doc.pages.delete([asset.page]);
|
|
569
|
+
rewritten = await doc.download();
|
|
570
|
+
} finally {
|
|
571
|
+
await doc.close();
|
|
572
|
+
}
|
|
573
|
+
throwIfCancelled(signal);
|
|
574
|
+
libraryBinaries.set(library.id, rewritten);
|
|
575
|
+
assetBinaries.delete(id);
|
|
576
|
+
ghostRenders.delete(id);
|
|
577
|
+
ctx.state.update(removeAsset, id);
|
|
578
|
+
libraryChanged.emit({
|
|
579
|
+
libraryId: library.id,
|
|
580
|
+
reason: "asset-removed"
|
|
581
|
+
});
|
|
582
|
+
assetDeleted.emit({
|
|
583
|
+
assetId: id,
|
|
584
|
+
libraryId: library.id,
|
|
585
|
+
asset: null
|
|
586
|
+
});
|
|
587
|
+
}, signal);
|
|
588
|
+
};
|
|
589
|
+
const updateAsset = async (id, patch, options) => {
|
|
590
|
+
const signal = options?.signal;
|
|
591
|
+
throwIfCancelled(signal);
|
|
592
|
+
const initial = ctx.state.get().assets[id];
|
|
593
|
+
if (!initial) throw notFound("asset", id);
|
|
594
|
+
return mutateLibrary(initial.libraryId, async () => {
|
|
595
|
+
const asset = ctx.state.get().assets[id];
|
|
596
|
+
const library = asset ? ctx.state.get().libraries[asset.libraryId] : void 0;
|
|
597
|
+
if (!asset || !library) throw notFound("asset", id);
|
|
598
|
+
const next = {
|
|
599
|
+
...asset,
|
|
600
|
+
...patch.label !== void 0 ? { label: patch.label } : {},
|
|
601
|
+
...patch.categories !== void 0 ? { categories: patch.categories } : {}
|
|
602
|
+
};
|
|
603
|
+
if (patch.subject === null) delete next.subject;
|
|
604
|
+
else if (patch.subject !== void 0) next.subject = patch.subject;
|
|
605
|
+
const canonicalBytes = libraryBinaries.get(library.id);
|
|
606
|
+
if (!canonicalBytes) throw stampError("operation-failed", `canonical bytes are missing for library '${library.id}'`);
|
|
607
|
+
const doc = await ctx.cancellable(signal, openAssetDocument(canonicalBytes));
|
|
608
|
+
let rewritten;
|
|
609
|
+
try {
|
|
610
|
+
if (next.label !== asset.label) await doc.pages.setName({
|
|
611
|
+
name: require_contract.stampKey(asset.name, next.label),
|
|
612
|
+
page: asset.page,
|
|
613
|
+
replace: require_contract.stampKey(asset.name, asset.label)
|
|
614
|
+
});
|
|
615
|
+
await doc.page(asset.page).pieceInfo.update(require_contract.STAMP_PIECEINFO_APP, require_contract.stampPieceInfo(next.kind, {
|
|
616
|
+
subject: next.subject,
|
|
617
|
+
categories: next.categories
|
|
618
|
+
}));
|
|
619
|
+
rewritten = await doc.download();
|
|
620
|
+
} finally {
|
|
621
|
+
await doc.close();
|
|
622
|
+
}
|
|
623
|
+
throwIfCancelled(signal);
|
|
624
|
+
libraryBinaries.set(library.id, rewritten);
|
|
625
|
+
ctx.state.update(setAsset, next);
|
|
626
|
+
libraryChanged.emit({
|
|
627
|
+
libraryId: library.id,
|
|
628
|
+
reason: "asset-updated"
|
|
629
|
+
});
|
|
630
|
+
assetUpdated.emit({
|
|
631
|
+
assetId: next.id,
|
|
632
|
+
libraryId: library.id,
|
|
633
|
+
asset: next
|
|
634
|
+
});
|
|
635
|
+
return { asset: next };
|
|
636
|
+
}, signal);
|
|
637
|
+
};
|
|
638
|
+
const moveAsset = async (id, to, options) => {
|
|
639
|
+
const signal = options?.signal;
|
|
640
|
+
throwIfCancelled(signal);
|
|
641
|
+
const asset = ctx.state.get().assets[id];
|
|
642
|
+
const binary = assetBinaries.get(id);
|
|
643
|
+
if (!asset || !binary) throw notFound("asset", id);
|
|
644
|
+
if (to.libraryId === asset.libraryId) return { asset };
|
|
645
|
+
const copy = await createAsset({
|
|
646
|
+
libraryId: to.libraryId,
|
|
647
|
+
source: new Uint8Array(binary.bytes),
|
|
648
|
+
kind: asset.kind,
|
|
649
|
+
label: asset.label,
|
|
650
|
+
...asset.subject !== void 0 ? { subject: asset.subject } : {},
|
|
651
|
+
...asset.categories ? { categories: [...asset.categories] } : {},
|
|
652
|
+
...binary.preview ? { preview: binary.preview.bytes } : {}
|
|
653
|
+
}, { signal });
|
|
654
|
+
await deleteAsset(id);
|
|
655
|
+
return copy;
|
|
656
|
+
};
|
|
657
|
+
const duplicateAsset = async (id, options) => {
|
|
658
|
+
throwIfCancelled(options?.signal);
|
|
659
|
+
const asset = ctx.state.get().assets[id];
|
|
660
|
+
const binary = assetBinaries.get(id);
|
|
661
|
+
if (!asset || !binary) throw notFound("asset", id);
|
|
662
|
+
return createAsset({
|
|
663
|
+
libraryId: asset.libraryId,
|
|
664
|
+
source: new Uint8Array(binary.bytes),
|
|
665
|
+
kind: asset.kind,
|
|
666
|
+
label: options?.label ?? `${asset.label} copy`,
|
|
667
|
+
...asset.subject !== void 0 ? { subject: asset.subject } : {},
|
|
668
|
+
...asset.categories ? { categories: [...asset.categories] } : {},
|
|
669
|
+
...binary.preview ? { preview: binary.preview.bytes } : {}
|
|
670
|
+
}, { signal: options?.signal });
|
|
671
|
+
};
|
|
672
|
+
return { api: {
|
|
673
|
+
createAsset: verb(createAsset),
|
|
674
|
+
createAssetFromAnnotations: verb(createAssetFromAnnotations),
|
|
675
|
+
updateAsset: verb(updateAsset),
|
|
676
|
+
deleteAsset: verb(deleteAsset),
|
|
677
|
+
moveAsset: verb(moveAsset),
|
|
678
|
+
duplicateAsset: verb(duplicateAsset)
|
|
679
|
+
} };
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
10
682
|
//#region src/blank-library.ts
|
|
11
683
|
/**
|
|
12
|
-
* The seed of a new library: a one-page PDF whose page is
|
|
684
|
+
* The seed of a new library: a one-page PDF whose page is unregistered, the
|
|
13
685
|
* shape Acrobat itself gives a fresh stamp library (its first page is a
|
|
14
686
|
* blank starter that no `/Names /Pages` key points at). A library must be a
|
|
15
687
|
* PDF from the moment it exists, and a PDF must have a page; the unregistered
|
|
@@ -37,57 +709,13 @@ function blankLibraryPdf() {
|
|
|
37
709
|
return new Uint8Array(cached);
|
|
38
710
|
}
|
|
39
711
|
//#endregion
|
|
40
|
-
//#region src/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
});
|
|
87
|
-
/** Session-unique ids. Assets are session-scoped for now (no persistence),
|
|
88
|
-
* so a timestamp + counter is enough — durable ids come with the store port. */
|
|
89
|
-
let seq = 0;
|
|
90
|
-
const uid = (prefix) => `${prefix}-${Date.now().toString(36)}-${(seq++).toString(36)}`;
|
|
712
|
+
//#region src/write/libraries.ts
|
|
713
|
+
/**
|
|
714
|
+
* Library writes. A library is a PDF: its `/Title` is the name, its
|
|
715
|
+
* `/Names /Pages` registry the assets, its pages the artwork, PieceInfo only
|
|
716
|
+
* what has no standard home. Every whole-PDF rewrite rides the per-library
|
|
717
|
+
* mutation queue.
|
|
718
|
+
*/
|
|
91
719
|
const entryString = (entries, key) => {
|
|
92
720
|
const entry = entries[key];
|
|
93
721
|
return entry?.type === "string" && entry.value.length > 0 ? entry.value : void 0;
|
|
@@ -108,130 +736,76 @@ const kindFromPdfName = (name) => {
|
|
|
108
736
|
default: return;
|
|
109
737
|
}
|
|
110
738
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const assetEngine = () => {
|
|
131
|
-
if (!assetEngineRef) {
|
|
132
|
-
const cfg = config.assetEngine;
|
|
133
|
-
assetEngineRef = !cfg ? ctx.engine : typeof cfg === "function" ? cfg() : cfg;
|
|
134
|
-
}
|
|
135
|
-
return assetEngineRef;
|
|
136
|
-
};
|
|
137
|
-
const openAssetDocument = async (bytes) => {
|
|
138
|
-
try {
|
|
139
|
-
return await (await assetEngine()).open({
|
|
140
|
-
kind: "bytes",
|
|
141
|
-
id: uid("stamp-import"),
|
|
142
|
-
bytes
|
|
143
|
-
}, { scope: ["*"] });
|
|
144
|
-
} catch (err) {
|
|
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.");
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
/** Import-time preview render → bytes. The asset engine is local by
|
|
150
|
-
* definition, so the image source is always inline bytes. */
|
|
151
|
-
const imageToPreview = (image) => {
|
|
152
|
-
if (image.source.kind !== "bytes") throw new _embedpdf_engine_core_runtime.EngineError(_embedpdf_engine_core_runtime.EngineErrorCode.NotImplemented, "[stamp] asset engine returned a URL-sourced render; asset engines must be local");
|
|
153
|
-
return {
|
|
154
|
-
bytes: image.source.bytes,
|
|
155
|
-
mimeType: image.contentType
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
const allocateId = (preferred, prefix, taken) => {
|
|
159
|
-
let id = preferred;
|
|
160
|
-
if (!id || taken.has(id)) do
|
|
161
|
-
id = uid(prefix);
|
|
162
|
-
while (taken.has(id));
|
|
163
|
-
taken.add(id);
|
|
164
|
-
return id;
|
|
165
|
-
};
|
|
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());
|
|
739
|
+
const allocateId = (preferred, prefix, taken) => {
|
|
740
|
+
let id = preferred;
|
|
741
|
+
if (!id || taken.has(id)) do
|
|
742
|
+
id = uid(prefix);
|
|
743
|
+
while (taken.has(id));
|
|
744
|
+
taken.add(id);
|
|
745
|
+
return id;
|
|
746
|
+
};
|
|
747
|
+
function createLibraryWrites(ctx, { events, binaries, assetEngine }) {
|
|
748
|
+
const { libraryChanged, libraryCreated, libraryUpdated, libraryDeleted, assetCreated } = events;
|
|
749
|
+
const { binaries: assetBinaries, libraryBinaries, ghostRenders, mutateLibrary } = binaries;
|
|
750
|
+
const { openAssetDocument, renderThumbnail } = assetEngine;
|
|
751
|
+
const createLibrary = async (name, options) => {
|
|
752
|
+
const signal = options?.signal;
|
|
753
|
+
throwIfCancelled(signal);
|
|
754
|
+
const kind = options?.kind ?? "stamps";
|
|
755
|
+
const taken = new Set(Object.keys(ctx.state.get().libraries));
|
|
756
|
+
const id = allocateId(options?.id, "stamp-lib", taken);
|
|
757
|
+
const doc = await ctx.cancellable(signal, openAssetDocument(blankLibraryPdf()));
|
|
182
758
|
let bytes;
|
|
183
759
|
try {
|
|
184
|
-
requireCanonicalServices(doc);
|
|
185
760
|
await doc.metadata.update({ title: name });
|
|
186
761
|
await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(id, {
|
|
187
762
|
kind,
|
|
188
|
-
categories:
|
|
763
|
+
categories: options?.categories
|
|
189
764
|
}));
|
|
190
765
|
bytes = await doc.download();
|
|
191
766
|
} finally {
|
|
192
767
|
await doc.close();
|
|
193
768
|
}
|
|
769
|
+
throwIfCancelled(signal);
|
|
770
|
+
const library = {
|
|
771
|
+
id,
|
|
772
|
+
name,
|
|
773
|
+
kind,
|
|
774
|
+
assetIds: [],
|
|
775
|
+
...options?.categories ? { categories: options.categories } : {}
|
|
776
|
+
};
|
|
194
777
|
libraryBinaries.set(id, bytes);
|
|
195
|
-
ctx.
|
|
196
|
-
type: "LIBRARY_ADDED",
|
|
197
|
-
library: {
|
|
198
|
-
id,
|
|
199
|
-
name,
|
|
200
|
-
kind,
|
|
201
|
-
assetIds: [],
|
|
202
|
-
...opts?.categories ? { categories: opts.categories } : {}
|
|
203
|
-
}
|
|
204
|
-
});
|
|
778
|
+
ctx.state.update(addLibrary, library);
|
|
205
779
|
libraryChanged.emit({
|
|
206
780
|
libraryId: id,
|
|
207
781
|
reason: "created"
|
|
208
782
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
783
|
+
libraryCreated.emit({
|
|
784
|
+
libraryId: id,
|
|
785
|
+
library
|
|
786
|
+
});
|
|
787
|
+
return { library };
|
|
214
788
|
};
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
const
|
|
789
|
+
const importLibrary = async (source, options) => {
|
|
790
|
+
const signal = options?.signal;
|
|
791
|
+
throwIfCancelled(signal);
|
|
792
|
+
const resolved = await ctx.cancellable(signal, (0, _embedpdf_engine_core_runtime.resolveBinarySource)(source));
|
|
793
|
+
if ((0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes)?.mimeType !== "application/pdf") throw stampError("invalid-input", "importLibrary needs PDF bytes (use createAsset for a raster image)");
|
|
794
|
+
const doc = await ctx.cancellable(signal, openAssetDocument(new Uint8Array(resolved.bytes)));
|
|
219
795
|
let imported;
|
|
220
796
|
try {
|
|
221
|
-
requireCanonicalServices(doc);
|
|
222
797
|
const layout = await doc.pages.list();
|
|
223
|
-
if (layout.pageCount === 0) throw
|
|
224
|
-
const
|
|
225
|
-
const catalogEntries = (await doc.pieceInfo.
|
|
226
|
-
const
|
|
227
|
-
const takenLibraryIds = new Set(Object.keys(state.libraries));
|
|
798
|
+
if (layout.pageCount === 0) throw stampError("invalid-input", "a canonical stamp library PDF must contain at least one page");
|
|
799
|
+
const pagesByObjectNumber = new Map(layout.pages.map((page) => [page.ref.objectNumber, page]));
|
|
800
|
+
const catalogEntries = (await doc.pieceInfo.get("EMBD_StampLibrary"))?.entries ?? {};
|
|
801
|
+
const takenLibraryIds = new Set(Object.keys(ctx.state.get().libraries));
|
|
228
802
|
const libraryId = allocateId(entryString(catalogEntries, "Id"), "stamp-lib", takenLibraryIds);
|
|
229
|
-
const docMeta = await doc.metadata.
|
|
230
|
-
const libraryName = (docMeta.title && docMeta.title.length > 0 ? docMeta.title : void 0) ?? entryString(catalogEntries, "Name") ??
|
|
803
|
+
const docMeta = await doc.metadata.get();
|
|
804
|
+
const libraryName = (docMeta.title && docMeta.title.length > 0 ? docMeta.title : void 0) ?? entryString(catalogEntries, "Name") ?? options?.name ?? "Stamps";
|
|
231
805
|
if (!docMeta.title) await doc.metadata.update({ title: libraryName });
|
|
232
|
-
const libraryCategories =
|
|
806
|
+
const libraryCategories = options?.categories ?? entryStringArray(catalogEntries, "Categories");
|
|
233
807
|
const libraryLocale = entryString(catalogEntries, "Locale");
|
|
234
|
-
const libraryKind =
|
|
808
|
+
const libraryKind = options?.libraryKind ?? require_contract.libraryKindFromPdfName(entryName(catalogEntries, "Kind"));
|
|
235
809
|
const registry = (layout.namedPages ?? []).filter((entry) => entry.target.kind === "page");
|
|
236
810
|
const hidden = (layout.namedPages ?? []).filter((entry) => entry.target.kind !== "page");
|
|
237
811
|
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`);
|
|
@@ -240,35 +814,35 @@ function createStampCapability(ctx, config = {}) {
|
|
|
240
814
|
const seen = /* @__PURE__ */ new Set();
|
|
241
815
|
descriptors = registry.map((entry) => {
|
|
242
816
|
const { name, label } = require_contract.parseStampKey(entry.name);
|
|
243
|
-
if (!name) throw
|
|
244
|
-
if (seen.has(name)) throw
|
|
817
|
+
if (!name) throw stampError("invalid-input", `library '${libraryName}': empty stamp identifier in key '${entry.name}'`);
|
|
818
|
+
if (seen.has(name)) throw stampError("invalid-input", `library '${libraryName}': duplicate stamp identifier '${name}'`);
|
|
245
819
|
seen.add(name);
|
|
246
|
-
const page =
|
|
820
|
+
const page = pagesByObjectNumber.get(entry.target.page.objectNumber);
|
|
247
821
|
return {
|
|
248
|
-
|
|
822
|
+
page: page.ref,
|
|
249
823
|
index: page.index,
|
|
250
824
|
name,
|
|
251
825
|
label
|
|
252
826
|
};
|
|
253
827
|
});
|
|
254
|
-
descriptors.sort((
|
|
255
|
-
} else if (layout.namedPages === void 0) throw
|
|
828
|
+
descriptors.sort((left, right) => left.index - right.index);
|
|
829
|
+
} else if (layout.namedPages === void 0) throw stampError("unsupported", "the asset engine reports no named-page registry; an engine with named pages is needed to import libraries");
|
|
256
830
|
else {
|
|
257
831
|
descriptors = [];
|
|
258
832
|
for (const page of layout.pages) {
|
|
259
|
-
const
|
|
260
|
-
const name = (
|
|
261
|
-
const label = (
|
|
833
|
+
const version1Entries = (await doc.page(page.ref).pieceInfo.get(require_contract.STAMP_PIECEINFO_APP))?.entries;
|
|
834
|
+
const name = (version1Entries && entryString(version1Entries, "Name")) ?? `Stamp${page.index + 1}`;
|
|
835
|
+
const label = (version1Entries && entryString(version1Entries, "Subject")) ?? options?.assetName?.(page.index) ?? name;
|
|
262
836
|
descriptors.push({
|
|
263
|
-
|
|
837
|
+
page: page.ref,
|
|
264
838
|
index: page.index,
|
|
265
839
|
name,
|
|
266
840
|
label
|
|
267
841
|
});
|
|
268
842
|
}
|
|
269
|
-
for (const
|
|
270
|
-
name: require_contract.stampKey(
|
|
271
|
-
|
|
843
|
+
for (const descriptor of descriptors) await doc.pages.setName({
|
|
844
|
+
name: require_contract.stampKey(descriptor.name, descriptor.label),
|
|
845
|
+
page: descriptor.page
|
|
272
846
|
});
|
|
273
847
|
}
|
|
274
848
|
await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(libraryId, {
|
|
@@ -277,25 +851,24 @@ function createStampCapability(ctx, config = {}) {
|
|
|
277
851
|
locale: libraryLocale
|
|
278
852
|
}));
|
|
279
853
|
const assets = [];
|
|
280
|
-
for (const
|
|
281
|
-
const handle = doc.page(
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const kind = opts?.kind ?? kindFromPdfName(entryName(entries, "Kind")) ?? "stamp";
|
|
854
|
+
for (const descriptor of descriptors) {
|
|
855
|
+
const handle = doc.page(descriptor.page);
|
|
856
|
+
const entries = (await handle.pieceInfo.get("EMBD_Stamp"))?.entries ?? {};
|
|
857
|
+
const kind = options?.kind ?? kindFromPdfName(entryName(entries, "Kind")) ?? "stamp";
|
|
285
858
|
const subject = entryString(entries, "SubjectOverride");
|
|
286
859
|
const categories = entryStringArray(entries, "Categories");
|
|
287
|
-
const page =
|
|
860
|
+
const page = pagesByObjectNumber.get(descriptor.page.objectNumber);
|
|
288
861
|
const asset = {
|
|
289
|
-
id: require_contract.assetIdFor(libraryId,
|
|
862
|
+
id: require_contract.assetIdFor(libraryId, descriptor.name),
|
|
290
863
|
libraryId,
|
|
291
864
|
kind,
|
|
292
|
-
name:
|
|
293
|
-
label:
|
|
865
|
+
name: descriptor.name,
|
|
866
|
+
label: descriptor.label,
|
|
294
867
|
size: {
|
|
295
868
|
width: page.size.width,
|
|
296
869
|
height: page.size.height
|
|
297
870
|
},
|
|
298
|
-
|
|
871
|
+
page: descriptor.page,
|
|
299
872
|
...subject !== void 0 ? { subject } : {},
|
|
300
873
|
...categories !== void 0 ? { categories } : {}
|
|
301
874
|
};
|
|
@@ -303,7 +876,7 @@ function createStampCapability(ctx, config = {}) {
|
|
|
303
876
|
subject,
|
|
304
877
|
categories
|
|
305
878
|
}));
|
|
306
|
-
const bytes = await doc.pages.extract([
|
|
879
|
+
const bytes = await doc.pages.extract([descriptor.page]);
|
|
307
880
|
assets.push({
|
|
308
881
|
asset,
|
|
309
882
|
bytes,
|
|
@@ -325,321 +898,71 @@ function createStampCapability(ctx, config = {}) {
|
|
|
325
898
|
} finally {
|
|
326
899
|
await doc.close();
|
|
327
900
|
}
|
|
901
|
+
throwIfCancelled(signal);
|
|
328
902
|
libraryBinaries.set(imported.library.id, imported.canonicalBytes);
|
|
329
|
-
ctx.
|
|
330
|
-
type: "LIBRARY_ADDED",
|
|
331
|
-
library: imported.library
|
|
332
|
-
});
|
|
903
|
+
ctx.state.update(addLibrary, imported.library);
|
|
333
904
|
for (const { asset, bytes, preview } of imported.assets) {
|
|
334
|
-
|
|
905
|
+
assetBinaries.set(asset.id, {
|
|
335
906
|
bytes,
|
|
336
907
|
preview
|
|
337
908
|
});
|
|
338
|
-
ctx.
|
|
339
|
-
type: "ASSET_ADDED",
|
|
340
|
-
asset
|
|
341
|
-
});
|
|
909
|
+
ctx.state.update(addAsset, asset);
|
|
342
910
|
}
|
|
343
911
|
libraryChanged.emit({
|
|
344
912
|
libraryId: imported.library.id,
|
|
345
|
-
reason: "imported"
|
|
346
|
-
});
|
|
347
|
-
|
|
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
|
-
};
|
|
416
|
-
const addAsset = async (input) => {
|
|
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);
|
|
419
|
-
const meta = (0, _embedpdf_engine_core_runtime.sniffBinaryMetadata)(resolved.bytes);
|
|
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");
|
|
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)");
|
|
430
|
-
let suppliedPreview = null;
|
|
431
|
-
if (input.preview) {
|
|
432
|
-
const preview = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.preview);
|
|
433
|
-
suppliedPreview = {
|
|
434
|
-
bytes: new Uint8Array(preview.bytes),
|
|
435
|
-
mimeType: preview.mimeType ?? "image/png"
|
|
436
|
-
};
|
|
437
|
-
}
|
|
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) {
|
|
452
|
-
const result = await doc.pages.insert(new Uint8Array(resolved.bytes));
|
|
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
|
|
465
|
-
},
|
|
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");
|
|
470
|
-
}
|
|
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
|
|
477
|
-
});
|
|
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
|
|
509
|
-
});
|
|
510
|
-
ctx.dispatch({
|
|
511
|
-
type: "ASSET_ADDED",
|
|
512
|
-
asset: appended.asset
|
|
513
|
-
});
|
|
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
|
|
530
|
-
});
|
|
531
|
-
};
|
|
532
|
-
const dropLibrary = (id) => {
|
|
533
|
-
const library = ctx.getState().libraries[id];
|
|
534
|
-
if (library) for (const assetId of library.assetIds) {
|
|
535
|
-
binaries.delete(assetId);
|
|
536
|
-
ghostRenders.delete(assetId);
|
|
537
|
-
}
|
|
538
|
-
libraryBinaries.delete(id);
|
|
539
|
-
ctx.dispatch({
|
|
540
|
-
type: "LIBRARY_REMOVED",
|
|
541
|
-
libraryId: id
|
|
542
|
-
});
|
|
543
|
-
};
|
|
544
|
-
const removeAsset = async (id) => {
|
|
545
|
-
const initialAsset = ctx.getState().assets[id];
|
|
546
|
-
if (!initialAsset) return;
|
|
547
|
-
return mutateLibrary(initialAsset.libraryId, async () => {
|
|
548
|
-
const asset = ctx.getState().assets[id];
|
|
549
|
-
if (!asset) return;
|
|
550
|
-
const library = ctx.getState().libraries[asset.libraryId];
|
|
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();
|
|
561
|
-
}
|
|
562
|
-
libraryBinaries.set(library.id, rewritten);
|
|
563
|
-
binaries.delete(id);
|
|
564
|
-
ghostRenders.delete(id);
|
|
565
|
-
ctx.dispatch({
|
|
566
|
-
type: "ASSET_REMOVED",
|
|
567
|
-
assetId: id
|
|
568
|
-
});
|
|
569
|
-
libraryChanged.emit({
|
|
570
|
-
libraryId: library.id,
|
|
571
|
-
reason: "asset-removed"
|
|
572
|
-
});
|
|
913
|
+
reason: "imported"
|
|
914
|
+
});
|
|
915
|
+
libraryCreated.emit({
|
|
916
|
+
libraryId: imported.library.id,
|
|
917
|
+
library: imported.library
|
|
573
918
|
});
|
|
919
|
+
for (const { asset } of imported.assets) assetCreated.emit({
|
|
920
|
+
assetId: asset.id,
|
|
921
|
+
libraryId: imported.library.id,
|
|
922
|
+
asset
|
|
923
|
+
});
|
|
924
|
+
return { library: ctx.state.get().libraries[imported.library.id] ?? imported.library };
|
|
925
|
+
};
|
|
926
|
+
const dropLibrary = (id) => {
|
|
927
|
+
const library = ctx.state.get().libraries[id];
|
|
928
|
+
if (library) for (const assetId of library.assetIds) {
|
|
929
|
+
assetBinaries.delete(assetId);
|
|
930
|
+
ghostRenders.delete(assetId);
|
|
931
|
+
}
|
|
932
|
+
libraryBinaries.delete(id);
|
|
933
|
+
ctx.state.update(removeLibrary, id);
|
|
574
934
|
};
|
|
575
|
-
const
|
|
576
|
-
const existed = ctx.
|
|
935
|
+
const deleteLibrary = (id, options) => mutateLibrary(id, async () => {
|
|
936
|
+
const existed = ctx.state.get().libraries[id] !== void 0;
|
|
577
937
|
dropLibrary(id);
|
|
578
|
-
if (existed)
|
|
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
|
-
});
|
|
938
|
+
if (existed) {
|
|
621
939
|
libraryChanged.emit({
|
|
622
|
-
libraryId:
|
|
623
|
-
reason: "
|
|
940
|
+
libraryId: id,
|
|
941
|
+
reason: "removed"
|
|
624
942
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
943
|
+
libraryDeleted.emit({
|
|
944
|
+
libraryId: id,
|
|
945
|
+
library: null
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}, options?.signal);
|
|
949
|
+
const updateLibrary = async (id, patch, options) => {
|
|
950
|
+
const signal = options?.signal;
|
|
951
|
+
throwIfCancelled(signal);
|
|
952
|
+
if (!ctx.state.get().libraries[id]) throw notFound("library", id);
|
|
629
953
|
return mutateLibrary(id, async () => {
|
|
630
|
-
const library = ctx.
|
|
631
|
-
if (!library) throw
|
|
954
|
+
const library = ctx.state.get().libraries[id];
|
|
955
|
+
if (!library) throw notFound("library", id);
|
|
632
956
|
const canonicalBytes = libraryBinaries.get(id);
|
|
633
|
-
if (!canonicalBytes) throw
|
|
957
|
+
if (!canonicalBytes) throw stampError("operation-failed", `canonical bytes are missing for library '${id}'`);
|
|
634
958
|
const next = {
|
|
635
959
|
...library,
|
|
636
960
|
...patch.name !== void 0 ? { name: patch.name } : {},
|
|
637
961
|
...patch.categories !== void 0 ? { categories: patch.categories } : {}
|
|
638
962
|
};
|
|
639
|
-
const doc = await openAssetDocument(canonicalBytes);
|
|
963
|
+
const doc = await ctx.cancellable(signal, openAssetDocument(canonicalBytes));
|
|
640
964
|
let rewritten;
|
|
641
965
|
try {
|
|
642
|
-
requireCanonicalServices(doc);
|
|
643
966
|
if (next.name !== library.name) await doc.metadata.update({ title: next.name });
|
|
644
967
|
await doc.pieceInfo.update(require_contract.STAMP_LIBRARY_PIECEINFO_APP, require_contract.stampLibraryPieceInfo(id, {
|
|
645
968
|
kind: next.kind,
|
|
@@ -650,72 +973,229 @@ function createStampCapability(ctx, config = {}) {
|
|
|
650
973
|
} finally {
|
|
651
974
|
await doc.close();
|
|
652
975
|
}
|
|
976
|
+
throwIfCancelled(signal);
|
|
653
977
|
libraryBinaries.set(id, rewritten);
|
|
654
|
-
ctx.
|
|
655
|
-
type: "LIBRARY_UPDATED",
|
|
656
|
-
library: next
|
|
657
|
-
});
|
|
978
|
+
ctx.state.update(setLibrary, next);
|
|
658
979
|
libraryChanged.emit({
|
|
659
980
|
libraryId: id,
|
|
660
981
|
reason: "updated"
|
|
661
982
|
});
|
|
662
|
-
|
|
983
|
+
libraryUpdated.emit({
|
|
984
|
+
libraryId: id,
|
|
985
|
+
library: next
|
|
986
|
+
});
|
|
987
|
+
return { library: next };
|
|
988
|
+
}, signal);
|
|
989
|
+
};
|
|
990
|
+
return {
|
|
991
|
+
createLibrary,
|
|
992
|
+
api: {
|
|
993
|
+
createLibrary: verb(createLibrary),
|
|
994
|
+
updateLibrary: verb(updateLibrary),
|
|
995
|
+
importLibrary: verb(importLibrary),
|
|
996
|
+
deleteLibrary: verb(deleteLibrary)
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
//#endregion
|
|
1001
|
+
//#region src/write/marks.ts
|
|
1002
|
+
/**
|
|
1003
|
+
* Authoring: a drawn or typed mark rendered to a one-page PDF (the
|
|
1004
|
+
* annotation is authored on a scratch page and exported the way "stamp from
|
|
1005
|
+
* selection" exports), a page of a PDF as a single-page PDF, and the bytes
|
|
1006
|
+
* an asset is made from.
|
|
1007
|
+
*/
|
|
1008
|
+
/** A mark's `#rrggbb` color, checked before anything is drawn. */
|
|
1009
|
+
const hexColor = (hex) => {
|
|
1010
|
+
const color = hex.trim();
|
|
1011
|
+
if (!/^#[0-9a-f]{6}$/i.test(color)) throw stampError("invalid-input", `mark color must be #rrggbb, got '${hex}'`);
|
|
1012
|
+
return color;
|
|
1013
|
+
};
|
|
1014
|
+
/** An exact ArrayBuffer over a view (the engine's binary idiom). */
|
|
1015
|
+
const toArrayBuffer = (bytes) => {
|
|
1016
|
+
const buffer = new ArrayBuffer(bytes.byteLength);
|
|
1017
|
+
new Uint8Array(buffer).set(bytes);
|
|
1018
|
+
return buffer;
|
|
1019
|
+
};
|
|
1020
|
+
/** An ink mark's strokes, as the engine takes them; a mark needs at least one point. */
|
|
1021
|
+
const inkListOf = (strokes) => {
|
|
1022
|
+
if (!strokes.some((stroke) => stroke.length > 0)) throw stampError("invalid-input", "an ink mark needs at least one point");
|
|
1023
|
+
return strokes.map((stroke) => stroke.map(({ x, y }) => ({
|
|
1024
|
+
x,
|
|
1025
|
+
y
|
|
1026
|
+
})));
|
|
1027
|
+
};
|
|
1028
|
+
/**
|
|
1029
|
+
* A generous box for typed text. The export keeps the whole box, so the
|
|
1030
|
+
* text is centered in it: the mark then sits in the middle of a preview
|
|
1031
|
+
* or a signature field.
|
|
1032
|
+
*/
|
|
1033
|
+
const textBounds = (text, fontSize) => ({
|
|
1034
|
+
x: 0,
|
|
1035
|
+
y: 0,
|
|
1036
|
+
width: Math.max(1, text.length) * fontSize * .75 + fontSize,
|
|
1037
|
+
height: fontSize * 1.6
|
|
1038
|
+
});
|
|
1039
|
+
function createMarks({ assetEngine }) {
|
|
1040
|
+
const { openAssetDocument } = assetEngine;
|
|
1041
|
+
/**
|
|
1042
|
+
* A drawn or typed mark rendered to a one-page PDF: the annotation is
|
|
1043
|
+
* authored on a scratch page and exported the way "stamp from selection"
|
|
1044
|
+
* exports — a fresh page the size of the mark, paths kept as paths, the
|
|
1045
|
+
* font subset-embedded. Self-contained and Acrobat-readable.
|
|
1046
|
+
*/
|
|
1047
|
+
const renderMark = async (mark) => {
|
|
1048
|
+
const doc = await openAssetDocument(blankLibraryPdf());
|
|
1049
|
+
try {
|
|
1050
|
+
const scratch = (await doc.pages.list()).pages[0];
|
|
1051
|
+
if (!scratch) throw stampError("operation-failed", "the scratch document has no page");
|
|
1052
|
+
const page = doc.page(scratch.ref);
|
|
1053
|
+
const color = hexColor(mark.color ?? "#1d2b53");
|
|
1054
|
+
const created = mark.kind === "ink" ? await page.annotations.create({
|
|
1055
|
+
subtype: "ink",
|
|
1056
|
+
inkList: inkListOf(mark.strokes),
|
|
1057
|
+
color,
|
|
1058
|
+
strokeWidth: mark.strokeWidth ?? 2
|
|
1059
|
+
}) : await page.annotations.create({
|
|
1060
|
+
subtype: "free-text",
|
|
1061
|
+
intent: "free-text",
|
|
1062
|
+
contents: mark.text,
|
|
1063
|
+
fontFamily: mark.fontFamily,
|
|
1064
|
+
fontSize: mark.fontSize ?? 36,
|
|
1065
|
+
textAlign: "center",
|
|
1066
|
+
verticalAlign: "middle",
|
|
1067
|
+
box: textBounds(mark.text, mark.fontSize ?? 36),
|
|
1068
|
+
fontColor: color,
|
|
1069
|
+
strokeWidth: 0
|
|
1070
|
+
});
|
|
1071
|
+
return new Uint8Array(await page.annotations.exportAppearance([created.annotation.ref]));
|
|
1072
|
+
} finally {
|
|
1073
|
+
await doc.close();
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
/** One page of a PDF as a single-page PDF (a `pdf` mark with `pageIndex`). */
|
|
1077
|
+
const extractPage = async (bytes, pageIndex) => {
|
|
1078
|
+
const doc = await openAssetDocument(bytes);
|
|
1079
|
+
try {
|
|
1080
|
+
const layout = await doc.pages.list();
|
|
1081
|
+
const page = layout.pages[pageIndex];
|
|
1082
|
+
if (!page) throw stampError("invalid-input", `the PDF has no page ${pageIndex} (${layout.pageCount} pages)`);
|
|
1083
|
+
return await doc.pages.extract([page.ref]);
|
|
1084
|
+
} finally {
|
|
1085
|
+
await doc.close();
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
/** The bytes an asset is made from: the caller's `source`, or its `mark` authored into one. */
|
|
1089
|
+
const resolveAssetSource = async (input) => {
|
|
1090
|
+
if (input.source === void 0 === (input.mark === void 0)) throw stampError("invalid-input", "createAsset takes exactly one of `source` / `mark`");
|
|
1091
|
+
if (input.source !== void 0) return (0, _embedpdf_engine_core_runtime.resolveBinarySource)(input.source);
|
|
1092
|
+
const mark = input.mark;
|
|
1093
|
+
switch (mark.kind) {
|
|
1094
|
+
case "image": return (0, _embedpdf_engine_core_runtime.resolveBinarySource)(mark.source);
|
|
1095
|
+
case "pdf": {
|
|
1096
|
+
const resolved = await (0, _embedpdf_engine_core_runtime.resolveBinarySource)(mark.source);
|
|
1097
|
+
if (mark.pageIndex === void 0) return resolved;
|
|
1098
|
+
return { bytes: toArrayBuffer(await extractPage(new Uint8Array(resolved.bytes), mark.pageIndex)) };
|
|
1099
|
+
}
|
|
1100
|
+
case "ink":
|
|
1101
|
+
case "text": return { bytes: toArrayBuffer(await renderMark(mark)) };
|
|
1102
|
+
}
|
|
663
1103
|
};
|
|
1104
|
+
return { resolveAssetSource };
|
|
1105
|
+
}
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/write/placement.ts
|
|
1108
|
+
/**
|
|
1109
|
+
* Placement: arm an asset on a document (the next click places) or place it
|
|
1110
|
+
* without the pointer. Form-backed stamps are executable templates only
|
|
1111
|
+
* until placement — they are evaluated on an isolated copy under the target
|
|
1112
|
+
* document's detached script realm, so neither the canonical library nor
|
|
1113
|
+
* its reusable per-page extraction becomes target/user/time specific.
|
|
1114
|
+
*/
|
|
1115
|
+
function createPlacement(ctx, { binaries, assetEngine, ghosts, targets }, { canPlace }) {
|
|
1116
|
+
const { binaries: assetBinaries, libraryBinaries } = binaries;
|
|
1117
|
+
const { openAssetDocument, imageToPreview } = assetEngine;
|
|
1118
|
+
const { ghostProvider } = ghosts;
|
|
1119
|
+
const { documentIdOf, targetOf, pageOf } = targets;
|
|
1120
|
+
const settings = ctx.settings();
|
|
1121
|
+
const droppedArms = () => Object.keys(ctx.state.get().armed).filter((documentId) => !ctx.tryForDocument(_embedpdf_plugin_annotation_contract_host.AnnotationToken, documentId)?.stamps.isArmed()).join("\n");
|
|
1122
|
+
ctx.watch(droppedArms, (dropped) => {
|
|
1123
|
+
if (dropped) ctx.state.update(disarmDocuments, dropped.split("\n"));
|
|
1124
|
+
});
|
|
1125
|
+
/** The one payload both entry points hand the annotation plugin: artwork,
|
|
1126
|
+
* a resolution-aware ghost, true size, and the identity a placement
|
|
1127
|
+
* writes (`/Name` = identifier, `/Subj` = the subject override or label). */
|
|
1128
|
+
const stampPayload = (asset, placement, materialized) => ({
|
|
1129
|
+
source: placement.bytes,
|
|
1130
|
+
preview: ghostProvider(placement.bytes, placement.preview, materialized ? null : asset.id),
|
|
1131
|
+
intrinsicSize: asset.size,
|
|
1132
|
+
name: asset.name,
|
|
1133
|
+
subject: asset.subject ?? asset.label
|
|
1134
|
+
});
|
|
664
1135
|
/**
|
|
665
1136
|
* Form-backed stamps are executable templates only until placement. Work
|
|
666
1137
|
* on an isolated copy so neither the canonical library nor its reusable
|
|
667
1138
|
* per-page extraction becomes target/user/time specific.
|
|
668
1139
|
*
|
|
669
|
-
* The realm comes from the
|
|
670
|
-
* realm under that document's policy and environment
|
|
671
|
-
* sandbox
|
|
672
|
-
* scripting off, or `dynamic: false
|
|
1140
|
+
* The realm comes from the target document's actions plugin: a detached
|
|
1141
|
+
* realm under that document's policy and environment, with its own
|
|
1142
|
+
* sandbox and none of the viewer realm's globals. No actions plugin,
|
|
1143
|
+
* scripting off, or `dynamic: false`: the template is armed as-is.
|
|
673
1144
|
*/
|
|
674
|
-
const materializeForPlacement = async (documentId, asset,
|
|
675
|
-
if (
|
|
1145
|
+
const materializeForPlacement = async (documentId, asset, binary) => {
|
|
1146
|
+
if (!settings.get().dynamic) return binary;
|
|
676
1147
|
const actions = ctx.tryForDocument(_embedpdf_plugin_actions_contract_host.ActionsToken, documentId);
|
|
677
1148
|
const mintRealm = actions?.createDetachedScriptRealm;
|
|
678
|
-
if (!actions || !mintRealm) return
|
|
679
|
-
const
|
|
680
|
-
|
|
1149
|
+
if (!actions || !mintRealm) return binary;
|
|
1150
|
+
const documents = ctx.get(_embedpdf_core.DocumentsToken);
|
|
1151
|
+
const target = documents.get(documentId);
|
|
1152
|
+
if (target?.status !== "ready") throw stampError("not-found", `document '${documentId}' is not open`);
|
|
1153
|
+
const targetDocument = {
|
|
1154
|
+
name: target.name,
|
|
1155
|
+
pageCount: target.pageCount,
|
|
1156
|
+
pages: documents.listPages(documentId)
|
|
1157
|
+
};
|
|
681
1158
|
const canonicalBytes = libraryBinaries.get(asset.libraryId);
|
|
682
|
-
const sourceBytes = canonicalBytes ??
|
|
1159
|
+
const sourceBytes = canonicalBytes ?? binary.bytes;
|
|
683
1160
|
const doc = await openAssetDocument(new Uint8Array(sourceBytes));
|
|
684
1161
|
let realm = null;
|
|
685
1162
|
let scripting = null;
|
|
686
1163
|
try {
|
|
687
1164
|
const layout = await doc.pages.list();
|
|
688
|
-
const selectedPage = asset.
|
|
689
|
-
if (!selectedPage) throw
|
|
690
|
-
|
|
691
|
-
if (!
|
|
1165
|
+
const selectedPage = asset.page === void 0 ? layout.pageCount === 1 ? layout.pages[0] : void 0 : layout.pages.find(({ ref }) => ref.objectNumber === asset.page.objectNumber);
|
|
1166
|
+
if (!selectedPage) throw stampError("invalid-input", canonicalBytes ? `canonical page ${asset.page?.objectNumber ?? "unknown"} no longer exists` : "a loose dynamic stamp asset must contain exactly one page");
|
|
1167
|
+
const snapshot = await doc.forms.list();
|
|
1168
|
+
if (!snapshot.fields.some((field) => field.widgets.some(({ page }) => page?.objectNumber === selectedPage.ref.objectNumber))) return binary;
|
|
1169
|
+
if (!doc.pages.flatten || !doc.pages.extract) throw stampError("unsupported", "dynamic PDF stamps need an asset engine with pages.flatten and pages.extract");
|
|
692
1170
|
realm = mintRealm.call(actions, {
|
|
693
1171
|
doc,
|
|
694
|
-
document: () =>
|
|
1172
|
+
document: () => targetDocument,
|
|
695
1173
|
bootSources: async () => {
|
|
696
|
-
return (doc.actions ? await doc.actions.
|
|
1174
|
+
return (doc.actions ? await doc.actions.get() : null)?.nameTreeScripts.map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action)) ?? [];
|
|
697
1175
|
}
|
|
698
1176
|
});
|
|
699
1177
|
scripting = (0, _embedpdf_plugin_form_scripting.createFormScriptingController)({
|
|
700
|
-
|
|
701
|
-
document: () => targetMeta,
|
|
1178
|
+
document: () => targetDocument,
|
|
702
1179
|
transaction: realm.transaction.bind(realm),
|
|
703
1180
|
budget: realm.budget
|
|
704
1181
|
});
|
|
705
|
-
const result = await scripting.
|
|
1182
|
+
const result = await scripting.transaction(async (scripts) => {
|
|
1183
|
+
const calculated = await scripts.recalculate(snapshot);
|
|
1184
|
+
if (calculated.status === "accepted" && calculated.effects.length > 0) await doc.apply({ ops: (0, _embedpdf_engine_core_runtime.formEffectOps)(calculated.effects) });
|
|
1185
|
+
return calculated;
|
|
1186
|
+
});
|
|
706
1187
|
actions.surfaceScriptCommit(result, {
|
|
707
1188
|
origin: "user",
|
|
708
1189
|
realm: "detached"
|
|
709
1190
|
});
|
|
710
|
-
if (result.status === "failed") throw
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const
|
|
715
|
-
const image = await doc.page(pageObjectNumber).render.image({
|
|
1191
|
+
if (result.status === "failed") throw stampError("operation-failed", `dynamic stamp scripting failed: ${result.error?.message ?? "native form effect failed"}`);
|
|
1192
|
+
const page = selectedPage.ref;
|
|
1193
|
+
await doc.pages.flatten([page], { usage: "display" });
|
|
1194
|
+
const bytes = await doc.pages.extract([page]);
|
|
1195
|
+
const image = await doc.page(page).render.image({
|
|
716
1196
|
viewport: {
|
|
717
1197
|
kind: "width",
|
|
718
|
-
width:
|
|
1198
|
+
width: settings.get().previewWidth
|
|
719
1199
|
},
|
|
720
1200
|
background: "transparent",
|
|
721
1201
|
includeAnnotations: false,
|
|
@@ -731,242 +1211,199 @@ function createStampCapability(ctx, config = {}) {
|
|
|
731
1211
|
await doc.close();
|
|
732
1212
|
}
|
|
733
1213
|
};
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
armedByDocument.set(documentId, assetId);
|
|
1214
|
+
/** An asset and its bytes, or `not-found`. */
|
|
1215
|
+
const assetOf = (assetId) => {
|
|
1216
|
+
const asset = ctx.state.get().assets[assetId];
|
|
1217
|
+
const binary = assetBinaries.get(assetId);
|
|
1218
|
+
if (!asset || !binary) throw notFound("asset", assetId);
|
|
1219
|
+
return {
|
|
1220
|
+
asset,
|
|
1221
|
+
binary
|
|
1222
|
+
};
|
|
744
1223
|
};
|
|
745
|
-
/**
|
|
746
|
-
|
|
747
|
-
|
|
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;
|
|
1224
|
+
/** Placing is annotation work: refused where the document's annotation plugin refuses a create. */
|
|
1225
|
+
const assertMayPlace = (documentId, operation) => {
|
|
1226
|
+
if (!canPlace(documentId)) throw permissionDenied("annotations:create", operation);
|
|
757
1227
|
};
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
}
|
|
1228
|
+
const armAsset$1 = async (assetId, options) => {
|
|
1229
|
+
const signal = options?.signal;
|
|
1230
|
+
throwIfCancelled(signal);
|
|
1231
|
+
const documentId = targetOf(options);
|
|
1232
|
+
const { asset, binary } = assetOf(assetId);
|
|
1233
|
+
assertMayPlace(documentId, "stamp.armAsset");
|
|
1234
|
+
const placement = await ctx.cancellable(signal, materializeForPlacement(documentId, asset, binary));
|
|
1235
|
+
throwIfCancelled(signal);
|
|
1236
|
+
await ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).stamps.arm({
|
|
1237
|
+
...stampPayload(asset, placement, placement !== binary),
|
|
1238
|
+
targetWidth: options?.targetWidth
|
|
1239
|
+
}, { signal });
|
|
1240
|
+
ctx.state.update(armAsset, documentId, assetId);
|
|
782
1241
|
};
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
if (
|
|
788
|
-
return (
|
|
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
|
-
};
|
|
1242
|
+
const getArmedAsset = (documentId) => {
|
|
1243
|
+
const id = documentIdOf(documentId);
|
|
1244
|
+
const assetId = id === null ? void 0 : ctx.state.get().armed[id];
|
|
1245
|
+
if (id === null || assetId === void 0) return null;
|
|
1246
|
+
if (!ctx.tryForDocument(_embedpdf_plugin_annotation_contract_host.AnnotationToken, id)?.stamps.isArmed()) return null;
|
|
1247
|
+
return ctx.state.get().assets[assetId] ?? null;
|
|
799
1248
|
};
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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);
|
|
1249
|
+
const disarm = (documentId) => {
|
|
1250
|
+
const id = documentIdOf(documentId);
|
|
1251
|
+
if (id === null) return;
|
|
1252
|
+
ctx.tryForDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, id)?.stamps.disarm();
|
|
1253
|
+
ctx.state.update(disarmDocuments, [id]);
|
|
816
1254
|
};
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return s.libraryOrder.map((id) => s.libraries[id]).filter((l) => l != null && (kinds === null || kinds.has(l.kind)));
|
|
831
|
-
},
|
|
832
|
-
library: (id) => ctx.getState().libraries[id] ?? null,
|
|
833
|
-
assets: (libraryId) => {
|
|
834
|
-
const s = ctx.getState();
|
|
835
|
-
if (libraryId) {
|
|
836
|
-
const library = s.libraries[libraryId];
|
|
837
|
-
return library ? library.assetIds.map((id) => s.assets[id]).filter((a) => a != null) : [];
|
|
838
|
-
}
|
|
839
|
-
return s.libraryOrder.flatMap((lid) => (s.libraries[lid]?.assetIds ?? []).map((id) => s.assets[id]).filter((a) => a != null));
|
|
840
|
-
},
|
|
841
|
-
asset: (id) => ctx.getState().assets[id] ?? null,
|
|
842
|
-
assetPreview: (id) => binaries.get(id)?.preview ?? null,
|
|
843
|
-
assetBytes: (id) => binaries.get(id)?.bytes ?? null,
|
|
844
|
-
exportLibrary: (id) => libraryBinaries.get(id) ?? null,
|
|
845
|
-
onLibraryChanged: libraryChanged.on,
|
|
846
|
-
createLibrary,
|
|
847
|
-
updateLibrary,
|
|
848
|
-
importLibraryPdf,
|
|
849
|
-
addAsset,
|
|
850
|
-
addAssetFromAnnotations,
|
|
851
|
-
updateAsset,
|
|
852
|
-
removeAsset,
|
|
853
|
-
removeLibrary,
|
|
854
|
-
armAsset,
|
|
855
|
-
placeAsset,
|
|
856
|
-
disarm: (documentId) => {
|
|
857
|
-
armedByDocument.delete(documentId);
|
|
858
|
-
ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).disarmStamp();
|
|
859
|
-
},
|
|
860
|
-
armedAsset
|
|
1255
|
+
/** Place on one document, its permission already checked. */
|
|
1256
|
+
const placeOn = async (documentId, assetId, placement, signal) => {
|
|
1257
|
+
const { asset, binary } = assetOf(assetId);
|
|
1258
|
+
const materialized = await ctx.cancellable(signal, materializeForPlacement(documentId, asset, binary));
|
|
1259
|
+
throwIfCancelled(signal);
|
|
1260
|
+
return ctx.forDocument(_embedpdf_plugin_annotation_contract.AnnotationToken, documentId).stamps.place(stampPayload(asset, materialized, materialized !== binary), placement, { signal });
|
|
1261
|
+
};
|
|
1262
|
+
const placeAsset = async (assetId, placement, options) => {
|
|
1263
|
+
throwIfCancelled(options?.signal);
|
|
1264
|
+
const documentId = targetOf(options);
|
|
1265
|
+
assetOf(assetId);
|
|
1266
|
+
assertMayPlace(documentId, "stamp.placeAsset");
|
|
1267
|
+
return placeOn(documentId, assetId, placement, options?.signal);
|
|
861
1268
|
};
|
|
1269
|
+
const placeAssetOnPages = async (assetId, pages, placement, options) => {
|
|
1270
|
+
const signal = options?.signal;
|
|
1271
|
+
throwIfCancelled(signal);
|
|
1272
|
+
const documentId = targetOf(options);
|
|
1273
|
+
assetOf(assetId);
|
|
1274
|
+
assertMayPlace(documentId, "stamp.placeAssetOnPages");
|
|
1275
|
+
const targetPages = pages === "all" ? ctx.get(_embedpdf_core.DocumentsToken).listPages(documentId).map((page) => page.ref) : pages.map((page) => pageOf(documentId, page).ref);
|
|
1276
|
+
const applied = [];
|
|
1277
|
+
const failed = [];
|
|
1278
|
+
for (const page of targetPages) try {
|
|
1279
|
+
const placed = await placeOn(documentId, assetId, {
|
|
1280
|
+
...placement,
|
|
1281
|
+
page
|
|
1282
|
+
}, signal);
|
|
1283
|
+
applied.push(placed.annotation);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
const refused = (0, _embedpdf_core.toPluginError)("stamp", error);
|
|
1286
|
+
if (refused.code === "operation-cancelled") throw refused;
|
|
1287
|
+
failed.push({
|
|
1288
|
+
ref: page,
|
|
1289
|
+
error: (0, _embedpdf_core.toPluginErrorInfo)(refused)
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
applied,
|
|
1294
|
+
skipped: [],
|
|
1295
|
+
failed
|
|
1296
|
+
};
|
|
1297
|
+
};
|
|
1298
|
+
return { api: {
|
|
1299
|
+
armAsset: verb(armAsset$1),
|
|
1300
|
+
disarm,
|
|
1301
|
+
getArmedAsset,
|
|
1302
|
+
placeAsset: verb(placeAsset),
|
|
1303
|
+
placeAssetOnPages: verb(placeAssetOnPages)
|
|
1304
|
+
} };
|
|
862
1305
|
}
|
|
863
1306
|
//#endregion
|
|
864
|
-
//#region src/
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
[action.library.id]: action.library
|
|
887
|
-
}
|
|
888
|
-
} : state;
|
|
889
|
-
case "LIBRARY_REMOVED": {
|
|
890
|
-
const library = state.libraries[action.libraryId];
|
|
891
|
-
if (!library) return state;
|
|
892
|
-
const libraries = { ...state.libraries };
|
|
893
|
-
delete libraries[action.libraryId];
|
|
894
|
-
const assets = { ...state.assets };
|
|
895
|
-
for (const id of library.assetIds) delete assets[id];
|
|
896
|
-
return {
|
|
897
|
-
...state,
|
|
898
|
-
libraries,
|
|
899
|
-
libraryOrder: state.libraryOrder.filter((id) => id !== action.libraryId),
|
|
900
|
-
assets
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
case "ASSET_ADDED": {
|
|
904
|
-
const library = state.libraries[action.asset.libraryId];
|
|
905
|
-
if (!library) return state;
|
|
906
|
-
return {
|
|
907
|
-
...state,
|
|
908
|
-
assets: {
|
|
909
|
-
...state.assets,
|
|
910
|
-
[action.asset.id]: action.asset
|
|
911
|
-
},
|
|
912
|
-
libraries: {
|
|
913
|
-
...state.libraries,
|
|
914
|
-
[library.id]: {
|
|
915
|
-
...library,
|
|
916
|
-
assetIds: [...library.assetIds, action.asset.id]
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
};
|
|
1307
|
+
//#region src/controller.ts
|
|
1308
|
+
/**
|
|
1309
|
+
* The stamp controller: the composition root. It builds the plugin's
|
|
1310
|
+
* services once, wires each area with the services it declares, and
|
|
1311
|
+
* assembles the capability from the areas' API slices. Every verb and read
|
|
1312
|
+
* has a home in `read/` or `write/`; the one state-change event is derived
|
|
1313
|
+
* here.
|
|
1314
|
+
*/
|
|
1315
|
+
function createStampController(ctx) {
|
|
1316
|
+
const services = createServices(ctx);
|
|
1317
|
+
const { events } = services;
|
|
1318
|
+
const settings = ctx.settings();
|
|
1319
|
+
ctx.state.onChange(({ previous, next }) => {
|
|
1320
|
+
if (previous.armed === next.armed) return;
|
|
1321
|
+
const documentIds = /* @__PURE__ */ new Set([...Object.keys(previous.armed), ...Object.keys(next.armed)]);
|
|
1322
|
+
for (const documentId of documentIds) {
|
|
1323
|
+
const assetId = next.armed[documentId];
|
|
1324
|
+
if (previous.armed[documentId] === assetId) continue;
|
|
1325
|
+
events.armChanged.emit({
|
|
1326
|
+
documentId,
|
|
1327
|
+
asset: assetId === void 0 ? null : next.assets[assetId] ?? null
|
|
1328
|
+
});
|
|
920
1329
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
} : state.libraries
|
|
944
|
-
};
|
|
1330
|
+
});
|
|
1331
|
+
const catalog = createCatalog(ctx, services);
|
|
1332
|
+
const marks = createMarks(services);
|
|
1333
|
+
const libraries = createLibraryWrites(ctx, services);
|
|
1334
|
+
const assets = createAssetWrites(ctx, services, marks, libraries, catalog);
|
|
1335
|
+
const placement = createPlacement(ctx, services, catalog);
|
|
1336
|
+
return { api: (0, _embedpdf_core.composeApi)("stamp", [
|
|
1337
|
+
settings.api,
|
|
1338
|
+
catalog.api,
|
|
1339
|
+
libraries.api,
|
|
1340
|
+
assets.api,
|
|
1341
|
+
placement.api,
|
|
1342
|
+
{
|
|
1343
|
+
onLibraryChanged: events.libraryChanged.on,
|
|
1344
|
+
onLibraryCreated: events.libraryCreated.on,
|
|
1345
|
+
onLibraryUpdated: events.libraryUpdated.on,
|
|
1346
|
+
onLibraryDeleted: events.libraryDeleted.on,
|
|
1347
|
+
onAssetCreated: events.assetCreated.on,
|
|
1348
|
+
onAssetUpdated: events.assetUpdated.on,
|
|
1349
|
+
onAssetDeleted: events.assetDeleted.on,
|
|
1350
|
+
onArmChanged: events.armChanged.on
|
|
945
1351
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
};
|
|
1352
|
+
]) };
|
|
1353
|
+
}
|
|
949
1354
|
//#endregion
|
|
950
1355
|
//#region src/stamp.plugin.ts
|
|
951
1356
|
/**
|
|
952
|
-
*
|
|
953
|
-
* document; placement
|
|
954
|
-
*
|
|
955
|
-
* because the annotation token is document-scoped and resolved lazily per
|
|
956
|
-
* placement (a viewer without the annotation plugin fails at the arm call,
|
|
957
|
-
* with the kernel's own missing-capability error).
|
|
1357
|
+
* Stamp libraries and assets. Workspace-scoped: libraries outlive any one
|
|
1358
|
+
* document; placement targets a document through its annotation plugin.
|
|
1359
|
+
* `config` is the settings the app registers, over {@link STAMP_DEFAULTS}.
|
|
958
1360
|
*/
|
|
959
|
-
const stampPlugin = (config
|
|
1361
|
+
const stampPlugin = (config) => (0, _embedpdf_core.definePlugin)({
|
|
960
1362
|
id: "stamp",
|
|
961
1363
|
token: require_contract.StampToken,
|
|
962
1364
|
scope: "workspace",
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1365
|
+
optional: [_embedpdf_plugin_annotation_contract.AnnotationToken, _embedpdf_plugin_actions_contract.ActionsToken],
|
|
1366
|
+
state: initialStampState,
|
|
1367
|
+
settings: {
|
|
1368
|
+
defaults: require_contract.STAMP_DEFAULTS,
|
|
1369
|
+
registered: config,
|
|
1370
|
+
whole: ["assetEngine"]
|
|
1371
|
+
},
|
|
1372
|
+
create: createStampController,
|
|
1373
|
+
inScope: (stamp, documentId) => {
|
|
1374
|
+
const scoped = (options) => ({
|
|
1375
|
+
...options,
|
|
1376
|
+
documentId: options?.documentId ?? documentId
|
|
1377
|
+
});
|
|
1378
|
+
return {
|
|
1379
|
+
...stamp,
|
|
1380
|
+
createAssetFromAnnotations: (page, refs, input, options) => stamp.createAssetFromAnnotations(page, refs, input, scoped(options)),
|
|
1381
|
+
armAsset: (assetId, options) => stamp.armAsset(assetId, scoped(options)),
|
|
1382
|
+
disarm: (target = documentId) => stamp.disarm(target),
|
|
1383
|
+
getArmedAsset: (target = documentId) => stamp.getArmedAsset(target),
|
|
1384
|
+
placeAsset: (assetId, placement, options) => stamp.placeAsset(assetId, placement, scoped(options)),
|
|
1385
|
+
placeAssetOnPages: (assetId, pages, placement, options) => stamp.placeAssetOnPages(assetId, pages, placement, scoped(options)),
|
|
1386
|
+
canPlace: (target = documentId) => stamp.canPlace(target),
|
|
1387
|
+
canCreateFromAnnotations: (target = documentId) => stamp.canCreateFromAnnotations(target)
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/state.ts
|
|
1393
|
+
/**
|
|
1394
|
+
* The stamps page's State table as code: what `useStampState()` returns, and
|
|
1395
|
+
* the same fields in every other framework. It reads the document in scope,
|
|
1396
|
+
* else the active one. The libraries and their stamps are not here:
|
|
1397
|
+
* `useStampLibraries()` and `useStampAssets()` read them, so a component that
|
|
1398
|
+
* only shows the armed stamp doesn't re-render when a library changes.
|
|
1399
|
+
*/
|
|
1400
|
+
const stampState = (0, _embedpdf_core.defineState)(require_contract.StampToken, {
|
|
1401
|
+
read: (stamp) => ({ armedAsset: stamp.getArmedAsset() }),
|
|
1402
|
+
empty: { armedAsset: null }
|
|
966
1403
|
});
|
|
967
1404
|
//#endregion
|
|
968
1405
|
//#region src/persistence.ts
|
|
969
|
-
/** An in-memory store
|
|
1406
|
+
/** An in-memory store, for tests and SSR. */
|
|
970
1407
|
function memoryStampStore() {
|
|
971
1408
|
const rows = /* @__PURE__ */ new Map();
|
|
972
1409
|
return {
|
|
@@ -984,75 +1421,73 @@ function memoryStampStore() {
|
|
|
984
1421
|
}
|
|
985
1422
|
/**
|
|
986
1423
|
* Bring every stored library back: each PDF is imported as-is (its title,
|
|
987
|
-
* registry, and PieceInfo id come from the file
|
|
1424
|
+
* registry, and PieceInfo id come from the file; the store's key is only a
|
|
988
1425
|
* hint). Call once at boot, before seeding defaults, so "already has
|
|
989
1426
|
* libraries" means the user's own.
|
|
990
1427
|
*/
|
|
991
1428
|
async function restoreStampLibraries(stamp, store) {
|
|
992
1429
|
const restored = [];
|
|
993
1430
|
for (const { id, bytes } of await store.list()) try {
|
|
994
|
-
restored.push(await stamp.
|
|
1431
|
+
restored.push((await stamp.importLibrary(bytes)).library.id);
|
|
995
1432
|
} catch (error) {
|
|
996
1433
|
globalThis.console?.warn(`[stamp] stored library '${id}' could not be restored:`, error);
|
|
997
1434
|
}
|
|
998
1435
|
return restored;
|
|
999
1436
|
}
|
|
1000
1437
|
/**
|
|
1001
|
-
* Keep the store in sync from now on: every canonical change
|
|
1002
|
-
* library's PDF
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1438
|
+
* Keep the store in sync from now on: every canonical change saves the
|
|
1439
|
+
* library's PDF, a removal deletes it. `except` names libraries never to
|
|
1440
|
+
* persist (the bundled default set, typically). Returns the unsubscribe;
|
|
1441
|
+
* work already asked for still finishes.
|
|
1442
|
+
*
|
|
1443
|
+
* Each library has one line of store work, so saves never overlap and a
|
|
1444
|
+
* delete never lands under a late save. The changes of one burst (one call
|
|
1445
|
+
* into the plugin) are one save; the changes made while a save runs are one
|
|
1446
|
+
* more after it.
|
|
1005
1447
|
*/
|
|
1006
|
-
function persistStampLibraries(stamp, store,
|
|
1007
|
-
const except = new Set(
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
const
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1448
|
+
function persistStampLibraries(stamp, store, options = {}) {
|
|
1449
|
+
const except = new Set(options.except ?? []);
|
|
1450
|
+
/** Per library: the store step waiting or running, and the one to follow it. */
|
|
1451
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1452
|
+
const run = (libraryId, step) => {
|
|
1453
|
+
const line = lines.get(libraryId);
|
|
1454
|
+
if (line) {
|
|
1455
|
+
if (line.started) line.next = step;
|
|
1456
|
+
else line.step = step;
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const fresh = {
|
|
1460
|
+
step,
|
|
1461
|
+
started: false,
|
|
1462
|
+
next: null
|
|
1463
|
+
};
|
|
1464
|
+
lines.set(libraryId, fresh);
|
|
1465
|
+
Promise.resolve().then(async () => {
|
|
1466
|
+
fresh.started = true;
|
|
1467
|
+
try {
|
|
1468
|
+
if (fresh.step === "save") await store.put(libraryId, await stamp.exportLibrary(libraryId));
|
|
1469
|
+
else await store.delete(libraryId);
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
const doing = fresh.step === "save" ? "persisting" : "deleting stored";
|
|
1472
|
+
globalThis.console?.warn(`[stamp] ${doing} library '${libraryId}' failed:`, error);
|
|
1473
|
+
}
|
|
1474
|
+
lines.delete(libraryId);
|
|
1475
|
+
if (fresh.next) run(libraryId, fresh.next);
|
|
1015
1476
|
});
|
|
1016
1477
|
};
|
|
1017
|
-
|
|
1478
|
+
return stamp.onLibraryChanged(({ libraryId, reason }) => {
|
|
1018
1479
|
if (except.has(libraryId)) return;
|
|
1019
|
-
|
|
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));
|
|
1480
|
+
run(libraryId, reason === "removed" ? "delete" : "save");
|
|
1029
1481
|
});
|
|
1030
|
-
return () => {
|
|
1031
|
-
off();
|
|
1032
|
-
for (const [libraryId, timer] of timers) {
|
|
1033
|
-
clearTimeout(timer);
|
|
1034
|
-
write(libraryId);
|
|
1035
|
-
}
|
|
1036
|
-
};
|
|
1037
1482
|
}
|
|
1038
1483
|
//#endregion
|
|
1039
1484
|
exports.DEFAULT_LIBRARY_KIND = require_contract.DEFAULT_LIBRARY_KIND;
|
|
1040
|
-
exports.
|
|
1041
|
-
exports.STAMP_PIECEINFO_APP = require_contract.STAMP_PIECEINFO_APP;
|
|
1042
|
-
exports.STAMP_PIECEINFO_VERSION = require_contract.STAMP_PIECEINFO_VERSION;
|
|
1485
|
+
exports.STAMP_DEFAULTS = require_contract.STAMP_DEFAULTS;
|
|
1043
1486
|
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
1487
|
exports.memoryStampStore = memoryStampStore;
|
|
1049
|
-
exports.parseStampKey = require_contract.parseStampKey;
|
|
1050
1488
|
exports.persistStampLibraries = persistStampLibraries;
|
|
1051
1489
|
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;
|
|
1056
1490
|
exports.stampPlugin = stampPlugin;
|
|
1491
|
+
exports.stampState = stampState;
|
|
1057
1492
|
|
|
1058
1493
|
//# sourceMappingURL=index.cjs.map
|