fabricjs-document-engine 0.4.0 → 0.6.0
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/README.md +87 -3
- package/dist/engine/create-document-engine.cjs +199 -6
- package/dist/engine/create-document-engine.d.cts +42 -0
- package/dist/engine/create-document-engine.d.ts +42 -0
- package/dist/engine/create-document-engine.js +199 -6
- package/dist/engine/errors.cjs +2 -0
- package/dist/engine/errors.d.cts +6 -1
- package/dist/engine/errors.d.ts +6 -1
- package/dist/engine/errors.js +2 -0
- package/dist/export/download-export.cjs +14 -0
- package/dist/export/download-export.d.cts +7 -0
- package/dist/export/download-export.d.ts +7 -0
- package/dist/export/download-export.js +14 -0
- package/dist/export/export-area.cjs +46 -0
- package/dist/export/export-area.js +46 -0
- package/dist/export/export-options.cjs +47 -0
- package/dist/export/export-options.d.cts +20 -0
- package/dist/export/export-options.d.ts +20 -0
- package/dist/export/export-options.js +46 -0
- package/dist/export/preflight-export.cjs +105 -0
- package/dist/export/preflight-export.d.cts +17 -0
- package/dist/export/preflight-export.d.ts +17 -0
- package/dist/export/preflight-export.js +105 -0
- package/dist/export/render-export.cjs +69 -0
- package/dist/export/render-export.js +65 -0
- package/dist/history/apply-state.cjs +1 -1
- package/dist/history/apply-state.js +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -1
- package/dist/migrations/migrate-document.cjs +65 -0
- package/dist/migrations/migrate-document.d.cts +15 -0
- package/dist/migrations/migrate-document.d.ts +15 -0
- package/dist/migrations/migrate-document.js +63 -0
- package/dist/storage/key-value-storage.cjs +27 -1
- package/dist/storage/key-value-storage.d.cts +2 -1
- package/dist/storage/key-value-storage.d.ts +2 -1
- package/dist/storage/key-value-storage.js +27 -1
- package/dist/storage.d.cts +2 -1
- package/dist/storage.d.ts +2 -1
- package/dist/versions/document-version.cjs +20 -0
- package/dist/versions/document-version.d.cts +25 -0
- package/dist/versions/document-version.d.ts +25 -0
- package/dist/versions/document-version.js +17 -0
- package/package.json +1 -1
|
@@ -2,16 +2,22 @@ import { buildAssetManifest } from "../assets/asset-manifest.js";
|
|
|
2
2
|
import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
|
|
3
3
|
import { isSameUrl } from "../assets/image-check.js";
|
|
4
4
|
import { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave } from "../assets/asset-pipeline.js";
|
|
5
|
+
import { resolveExportArea } from "../export/export-area.js";
|
|
6
|
+
import { normalizeExportOptions } from "../export/export-options.js";
|
|
7
|
+
import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
|
|
8
|
+
import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
|
|
9
|
+
import { preflightExport } from "../export/preflight-export.js";
|
|
10
|
+
import { dataUrlToBlob, mimeTypes, renderRaster, renderSvg, withExportView } from "../export/render-export.js";
|
|
5
11
|
import "../document/document-format.js";
|
|
6
12
|
import { createId } from "../document/ids.js";
|
|
7
13
|
import { createDocumentInfo } from "../document/create-document.js";
|
|
8
14
|
import { validateDocument } from "../document/validate-document.js";
|
|
9
|
-
import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
|
|
10
15
|
import { loadIntoCanvas, serializeCanvas } from "../fabric/fabric-adapter.js";
|
|
11
|
-
import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
|
|
12
16
|
import { createObjectRegistry } from "../fabric/object-registry.js";
|
|
13
17
|
import { createHistory } from "../history/create-history.js";
|
|
18
|
+
import { migrateDocument } from "../migrations/migrate-document.js";
|
|
14
19
|
import { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
|
|
20
|
+
import { summarize, supportsVersions, versionsToPrune } from "../versions/document-version.js";
|
|
15
21
|
import { createSaveController } from "../save/save-controller.js";
|
|
16
22
|
import { createEventEmitter } from "./event-emitter.js";
|
|
17
23
|
//#region src/engine/create-document-engine.ts
|
|
@@ -87,6 +93,7 @@ function createDocumentEngine(options) {
|
|
|
87
93
|
onSuccess: (document) => {
|
|
88
94
|
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
89
95
|
events.emit("save:success", { document });
|
|
96
|
+
createAutomaticVersionAfterSave(document);
|
|
90
97
|
},
|
|
91
98
|
onError: (error) => events.emit("save:error", { error }),
|
|
92
99
|
onRetry: (event) => events.emit("save:retry", event)
|
|
@@ -151,7 +158,10 @@ function createDocumentEngine(options) {
|
|
|
151
158
|
if (unknownTypes.length > 0) throw new DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
|
|
152
159
|
return document;
|
|
153
160
|
}
|
|
154
|
-
|
|
161
|
+
function loadDocument(input, loadOptions = {}) {
|
|
162
|
+
return loadMigratedDocument(input, loadOptions, {});
|
|
163
|
+
}
|
|
164
|
+
async function loadMigratedDocument(input, loadOptions, importDetails) {
|
|
155
165
|
ensureUsable();
|
|
156
166
|
activeLoad?.abort();
|
|
157
167
|
const controller = new AbortController();
|
|
@@ -161,7 +171,12 @@ function createDocumentEngine(options) {
|
|
|
161
171
|
try {
|
|
162
172
|
await recovery?.markLoadStarted(documentId);
|
|
163
173
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
164
|
-
const
|
|
174
|
+
const { document: migrated, migratedFrom } = migrateDocument(input, {
|
|
175
|
+
canvasWidth: canvas.getWidth(),
|
|
176
|
+
canvasHeight: canvas.getHeight(),
|
|
177
|
+
...importDetails
|
|
178
|
+
});
|
|
179
|
+
const checked = checkDocument(migrated);
|
|
165
180
|
const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
166
181
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
167
182
|
await history.withoutRecording(() => loadIntoCanvas(canvas, {
|
|
@@ -188,7 +203,8 @@ function createDocumentEngine(options) {
|
|
|
188
203
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
189
204
|
events.emit("load:success", {
|
|
190
205
|
document,
|
|
191
|
-
warnings
|
|
206
|
+
warnings,
|
|
207
|
+
migratedFrom
|
|
192
208
|
});
|
|
193
209
|
return document;
|
|
194
210
|
} catch (error) {
|
|
@@ -202,6 +218,90 @@ function createDocumentEngine(options) {
|
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
}
|
|
221
|
+
function importFabricJson(json, importOptions = {}) {
|
|
222
|
+
let parsed = json;
|
|
223
|
+
if (typeof json === "string") try {
|
|
224
|
+
parsed = JSON.parse(json);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
227
|
+
return Promise.reject(new DocumentEngineError("INVALID_DOCUMENT", `The text is not valid JSON: ${reason}`, { cause: error }));
|
|
228
|
+
}
|
|
229
|
+
const { id, metadata, ...loadOptions } = importOptions;
|
|
230
|
+
return loadMigratedDocument(parsed, loadOptions, {
|
|
231
|
+
id,
|
|
232
|
+
metadata
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
let savesSinceAutomaticVersion = 0;
|
|
236
|
+
let lastVersionTime = 0;
|
|
237
|
+
function nextVersionTimestamp() {
|
|
238
|
+
lastVersionTime = Math.max(Date.now(), lastVersionTime + 1);
|
|
239
|
+
return new Date(lastVersionTime).toISOString();
|
|
240
|
+
}
|
|
241
|
+
function requireVersions() {
|
|
242
|
+
ensureUsable();
|
|
243
|
+
if (!supportsVersions(storage)) throw new DocumentEngineError("VERSIONS_UNSUPPORTED", "The storage adapter needs saveVersion, listVersions, loadVersion and deleteVersion to keep versions");
|
|
244
|
+
return storage;
|
|
245
|
+
}
|
|
246
|
+
async function pruneAutomaticVersions(versionStorage, documentId) {
|
|
247
|
+
const keepAuto = options.versions?.keepAuto ?? 20;
|
|
248
|
+
const existing = await versionStorage.listVersions(documentId);
|
|
249
|
+
await Promise.all(versionsToPrune(existing, keepAuto).map((version) => versionStorage.deleteVersion(documentId, version.id)));
|
|
250
|
+
}
|
|
251
|
+
async function storeVersion(name, kind, document) {
|
|
252
|
+
const versionStorage = requireVersions();
|
|
253
|
+
const content = document ?? (await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls)).document;
|
|
254
|
+
const version = {
|
|
255
|
+
id: createId(),
|
|
256
|
+
documentId: content.id,
|
|
257
|
+
name,
|
|
258
|
+
kind,
|
|
259
|
+
createdAt: nextVersionTimestamp(),
|
|
260
|
+
revision: content.revision ?? saving.state().revision,
|
|
261
|
+
document: content
|
|
262
|
+
};
|
|
263
|
+
await versionStorage.saveVersion(version);
|
|
264
|
+
await pruneAutomaticVersions(versionStorage, content.id);
|
|
265
|
+
const summary = summarize(version);
|
|
266
|
+
events.emit("version:created", summary);
|
|
267
|
+
return summary;
|
|
268
|
+
}
|
|
269
|
+
function reportVersionError(error) {
|
|
270
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
271
|
+
const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("VERSION_FAILED", `Could not keep an automatic version: ${reason}`, { cause: error });
|
|
272
|
+
events.emit("version:error", { error: engineError });
|
|
273
|
+
}
|
|
274
|
+
function createAutomaticVersionAfterSave(document) {
|
|
275
|
+
const every = options.versions?.autoEvery ?? 0;
|
|
276
|
+
if (every <= 0 || !supportsVersions(storage)) return;
|
|
277
|
+
savesSinceAutomaticVersion += 1;
|
|
278
|
+
if (savesSinceAutomaticVersion < every) return;
|
|
279
|
+
savesSinceAutomaticVersion = 0;
|
|
280
|
+
storeVersion(`Autosave ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "auto", document).catch(reportVersionError);
|
|
281
|
+
}
|
|
282
|
+
async function restoreVersion(versionId) {
|
|
283
|
+
const versionStorage = requireVersions();
|
|
284
|
+
const current = documentInfo;
|
|
285
|
+
const version = await versionStorage.loadVersion(current.id, versionId);
|
|
286
|
+
await storeVersion(`Before restoring "${version.name}"`, "auto");
|
|
287
|
+
const { document: migrated } = migrateDocument(version.document, {
|
|
288
|
+
canvasWidth: canvas.getWidth(),
|
|
289
|
+
canvasHeight: canvas.getHeight()
|
|
290
|
+
});
|
|
291
|
+
const baseRevision = saving.state().revision;
|
|
292
|
+
const loaded = await loadDocument({
|
|
293
|
+
...migrated,
|
|
294
|
+
id: current.id,
|
|
295
|
+
createdAt: current.createdAt,
|
|
296
|
+
revision: baseRevision
|
|
297
|
+
});
|
|
298
|
+
noteContentChange();
|
|
299
|
+
events.emit("version:restored", {
|
|
300
|
+
version: summarize(version),
|
|
301
|
+
document: loaded
|
|
302
|
+
});
|
|
303
|
+
return loaded;
|
|
304
|
+
}
|
|
205
305
|
function requireStorage() {
|
|
206
306
|
if (!storage) throw new DocumentEngineError("STORAGE_MISSING", "Pass a storage adapter to createDocumentEngine to use load and save");
|
|
207
307
|
return storage;
|
|
@@ -218,7 +318,7 @@ function createDocumentEngine(options) {
|
|
|
218
318
|
events.emit("load:error", { error: engineError });
|
|
219
319
|
throw engineError;
|
|
220
320
|
}
|
|
221
|
-
return
|
|
321
|
+
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
|
|
222
322
|
}
|
|
223
323
|
function save(saveOptions) {
|
|
224
324
|
try {
|
|
@@ -270,6 +370,92 @@ function createDocumentEngine(options) {
|
|
|
270
370
|
canvas.requestRenderAll();
|
|
271
371
|
return images.length;
|
|
272
372
|
}
|
|
373
|
+
function fontsOnCanvas() {
|
|
374
|
+
rebuildIndex();
|
|
375
|
+
return buildAssetManifest(serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
|
|
376
|
+
}
|
|
377
|
+
async function checkBeforeExport(exportOptions) {
|
|
378
|
+
ensureUsable();
|
|
379
|
+
const { format } = normalizeExportOptions(exportOptions);
|
|
380
|
+
return preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
|
|
381
|
+
}
|
|
382
|
+
function describeProblems(check) {
|
|
383
|
+
return check.problems.map((problem) => problem.message).join("; ");
|
|
384
|
+
}
|
|
385
|
+
async function exportContent(exportOptions) {
|
|
386
|
+
const settings = normalizeExportOptions(exportOptions);
|
|
387
|
+
const stopIfCancelled = () => {
|
|
388
|
+
if (settings.signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
|
|
389
|
+
};
|
|
390
|
+
stopIfCancelled();
|
|
391
|
+
if (settings.format === "json") {
|
|
392
|
+
const prepared = await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
|
|
393
|
+
stopIfCancelled();
|
|
394
|
+
return {
|
|
395
|
+
format: "json",
|
|
396
|
+
mimeType: mimeTypes.json,
|
|
397
|
+
blob: new Blob([JSON.stringify(prepared.document)], { type: mimeTypes.json }),
|
|
398
|
+
width: prepared.document.canvas.width,
|
|
399
|
+
height: prepared.document.canvas.height,
|
|
400
|
+
warnings: prepared.warnings,
|
|
401
|
+
document: prepared.document
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const check = await checkBeforeExport(settings);
|
|
405
|
+
stopIfCancelled();
|
|
406
|
+
if (!check.ok) throw new DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
|
|
407
|
+
const format = settings.format;
|
|
408
|
+
try {
|
|
409
|
+
const rendered = withExportView(canvas, settings.background, format, () => {
|
|
410
|
+
const area = resolveExportArea(canvas, settings.area, settings.padding);
|
|
411
|
+
return {
|
|
412
|
+
area,
|
|
413
|
+
content: format === "svg" ? renderSvg(canvas, area, settings.scale) : renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
const blob = format === "svg" ? new Blob([rendered.content], { type: mimeTypes.svg }) : dataUrlToBlob(rendered.content);
|
|
417
|
+
return {
|
|
418
|
+
format,
|
|
419
|
+
mimeType: blob.type || mimeTypes[format],
|
|
420
|
+
blob,
|
|
421
|
+
width: Math.round(rendered.area.width * settings.scale),
|
|
422
|
+
height: Math.round(rendered.area.height * settings.scale),
|
|
423
|
+
warnings: check.warnings
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (isDocumentEngineError(error)) throw error;
|
|
427
|
+
if (error?.name === "SecurityError") {
|
|
428
|
+
const problem = {
|
|
429
|
+
code: "CROSS_ORIGIN_IMAGE",
|
|
430
|
+
message: "The browser blocked the export because the canvas shows an image from another site without CORS",
|
|
431
|
+
objectIds: []
|
|
432
|
+
};
|
|
433
|
+
throw new DocumentEngineError("EXPORT_BLOCKED", problem.message, {
|
|
434
|
+
problems: [problem],
|
|
435
|
+
cause: error
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
439
|
+
throw new DocumentEngineError("EXPORT_FAILED", `Fabric could not export the canvas: ${reason}`, { cause: error });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function exportDocument(exportOptions) {
|
|
443
|
+
ensureUsable();
|
|
444
|
+
try {
|
|
445
|
+
const result = await exportContent(exportOptions);
|
|
446
|
+
events.emit("export:success", {
|
|
447
|
+
format: result.format,
|
|
448
|
+
width: result.width,
|
|
449
|
+
height: result.height,
|
|
450
|
+
warnings: result.warnings
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
|
|
455
|
+
events.emit("export:error", { error: engineError });
|
|
456
|
+
throw engineError;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
273
459
|
function requireRecovery() {
|
|
274
460
|
ensureUsable();
|
|
275
461
|
if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
@@ -329,6 +515,11 @@ function createDocumentEngine(options) {
|
|
|
329
515
|
toDocument,
|
|
330
516
|
loadDocument,
|
|
331
517
|
load,
|
|
518
|
+
importFabricJson,
|
|
519
|
+
createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
|
|
520
|
+
listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
|
|
521
|
+
restoreVersion,
|
|
522
|
+
deleteVersion: async (versionId) => requireVersions().deleteVersion(documentInfo.id, versionId),
|
|
332
523
|
save,
|
|
333
524
|
isDirty: () => saving.state().isDirty,
|
|
334
525
|
getSaveState: () => saving.state(),
|
|
@@ -343,6 +534,8 @@ function createDocumentEngine(options) {
|
|
|
343
534
|
},
|
|
344
535
|
checkAssets,
|
|
345
536
|
replaceImage,
|
|
537
|
+
export: exportDocument,
|
|
538
|
+
preflightExport: checkBeforeExport,
|
|
346
539
|
flushRecovery: async () => requireRecovery().flush(),
|
|
347
540
|
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
348
541
|
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -8,6 +8,8 @@ var DocumentEngineError = class extends Error {
|
|
|
8
8
|
this.unknownTypes = details.unknownTypes ?? [];
|
|
9
9
|
this.missingAssets = details.missingAssets ?? [];
|
|
10
10
|
this.missingFonts = details.missingFonts ?? [];
|
|
11
|
+
this.problems = details.problems ?? [];
|
|
12
|
+
this.migrationFrom = details.migrationFrom;
|
|
11
13
|
this.cause = details.cause;
|
|
12
14
|
this.retryable = details.retryable ?? false;
|
|
13
15
|
}
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.cjs";
|
|
2
|
+
import { ExportProblem } from "../export/preflight-export.cjs";
|
|
2
3
|
//#region src/engine/errors.d.ts
|
|
3
|
-
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "ENGINE_DESTROYED";
|
|
4
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
|
|
4
5
|
export interface DocumentIssue {
|
|
5
6
|
code: DocumentErrorCode;
|
|
6
7
|
path: string;
|
|
@@ -11,6 +12,8 @@ export interface DocumentEngineErrorDetails {
|
|
|
11
12
|
unknownTypes?: string[];
|
|
12
13
|
missingAssets?: ImageAsset[];
|
|
13
14
|
missingFonts?: FontAsset[];
|
|
15
|
+
problems?: ExportProblem[];
|
|
16
|
+
migrationFrom?: number;
|
|
14
17
|
cause?: unknown;
|
|
15
18
|
retryable?: boolean;
|
|
16
19
|
}
|
|
@@ -20,6 +23,8 @@ export declare class DocumentEngineError extends Error {
|
|
|
20
23
|
readonly unknownTypes: string[];
|
|
21
24
|
readonly missingAssets: ImageAsset[];
|
|
22
25
|
readonly missingFonts: FontAsset[];
|
|
26
|
+
readonly problems: ExportProblem[];
|
|
27
|
+
readonly migrationFrom: number | undefined;
|
|
23
28
|
readonly cause: unknown;
|
|
24
29
|
readonly retryable: boolean;
|
|
25
30
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.js";
|
|
2
|
+
import { ExportProblem } from "../export/preflight-export.js";
|
|
2
3
|
//#region src/engine/errors.d.ts
|
|
3
|
-
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "ENGINE_DESTROYED";
|
|
4
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
|
|
4
5
|
export interface DocumentIssue {
|
|
5
6
|
code: DocumentErrorCode;
|
|
6
7
|
path: string;
|
|
@@ -11,6 +12,8 @@ export interface DocumentEngineErrorDetails {
|
|
|
11
12
|
unknownTypes?: string[];
|
|
12
13
|
missingAssets?: ImageAsset[];
|
|
13
14
|
missingFonts?: FontAsset[];
|
|
15
|
+
problems?: ExportProblem[];
|
|
16
|
+
migrationFrom?: number;
|
|
14
17
|
cause?: unknown;
|
|
15
18
|
retryable?: boolean;
|
|
16
19
|
}
|
|
@@ -20,6 +23,8 @@ export declare class DocumentEngineError extends Error {
|
|
|
20
23
|
readonly unknownTypes: string[];
|
|
21
24
|
readonly missingAssets: ImageAsset[];
|
|
22
25
|
readonly missingFonts: FontAsset[];
|
|
26
|
+
readonly problems: ExportProblem[];
|
|
27
|
+
readonly migrationFrom: number | undefined;
|
|
23
28
|
readonly cause: unknown;
|
|
24
29
|
readonly retryable: boolean;
|
|
25
30
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
package/dist/engine/errors.js
CHANGED
|
@@ -8,6 +8,8 @@ var DocumentEngineError = class extends Error {
|
|
|
8
8
|
this.unknownTypes = details.unknownTypes ?? [];
|
|
9
9
|
this.missingAssets = details.missingAssets ?? [];
|
|
10
10
|
this.missingFonts = details.missingFonts ?? [];
|
|
11
|
+
this.problems = details.problems ?? [];
|
|
12
|
+
this.migrationFrom = details.migrationFrom;
|
|
11
13
|
this.cause = details.cause;
|
|
12
14
|
this.retryable = details.retryable ?? false;
|
|
13
15
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/export/download-export.ts
|
|
2
|
+
function downloadExport(result, fileName = `document.${result.format === "jpeg" ? "jpg" : result.format}`) {
|
|
3
|
+
const url = URL.createObjectURL(result.blob);
|
|
4
|
+
const link = document.createElement("a");
|
|
5
|
+
link.href = url;
|
|
6
|
+
link.download = fileName;
|
|
7
|
+
link.style.display = "none";
|
|
8
|
+
document.body.append(link);
|
|
9
|
+
link.click();
|
|
10
|
+
link.remove();
|
|
11
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
exports.downloadExport = downloadExport;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/export/download-export.ts
|
|
2
|
+
function downloadExport(result, fileName = `document.${result.format === "jpeg" ? "jpg" : result.format}`) {
|
|
3
|
+
const url = URL.createObjectURL(result.blob);
|
|
4
|
+
const link = document.createElement("a");
|
|
5
|
+
link.href = url;
|
|
6
|
+
link.download = fileName;
|
|
7
|
+
link.style.display = "none";
|
|
8
|
+
document.body.append(link);
|
|
9
|
+
link.click();
|
|
10
|
+
link.remove();
|
|
11
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { downloadExport };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
//#region src/export/export-area.ts
|
|
3
|
+
function boundsOf(objects) {
|
|
4
|
+
if (objects.length === 0) return null;
|
|
5
|
+
let left = Infinity;
|
|
6
|
+
let top = Infinity;
|
|
7
|
+
let right = -Infinity;
|
|
8
|
+
let bottom = -Infinity;
|
|
9
|
+
for (const object of objects) {
|
|
10
|
+
const box = object.getBoundingRect();
|
|
11
|
+
left = Math.min(left, box.left);
|
|
12
|
+
top = Math.min(top, box.top);
|
|
13
|
+
right = Math.max(right, box.left + box.width);
|
|
14
|
+
bottom = Math.max(bottom, box.top + box.height);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
left,
|
|
18
|
+
top,
|
|
19
|
+
width: right - left,
|
|
20
|
+
height: bottom - top
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function padAndRound(rect, padding) {
|
|
24
|
+
const left = Math.floor(rect.left - padding);
|
|
25
|
+
const top = Math.floor(rect.top - padding);
|
|
26
|
+
return {
|
|
27
|
+
left,
|
|
28
|
+
top,
|
|
29
|
+
width: Math.max(1, Math.ceil(rect.left + rect.width + padding) - left),
|
|
30
|
+
height: Math.max(1, Math.ceil(rect.top + rect.height + padding) - top)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function resolveExportArea(canvas, area, padding) {
|
|
34
|
+
if (typeof area === "object") return padAndRound(area, padding);
|
|
35
|
+
if (area === "canvas") return {
|
|
36
|
+
left: 0,
|
|
37
|
+
top: 0,
|
|
38
|
+
width: canvas.getWidth(),
|
|
39
|
+
height: canvas.getHeight()
|
|
40
|
+
};
|
|
41
|
+
const bounds = boundsOf(area === "selection" ? canvas.getActiveObjects?.() ?? [] : canvas.getObjects());
|
|
42
|
+
if (bounds === null) throw new require_errors.DocumentEngineError("INVALID_EXPORT_OPTIONS", `Cannot export the ${area} because ${area === "selection" ? "nothing is selected" : "the canvas is empty"}`);
|
|
43
|
+
return padAndRound(bounds, padding);
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.resolveExportArea = resolveExportArea;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
//#region src/export/export-area.ts
|
|
3
|
+
function boundsOf(objects) {
|
|
4
|
+
if (objects.length === 0) return null;
|
|
5
|
+
let left = Infinity;
|
|
6
|
+
let top = Infinity;
|
|
7
|
+
let right = -Infinity;
|
|
8
|
+
let bottom = -Infinity;
|
|
9
|
+
for (const object of objects) {
|
|
10
|
+
const box = object.getBoundingRect();
|
|
11
|
+
left = Math.min(left, box.left);
|
|
12
|
+
top = Math.min(top, box.top);
|
|
13
|
+
right = Math.max(right, box.left + box.width);
|
|
14
|
+
bottom = Math.max(bottom, box.top + box.height);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
left,
|
|
18
|
+
top,
|
|
19
|
+
width: right - left,
|
|
20
|
+
height: bottom - top
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function padAndRound(rect, padding) {
|
|
24
|
+
const left = Math.floor(rect.left - padding);
|
|
25
|
+
const top = Math.floor(rect.top - padding);
|
|
26
|
+
return {
|
|
27
|
+
left,
|
|
28
|
+
top,
|
|
29
|
+
width: Math.max(1, Math.ceil(rect.left + rect.width + padding) - left),
|
|
30
|
+
height: Math.max(1, Math.ceil(rect.top + rect.height + padding) - top)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function resolveExportArea(canvas, area, padding) {
|
|
34
|
+
if (typeof area === "object") return padAndRound(area, padding);
|
|
35
|
+
if (area === "canvas") return {
|
|
36
|
+
left: 0,
|
|
37
|
+
top: 0,
|
|
38
|
+
width: canvas.getWidth(),
|
|
39
|
+
height: canvas.getHeight()
|
|
40
|
+
};
|
|
41
|
+
const bounds = boundsOf(area === "selection" ? canvas.getActiveObjects?.() ?? [] : canvas.getObjects());
|
|
42
|
+
if (bounds === null) throw new DocumentEngineError("INVALID_EXPORT_OPTIONS", `Cannot export the ${area} because ${area === "selection" ? "nothing is selected" : "the canvas is empty"}`);
|
|
43
|
+
return padAndRound(bounds, padding);
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { resolveExportArea };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
//#region src/export/export-options.ts
|
|
3
|
+
const formats = /* @__PURE__ */ new Set([
|
|
4
|
+
"png",
|
|
5
|
+
"jpeg",
|
|
6
|
+
"webp",
|
|
7
|
+
"svg",
|
|
8
|
+
"json"
|
|
9
|
+
]);
|
|
10
|
+
function invalid(message) {
|
|
11
|
+
return new require_errors.DocumentEngineError("INVALID_EXPORT_OPTIONS", message);
|
|
12
|
+
}
|
|
13
|
+
function isPositive(value) {
|
|
14
|
+
return Number.isFinite(value) && value > 0;
|
|
15
|
+
}
|
|
16
|
+
function isRasterFormat(format) {
|
|
17
|
+
return format === "png" || format === "jpeg" || format === "webp";
|
|
18
|
+
}
|
|
19
|
+
function normalizeExportOptions(options) {
|
|
20
|
+
if (!formats.has(options.format)) throw invalid(`Unknown export format "${String(options.format)}", use png, jpeg, webp, svg or json`);
|
|
21
|
+
const scale = options.scale ?? 1;
|
|
22
|
+
if (!isPositive(scale)) throw invalid("scale must be a number greater than zero");
|
|
23
|
+
const quality = options.quality ?? .92;
|
|
24
|
+
if (!(quality >= 0 && quality <= 1)) throw invalid("quality must be between 0 and 1");
|
|
25
|
+
const padding = options.padding ?? 0;
|
|
26
|
+
if (!(Number.isFinite(padding) && padding >= 0)) throw invalid("padding must be zero or more");
|
|
27
|
+
const area = options.area ?? "canvas";
|
|
28
|
+
if (typeof area === "object") {
|
|
29
|
+
if (![area.left, area.top].every(Number.isFinite) || !isPositive(area.width) || !isPositive(area.height)) throw invalid("area needs finite left and top and a positive width and height");
|
|
30
|
+
} else if (![
|
|
31
|
+
"canvas",
|
|
32
|
+
"content",
|
|
33
|
+
"selection"
|
|
34
|
+
].includes(area)) throw invalid(`Unknown export area "${String(area)}", use canvas, content, selection or a rectangle`);
|
|
35
|
+
return {
|
|
36
|
+
format: options.format,
|
|
37
|
+
scale,
|
|
38
|
+
quality,
|
|
39
|
+
area,
|
|
40
|
+
padding,
|
|
41
|
+
background: options.background ?? "keep",
|
|
42
|
+
signal: options.signal
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
exports.isRasterFormat = isRasterFormat;
|
|
47
|
+
exports.normalizeExportOptions = normalizeExportOptions;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/export/export-options.d.ts
|
|
2
|
+
export type ExportFormat = "png" | "jpeg" | "webp" | "svg" | "json";
|
|
3
|
+
export interface ExportRect {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export type ExportArea = "canvas" | "content" | "selection" | ExportRect;
|
|
10
|
+
export type ExportBackground = "keep" | "transparent" | (string & {});
|
|
11
|
+
export interface ExportOptions {
|
|
12
|
+
format: ExportFormat;
|
|
13
|
+
scale?: number;
|
|
14
|
+
quality?: number;
|
|
15
|
+
area?: ExportArea;
|
|
16
|
+
padding?: number;
|
|
17
|
+
background?: ExportBackground;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/export/export-options.d.ts
|
|
2
|
+
export type ExportFormat = "png" | "jpeg" | "webp" | "svg" | "json";
|
|
3
|
+
export interface ExportRect {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export type ExportArea = "canvas" | "content" | "selection" | ExportRect;
|
|
10
|
+
export type ExportBackground = "keep" | "transparent" | (string & {});
|
|
11
|
+
export interface ExportOptions {
|
|
12
|
+
format: ExportFormat;
|
|
13
|
+
scale?: number;
|
|
14
|
+
quality?: number;
|
|
15
|
+
area?: ExportArea;
|
|
16
|
+
padding?: number;
|
|
17
|
+
background?: ExportBackground;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
//#region src/export/export-options.ts
|
|
3
|
+
const formats = /* @__PURE__ */ new Set([
|
|
4
|
+
"png",
|
|
5
|
+
"jpeg",
|
|
6
|
+
"webp",
|
|
7
|
+
"svg",
|
|
8
|
+
"json"
|
|
9
|
+
]);
|
|
10
|
+
function invalid(message) {
|
|
11
|
+
return new DocumentEngineError("INVALID_EXPORT_OPTIONS", message);
|
|
12
|
+
}
|
|
13
|
+
function isPositive(value) {
|
|
14
|
+
return Number.isFinite(value) && value > 0;
|
|
15
|
+
}
|
|
16
|
+
function isRasterFormat(format) {
|
|
17
|
+
return format === "png" || format === "jpeg" || format === "webp";
|
|
18
|
+
}
|
|
19
|
+
function normalizeExportOptions(options) {
|
|
20
|
+
if (!formats.has(options.format)) throw invalid(`Unknown export format "${String(options.format)}", use png, jpeg, webp, svg or json`);
|
|
21
|
+
const scale = options.scale ?? 1;
|
|
22
|
+
if (!isPositive(scale)) throw invalid("scale must be a number greater than zero");
|
|
23
|
+
const quality = options.quality ?? .92;
|
|
24
|
+
if (!(quality >= 0 && quality <= 1)) throw invalid("quality must be between 0 and 1");
|
|
25
|
+
const padding = options.padding ?? 0;
|
|
26
|
+
if (!(Number.isFinite(padding) && padding >= 0)) throw invalid("padding must be zero or more");
|
|
27
|
+
const area = options.area ?? "canvas";
|
|
28
|
+
if (typeof area === "object") {
|
|
29
|
+
if (![area.left, area.top].every(Number.isFinite) || !isPositive(area.width) || !isPositive(area.height)) throw invalid("area needs finite left and top and a positive width and height");
|
|
30
|
+
} else if (![
|
|
31
|
+
"canvas",
|
|
32
|
+
"content",
|
|
33
|
+
"selection"
|
|
34
|
+
].includes(area)) throw invalid(`Unknown export area "${String(area)}", use canvas, content, selection or a rectangle`);
|
|
35
|
+
return {
|
|
36
|
+
format: options.format,
|
|
37
|
+
scale,
|
|
38
|
+
quality,
|
|
39
|
+
area,
|
|
40
|
+
padding,
|
|
41
|
+
background: options.background ?? "keep",
|
|
42
|
+
signal: options.signal
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { isRasterFormat, normalizeExportOptions };
|