fabricjs-document-engine 0.3.0 → 0.5.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 +88 -3
- package/dist/assets/asset-pipeline.cjs +1 -0
- package/dist/assets/asset-pipeline.js +1 -1
- package/dist/engine/create-document-engine.cjs +150 -7
- package/dist/engine/create-document-engine.d.cts +40 -0
- package/dist/engine/create-document-engine.d.ts +40 -0
- package/dist/engine/create-document-engine.js +150 -7
- package/dist/engine/errors.cjs +1 -0
- package/dist/engine/errors.d.cts +4 -1
- package/dist/engine/errors.d.ts +4 -1
- package/dist/engine/errors.js +1 -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 +2 -0
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +2 -1
- package/dist/recovery/indexed-db-recovery.cjs +80 -0
- package/dist/recovery/indexed-db-recovery.d.cts +8 -0
- package/dist/recovery/indexed-db-recovery.d.ts +8 -0
- package/dist/recovery/indexed-db-recovery.js +80 -0
- package/dist/recovery/recovery-controller.cjs +159 -0
- package/dist/recovery/recovery-controller.d.cts +19 -0
- package/dist/recovery/recovery-controller.d.ts +19 -0
- package/dist/recovery/recovery-controller.js +158 -0
- package/dist/recovery/recovery-store.cjs +19 -0
- package/dist/recovery/recovery-store.d.cts +10 -0
- package/dist/recovery/recovery-store.d.ts +10 -0
- package/dist/recovery/recovery-store.js +19 -0
- package/dist/recovery.cjs +5 -0
- package/dist/recovery.d.cts +4 -0
- package/dist/recovery.d.ts +4 -0
- package/dist/recovery.js +3 -0
- package/package.json +13 -2
|
@@ -2,15 +2,20 @@ 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 { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
|
|
14
19
|
import { createSaveController } from "../save/save-controller.js";
|
|
15
20
|
import { createEventEmitter } from "./event-emitter.js";
|
|
16
21
|
//#region src/engine/create-document-engine.ts
|
|
@@ -70,7 +75,7 @@ function createDocumentEngine(options) {
|
|
|
70
75
|
return serializeCanvas(canvas, registry.propertiesToInclude()).objects;
|
|
71
76
|
},
|
|
72
77
|
onChange: (state) => events.emit("history:change", state),
|
|
73
|
-
onContentChange: () =>
|
|
78
|
+
onContentChange: () => noteContentChange()
|
|
74
79
|
});
|
|
75
80
|
const saving = createSaveController({
|
|
76
81
|
getStorage: () => requireStorage(),
|
|
@@ -83,10 +88,30 @@ function createDocumentEngine(options) {
|
|
|
83
88
|
autosave: options.autosave === true ? {} : options.autosave ?? false,
|
|
84
89
|
onStateChange: (state) => events.emit("save:status", state),
|
|
85
90
|
onStart: (document) => events.emit("save:start", { document }),
|
|
86
|
-
onSuccess: (document) =>
|
|
91
|
+
onSuccess: (document) => {
|
|
92
|
+
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
93
|
+
events.emit("save:success", { document });
|
|
94
|
+
},
|
|
87
95
|
onError: (error) => events.emit("save:error", { error }),
|
|
88
96
|
onRetry: (event) => events.emit("save:retry", event)
|
|
89
97
|
});
|
|
98
|
+
const recovery = options.recovery ? createRecoveryController({
|
|
99
|
+
...options.recovery,
|
|
100
|
+
createDocument: () => toDocument(),
|
|
101
|
+
shouldWrite: () => !destroyed && saving.state().isDirty,
|
|
102
|
+
onCheckpoint: (record) => events.emit("recovery:checkpoint", {
|
|
103
|
+
documentId: record.documentId,
|
|
104
|
+
savedAt: record.savedAt
|
|
105
|
+
}),
|
|
106
|
+
onError: (error) => {
|
|
107
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
108
|
+
events.emit("recovery:error", { error: new DocumentEngineError("RECOVERY_FAILED", `Could not write the recovery copy: ${reason}`, { cause: error }) });
|
|
109
|
+
}
|
|
110
|
+
}) : void 0;
|
|
111
|
+
function noteContentChange() {
|
|
112
|
+
saving.noteContentChange();
|
|
113
|
+
recovery?.schedule();
|
|
114
|
+
}
|
|
90
115
|
function toDocument() {
|
|
91
116
|
ensureUsable();
|
|
92
117
|
rebuildIndex();
|
|
@@ -135,8 +160,11 @@ function createDocumentEngine(options) {
|
|
|
135
160
|
activeLoad?.abort();
|
|
136
161
|
const controller = new AbortController();
|
|
137
162
|
activeLoad = controller;
|
|
138
|
-
|
|
163
|
+
const documentId = describeDocumentId(input);
|
|
164
|
+
events.emit("load:start", { documentId });
|
|
139
165
|
try {
|
|
166
|
+
await recovery?.markLoadStarted(documentId);
|
|
167
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
140
168
|
const checked = checkDocument(input);
|
|
141
169
|
const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
142
170
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
@@ -158,6 +186,7 @@ function createDocumentEngine(options) {
|
|
|
158
186
|
};
|
|
159
187
|
rebuildIndex();
|
|
160
188
|
history.reset();
|
|
189
|
+
recovery?.cancel();
|
|
161
190
|
saving.startSession(document.revision ?? 0);
|
|
162
191
|
canvas.requestRenderAll();
|
|
163
192
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
@@ -171,7 +200,10 @@ function createDocumentEngine(options) {
|
|
|
171
200
|
events.emit("load:error", { error: engineError });
|
|
172
201
|
throw engineError;
|
|
173
202
|
} finally {
|
|
174
|
-
if (activeLoad === controller)
|
|
203
|
+
if (activeLoad === controller) {
|
|
204
|
+
activeLoad = null;
|
|
205
|
+
await recovery?.markLoadFinished();
|
|
206
|
+
}
|
|
175
207
|
}
|
|
176
208
|
}
|
|
177
209
|
function requireStorage() {
|
|
@@ -208,6 +240,7 @@ function createDocumentEngine(options) {
|
|
|
208
240
|
documentInfo = createDocumentInfo(newOptions);
|
|
209
241
|
uploadedUrls.clear();
|
|
210
242
|
history.reset();
|
|
243
|
+
recovery?.cancel();
|
|
211
244
|
saving.startSession(0);
|
|
212
245
|
}
|
|
213
246
|
function getObjectById(id) {
|
|
@@ -241,6 +274,107 @@ function createDocumentEngine(options) {
|
|
|
241
274
|
canvas.requestRenderAll();
|
|
242
275
|
return images.length;
|
|
243
276
|
}
|
|
277
|
+
function fontsOnCanvas() {
|
|
278
|
+
rebuildIndex();
|
|
279
|
+
return buildAssetManifest(serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
|
|
280
|
+
}
|
|
281
|
+
async function checkBeforeExport(exportOptions) {
|
|
282
|
+
ensureUsable();
|
|
283
|
+
const { format } = normalizeExportOptions(exportOptions);
|
|
284
|
+
return preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
|
|
285
|
+
}
|
|
286
|
+
function describeProblems(check) {
|
|
287
|
+
return check.problems.map((problem) => problem.message).join("; ");
|
|
288
|
+
}
|
|
289
|
+
async function exportContent(exportOptions) {
|
|
290
|
+
const settings = normalizeExportOptions(exportOptions);
|
|
291
|
+
const stopIfCancelled = () => {
|
|
292
|
+
if (settings.signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
|
|
293
|
+
};
|
|
294
|
+
stopIfCancelled();
|
|
295
|
+
if (settings.format === "json") {
|
|
296
|
+
const prepared = await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
|
|
297
|
+
stopIfCancelled();
|
|
298
|
+
return {
|
|
299
|
+
format: "json",
|
|
300
|
+
mimeType: mimeTypes.json,
|
|
301
|
+
blob: new Blob([JSON.stringify(prepared.document)], { type: mimeTypes.json }),
|
|
302
|
+
width: prepared.document.canvas.width,
|
|
303
|
+
height: prepared.document.canvas.height,
|
|
304
|
+
warnings: prepared.warnings,
|
|
305
|
+
document: prepared.document
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const check = await checkBeforeExport(settings);
|
|
309
|
+
stopIfCancelled();
|
|
310
|
+
if (!check.ok) throw new DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
|
|
311
|
+
const format = settings.format;
|
|
312
|
+
try {
|
|
313
|
+
const rendered = withExportView(canvas, settings.background, format, () => {
|
|
314
|
+
const area = resolveExportArea(canvas, settings.area, settings.padding);
|
|
315
|
+
return {
|
|
316
|
+
area,
|
|
317
|
+
content: format === "svg" ? renderSvg(canvas, area, settings.scale) : renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
const blob = format === "svg" ? new Blob([rendered.content], { type: mimeTypes.svg }) : dataUrlToBlob(rendered.content);
|
|
321
|
+
return {
|
|
322
|
+
format,
|
|
323
|
+
mimeType: blob.type || mimeTypes[format],
|
|
324
|
+
blob,
|
|
325
|
+
width: Math.round(rendered.area.width * settings.scale),
|
|
326
|
+
height: Math.round(rendered.area.height * settings.scale),
|
|
327
|
+
warnings: check.warnings
|
|
328
|
+
};
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (isDocumentEngineError(error)) throw error;
|
|
331
|
+
if (error?.name === "SecurityError") {
|
|
332
|
+
const problem = {
|
|
333
|
+
code: "CROSS_ORIGIN_IMAGE",
|
|
334
|
+
message: "The browser blocked the export because the canvas shows an image from another site without CORS",
|
|
335
|
+
objectIds: []
|
|
336
|
+
};
|
|
337
|
+
throw new DocumentEngineError("EXPORT_BLOCKED", problem.message, {
|
|
338
|
+
problems: [problem],
|
|
339
|
+
cause: error
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
343
|
+
throw new DocumentEngineError("EXPORT_FAILED", `Fabric could not export the canvas: ${reason}`, { cause: error });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function exportDocument(exportOptions) {
|
|
347
|
+
ensureUsable();
|
|
348
|
+
try {
|
|
349
|
+
const result = await exportContent(exportOptions);
|
|
350
|
+
events.emit("export:success", {
|
|
351
|
+
format: result.format,
|
|
352
|
+
width: result.width,
|
|
353
|
+
height: result.height,
|
|
354
|
+
warnings: result.warnings
|
|
355
|
+
});
|
|
356
|
+
return result;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
|
|
359
|
+
events.emit("export:error", { error: engineError });
|
|
360
|
+
throw engineError;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function requireRecovery() {
|
|
364
|
+
ensureUsable();
|
|
365
|
+
if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
366
|
+
return recovery;
|
|
367
|
+
}
|
|
368
|
+
async function restoreRecovery(documentId, loadOptions) {
|
|
369
|
+
const source = requireRecovery();
|
|
370
|
+
const id = documentId ?? documentInfo.id;
|
|
371
|
+
const record = await source.read(id);
|
|
372
|
+
if (!record) throw new DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
|
|
373
|
+
const loaded = await loadDocument(await restoreRecordedFiles(record), loadOptions);
|
|
374
|
+
noteContentChange();
|
|
375
|
+
events.emit("recovery:restored", { document: loaded });
|
|
376
|
+
return loaded;
|
|
377
|
+
}
|
|
244
378
|
async function travelThroughHistory(direction) {
|
|
245
379
|
ensureUsable();
|
|
246
380
|
try {
|
|
@@ -258,6 +392,7 @@ function createDocumentEngine(options) {
|
|
|
258
392
|
activeLoad?.abort();
|
|
259
393
|
history.destroy();
|
|
260
394
|
saving.destroy();
|
|
395
|
+
recovery?.destroy();
|
|
261
396
|
canvas.off("object:added", handleObjectAdded);
|
|
262
397
|
canvas.off("object:removed", handleObjectRemoved);
|
|
263
398
|
objectsById.clear();
|
|
@@ -278,7 +413,7 @@ function createDocumentEngine(options) {
|
|
|
278
413
|
...changes
|
|
279
414
|
}
|
|
280
415
|
};
|
|
281
|
-
|
|
416
|
+
noteContentChange();
|
|
282
417
|
},
|
|
283
418
|
newDocument,
|
|
284
419
|
toDocument,
|
|
@@ -298,6 +433,14 @@ function createDocumentEngine(options) {
|
|
|
298
433
|
},
|
|
299
434
|
checkAssets,
|
|
300
435
|
replaceImage,
|
|
436
|
+
export: exportDocument,
|
|
437
|
+
preflightExport: checkBeforeExport,
|
|
438
|
+
flushRecovery: async () => requireRecovery().flush(),
|
|
439
|
+
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
440
|
+
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
441
|
+
restoreRecovery,
|
|
442
|
+
discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
|
|
443
|
+
getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
|
|
301
444
|
transaction(label, work) {
|
|
302
445
|
ensureUsable();
|
|
303
446
|
return history.transaction(label, work);
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -8,6 +8,7 @@ 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 ?? [];
|
|
11
12
|
this.cause = details.cause;
|
|
12
13
|
this.retryable = details.retryable ?? false;
|
|
13
14
|
}
|
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" | "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" | "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,7 @@ export interface DocumentEngineErrorDetails {
|
|
|
11
12
|
unknownTypes?: string[];
|
|
12
13
|
missingAssets?: ImageAsset[];
|
|
13
14
|
missingFonts?: FontAsset[];
|
|
15
|
+
problems?: ExportProblem[];
|
|
14
16
|
cause?: unknown;
|
|
15
17
|
retryable?: boolean;
|
|
16
18
|
}
|
|
@@ -20,6 +22,7 @@ export declare class DocumentEngineError extends Error {
|
|
|
20
22
|
readonly unknownTypes: string[];
|
|
21
23
|
readonly missingAssets: ImageAsset[];
|
|
22
24
|
readonly missingFonts: FontAsset[];
|
|
25
|
+
readonly problems: ExportProblem[];
|
|
23
26
|
readonly cause: unknown;
|
|
24
27
|
readonly retryable: boolean;
|
|
25
28
|
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" | "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" | "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,7 @@ export interface DocumentEngineErrorDetails {
|
|
|
11
12
|
unknownTypes?: string[];
|
|
12
13
|
missingAssets?: ImageAsset[];
|
|
13
14
|
missingFonts?: FontAsset[];
|
|
15
|
+
problems?: ExportProblem[];
|
|
14
16
|
cause?: unknown;
|
|
15
17
|
retryable?: boolean;
|
|
16
18
|
}
|
|
@@ -20,6 +22,7 @@ export declare class DocumentEngineError extends Error {
|
|
|
20
22
|
readonly unknownTypes: string[];
|
|
21
23
|
readonly missingAssets: ImageAsset[];
|
|
22
24
|
readonly missingFonts: FontAsset[];
|
|
25
|
+
readonly problems: ExportProblem[];
|
|
23
26
|
readonly cause: unknown;
|
|
24
27
|
readonly retryable: boolean;
|
|
25
28
|
constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
|
package/dist/engine/errors.js
CHANGED
|
@@ -8,6 +8,7 @@ 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 ?? [];
|
|
11
12
|
this.cause = details.cause;
|
|
12
13
|
this.retryable = details.retryable ?? false;
|
|
13
14
|
}
|
|
@@ -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 };
|