fabricjs-document-engine 1.0.1 → 1.1.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 +277 -36
- package/README.zh-CN.md +697 -0
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +22 -10
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +22 -10
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +88 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +88 -7
- package/dist/commands/clipboard.cjs +136 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +135 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -0
- package/dist/engine/create-document-engine.cjs +85 -14
- package/dist/engine/create-document-engine.d.cts +20 -1
- package/dist/engine/create-document-engine.d.ts +20 -1
- package/dist/engine/create-document-engine.js +86 -15
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +18 -1
- package/dist/export/export-options.d.cts +23 -0
- package/dist/export/export-options.d.ts +23 -0
- package/dist/export/export-options.js +18 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +18 -3
- package/dist/export/render-export.js +18 -4
- package/dist/export/svg/embed-assets.cjs +249 -0
- package/dist/export/svg/embed-assets.js +249 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/text-on-path.cjs +252 -0
- package/dist/export/svg/text-on-path.js +249 -0
- package/dist/fabric/fabric-adapter.cjs +46 -5
- package/dist/fabric/fabric-adapter.js +46 -5
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -1
- package/dist/pdf/export-pdf.cjs +440 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +440 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf/text-decorations.cjs +108 -0
- package/dist/pdf/text-decorations.js +107 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/util/concurrency.cjs +28 -0
- package/dist/util/concurrency.js +27 -0
- package/package.json +55 -14
|
@@ -9,6 +9,8 @@ const require_object_ids = require("../fabric/object-ids.cjs");
|
|
|
9
9
|
const require_walk_objects = require("../fabric/walk-objects.cjs");
|
|
10
10
|
const require_preflight_export = require("../export/preflight-export.cjs");
|
|
11
11
|
const require_render_export = require("../export/render-export.cjs");
|
|
12
|
+
const require_embed_assets = require("../export/svg/embed-assets.cjs");
|
|
13
|
+
const require_svg_import = require("../import/svg-import.cjs");
|
|
12
14
|
require("../document/document-format.cjs");
|
|
13
15
|
const require_ids = require("../document/ids.cjs");
|
|
14
16
|
const require_create_document = require("../document/create-document.cjs");
|
|
@@ -143,8 +145,11 @@ function createDocumentEngine(options) {
|
|
|
143
145
|
if (serialized.background !== void 0) document.canvas.background = serialized.background;
|
|
144
146
|
return document;
|
|
145
147
|
}
|
|
146
|
-
function toLoadError(error,
|
|
147
|
-
if (signal.aborted)
|
|
148
|
+
function toLoadError(error, controller) {
|
|
149
|
+
if (controller.signal.aborted) {
|
|
150
|
+
const reason = cancelledByCaller.has(controller) ? "Loading was cancelled" : destroyed ? "Loading stopped because the engine was destroyed" : "Loading stopped because a newer load started";
|
|
151
|
+
return new require_errors.DocumentEngineError("LOAD_ABORTED", reason, { cause: error });
|
|
152
|
+
}
|
|
148
153
|
if (require_errors.isDocumentEngineError(error)) return error;
|
|
149
154
|
const reason = error instanceof Error ? error.message : String(error);
|
|
150
155
|
return new require_errors.DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
|
|
@@ -167,19 +172,49 @@ function createDocumentEngine(options) {
|
|
|
167
172
|
if (!storage || discardUnsavedChanges || !saving.state().isDirty) return;
|
|
168
173
|
throw new require_errors.DocumentEngineError("UNSAVED_CHANGES", "The current document has unsaved changes. Save first, or pass { discardUnsavedChanges: true } to replace it anyway.");
|
|
169
174
|
}
|
|
170
|
-
|
|
175
|
+
const cancelledByCaller = /* @__PURE__ */ new WeakSet();
|
|
176
|
+
const unlinkCallerSignal = /* @__PURE__ */ new WeakMap();
|
|
177
|
+
function startLoad(callerSignal) {
|
|
171
178
|
activeLoad?.abort();
|
|
172
179
|
const controller = new AbortController();
|
|
173
180
|
activeLoad = controller;
|
|
181
|
+
if (callerSignal) {
|
|
182
|
+
const cancel = () => {
|
|
183
|
+
cancelledByCaller.add(controller);
|
|
184
|
+
controller.abort();
|
|
185
|
+
};
|
|
186
|
+
if (callerSignal.aborted) cancel();
|
|
187
|
+
else {
|
|
188
|
+
callerSignal.addEventListener("abort", cancel, { once: true });
|
|
189
|
+
unlinkCallerSignal.set(controller, () => callerSignal.removeEventListener("abort", cancel));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
174
192
|
return controller;
|
|
175
193
|
}
|
|
194
|
+
function finishLoad(controller) {
|
|
195
|
+
unlinkCallerSignal.get(controller)?.();
|
|
196
|
+
unlinkCallerSignal.delete(controller);
|
|
197
|
+
}
|
|
176
198
|
async function loadMigratedDocument(input, loadOptions, importDetails, startedLoad) {
|
|
177
199
|
ensureUsable();
|
|
178
200
|
protectUnsavedChanges(loadOptions.discardUnsavedChanges);
|
|
179
|
-
const controller = startedLoad ?? startLoad();
|
|
201
|
+
const controller = startedLoad ?? startLoad(loadOptions.signal);
|
|
180
202
|
const documentId = describeDocumentId(input);
|
|
203
|
+
const report = (stage, done, total) => {
|
|
204
|
+
if (controller.signal.aborted) return;
|
|
205
|
+
const progress = {
|
|
206
|
+
documentId,
|
|
207
|
+
stage,
|
|
208
|
+
done,
|
|
209
|
+
total
|
|
210
|
+
};
|
|
211
|
+
loadOptions.onProgress?.(progress);
|
|
212
|
+
events.emit("load:progress", progress);
|
|
213
|
+
};
|
|
181
214
|
events.emit("load:start", { documentId });
|
|
182
215
|
try {
|
|
216
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
217
|
+
report("prepare", 0, 1);
|
|
183
218
|
await recovery?.markLoadStarted(documentId);
|
|
184
219
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
185
220
|
const { document: migrated, migratedFrom } = require_migrate_document.migrateDocument(require_content_limits.secureDocument(input, options.limits), {
|
|
@@ -188,13 +223,17 @@ function createDocumentEngine(options) {
|
|
|
188
223
|
...importDetails
|
|
189
224
|
});
|
|
190
225
|
const checked = checkDocument(migrated);
|
|
191
|
-
|
|
226
|
+
report("prepare", 1, 1);
|
|
227
|
+
const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal, options.limits?.isAllowedUrl, (done, total) => report("images", done, total));
|
|
192
228
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
193
229
|
await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
|
|
194
230
|
version: document.fabricVersion,
|
|
195
231
|
background: document.canvas.background,
|
|
196
232
|
objects: document.objects
|
|
197
|
-
},
|
|
233
|
+
}, {
|
|
234
|
+
signal: controller.signal,
|
|
235
|
+
onObjects: (done, total) => report("objects", done, total)
|
|
236
|
+
}));
|
|
198
237
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
199
238
|
if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
|
|
200
239
|
width: document.canvas.width,
|
|
@@ -211,6 +250,7 @@ function createDocumentEngine(options) {
|
|
|
211
250
|
recovery?.cancel();
|
|
212
251
|
saving.startSession(document.revision ?? 0);
|
|
213
252
|
canvas.requestRenderAll();
|
|
253
|
+
report("done", 1, 1);
|
|
214
254
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
215
255
|
events.emit("document:change", { documentId: document.id });
|
|
216
256
|
events.emit("load:success", {
|
|
@@ -220,10 +260,11 @@ function createDocumentEngine(options) {
|
|
|
220
260
|
});
|
|
221
261
|
return document;
|
|
222
262
|
} catch (error) {
|
|
223
|
-
const engineError = toLoadError(error, controller
|
|
263
|
+
const engineError = toLoadError(error, controller);
|
|
224
264
|
events.emit("load:error", { error: engineError });
|
|
225
265
|
throw engineError;
|
|
226
266
|
} finally {
|
|
267
|
+
finishLoad(controller);
|
|
227
268
|
if (activeLoad === controller) {
|
|
228
269
|
activeLoad = null;
|
|
229
270
|
await recovery?.markLoadFinished();
|
|
@@ -244,6 +285,16 @@ function createDocumentEngine(options) {
|
|
|
244
285
|
metadata
|
|
245
286
|
});
|
|
246
287
|
}
|
|
288
|
+
async function importSvg(svg, importOptions = {}) {
|
|
289
|
+
ensureUsable();
|
|
290
|
+
const result = await require_svg_import.readSvg(svg, importOptions, options.limits);
|
|
291
|
+
ensureUsable();
|
|
292
|
+
if (result.objects.length > 0) {
|
|
293
|
+
history.transaction("Import SVG", () => canvas.add(...result.objects));
|
|
294
|
+
canvas.requestRenderAll();
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
247
298
|
let savesSinceAutomaticVersion = 0;
|
|
248
299
|
let lastVersionTime = 0;
|
|
249
300
|
function nextVersionTimestamp() {
|
|
@@ -322,15 +373,17 @@ function createDocumentEngine(options) {
|
|
|
322
373
|
ensureUsable();
|
|
323
374
|
const source = requireStorage();
|
|
324
375
|
protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
|
|
325
|
-
const controller = startLoad();
|
|
376
|
+
const controller = startLoad(loadOptions?.signal);
|
|
326
377
|
let stored;
|
|
327
378
|
try {
|
|
379
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
328
380
|
stored = await source.loadDocument(documentId);
|
|
329
381
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
330
382
|
} catch (error) {
|
|
383
|
+
finishLoad(controller);
|
|
331
384
|
if (activeLoad === controller) activeLoad = null;
|
|
332
385
|
if (controller.signal.aborted) {
|
|
333
|
-
const aborted = toLoadError(error, controller
|
|
386
|
+
const aborted = toLoadError(error, controller);
|
|
334
387
|
events.emit("load:error", { error: aborted });
|
|
335
388
|
throw aborted;
|
|
336
389
|
}
|
|
@@ -400,8 +453,8 @@ function createDocumentEngine(options) {
|
|
|
400
453
|
}
|
|
401
454
|
async function checkBeforeExport(exportOptions) {
|
|
402
455
|
ensureUsable();
|
|
403
|
-
const { format } = require_export_options.normalizeExportOptions(exportOptions);
|
|
404
|
-
return require_preflight_export.preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
|
|
456
|
+
const { format, svg } = require_export_options.normalizeExportOptions(exportOptions);
|
|
457
|
+
return require_preflight_export.preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions, svg);
|
|
405
458
|
}
|
|
406
459
|
function describeProblems(check) {
|
|
407
460
|
return check.problems.map((problem) => problem.message).join("; ");
|
|
@@ -430,24 +483,41 @@ function createDocumentEngine(options) {
|
|
|
430
483
|
if (!check.ok) throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
|
|
431
484
|
const format = settings.format;
|
|
432
485
|
try {
|
|
486
|
+
const warnings = [...check.warnings];
|
|
433
487
|
const rendered = require_render_export.withExportView(canvas, settings.background, format, () => {
|
|
434
488
|
const area = require_export_area.resolveExportArea(canvas, settings.area, settings.padding);
|
|
435
489
|
return {
|
|
436
490
|
area,
|
|
437
|
-
content: format === "svg" ? Promise.resolve(
|
|
491
|
+
content: format === "svg" ? Promise.resolve(require_render_export.renderSvg(canvas, area, settings.scale, settings.svg)) : require_render_export.renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
438
492
|
};
|
|
439
493
|
});
|
|
440
|
-
|
|
494
|
+
let blob;
|
|
495
|
+
if (format === "svg") {
|
|
496
|
+
const embedded = await require_embed_assets.embedSvgAssets(await rendered.content, canvas, settings.svg, settings.signal, options.limits?.isAllowedUrl ?? require_content_limits.isSafeImageUrl);
|
|
497
|
+
stopIfCancelled();
|
|
498
|
+
if (settings.svg.embedImages === "require" && embedded.problems.length > 0) {
|
|
499
|
+
const problems = embedded.warnings.filter((warning) => warning.code === "IMAGE_NOT_EMBEDDED").map((warning) => ({
|
|
500
|
+
code: "IMAGE_NOT_EMBEDDED",
|
|
501
|
+
message: warning.message,
|
|
502
|
+
url: warning.url,
|
|
503
|
+
objectIds: warning.objectIds
|
|
504
|
+
}));
|
|
505
|
+
throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${embedded.problems.join("; ")}`, { problems });
|
|
506
|
+
}
|
|
507
|
+
warnings.push(...embedded.warnings);
|
|
508
|
+
blob = new Blob([embedded.svg], { type: require_render_export.mimeTypes.svg });
|
|
509
|
+
} else blob = await rendered.content;
|
|
441
510
|
return {
|
|
442
511
|
format,
|
|
443
512
|
mimeType: blob.type || require_render_export.mimeTypes[format],
|
|
444
513
|
blob,
|
|
445
514
|
width: Math.round(rendered.area.width * settings.scale),
|
|
446
515
|
height: Math.round(rendered.area.height * settings.scale),
|
|
447
|
-
warnings
|
|
516
|
+
warnings
|
|
448
517
|
};
|
|
449
518
|
} catch (error) {
|
|
450
519
|
if (require_errors.isDocumentEngineError(error)) throw error;
|
|
520
|
+
if (settings.signal?.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "The export was cancelled", { cause: error });
|
|
451
521
|
if (error?.name === "SecurityError") {
|
|
452
522
|
const problem = {
|
|
453
523
|
code: "CROSS_ORIGIN_IMAGE",
|
|
@@ -542,6 +612,7 @@ function createDocumentEngine(options) {
|
|
|
542
612
|
loadDocument,
|
|
543
613
|
load,
|
|
544
614
|
importFabricJson,
|
|
615
|
+
importSvg,
|
|
545
616
|
createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
|
|
546
617
|
listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
|
|
547
618
|
restoreVersion,
|
|
@@ -3,10 +3,11 @@ import { AssetManifest } from "../assets/asset-manifest.cjs";
|
|
|
3
3
|
import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.cjs";
|
|
4
4
|
import { ExportFormat, ExportOptions } from "../export/export-options.cjs";
|
|
5
5
|
import { ExportPreflight } from "../export/preflight-export.cjs";
|
|
6
|
+
import { ContentLimits } from "../security/content-limits.cjs";
|
|
7
|
+
import { SvgImportOptions, SvgImportResult } from "../import/svg-import.cjs";
|
|
6
8
|
import { NewDocumentOptions } from "../document/create-document.cjs";
|
|
7
9
|
import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
|
|
8
10
|
import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
|
|
9
|
-
import { ContentLimits } from "../security/content-limits.cjs";
|
|
10
11
|
import { VersionOptions, VersionSummary } from "../versions/document-version.cjs";
|
|
11
12
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.cjs";
|
|
12
13
|
import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
|
|
@@ -33,6 +34,22 @@ export interface DocumentEngineOptions {
|
|
|
33
34
|
export interface LoadOptions {
|
|
34
35
|
restoreCanvasSize?: boolean;
|
|
35
36
|
discardUnsavedChanges?: boolean;
|
|
37
|
+
/** Cancels the load. The canvas keeps its current content and the load rejects with `LOAD_ABORTED`. */
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
/** Called as the load moves through its stages. The `load:progress` event carries the same values. */
|
|
40
|
+
onProgress?: (progress: LoadProgress) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where a load is. `prepare` covers validation and migration, `images` counts
|
|
44
|
+
* images checked, `objects` counts top-level objects created, and `done` comes
|
|
45
|
+
* once the canvas shows the document.
|
|
46
|
+
*/
|
|
47
|
+
export type LoadStage = "prepare" | "images" | "objects" | "done";
|
|
48
|
+
export interface LoadProgress {
|
|
49
|
+
documentId: string | undefined;
|
|
50
|
+
stage: LoadStage;
|
|
51
|
+
done: number;
|
|
52
|
+
total: number;
|
|
36
53
|
}
|
|
37
54
|
export interface NewDocumentRequest extends NewDocumentOptions {
|
|
38
55
|
discardUnsavedChanges?: boolean;
|
|
@@ -54,6 +71,7 @@ export interface DocumentEngineEvents {
|
|
|
54
71
|
"load:start": {
|
|
55
72
|
documentId: string | undefined;
|
|
56
73
|
};
|
|
74
|
+
"load:progress": LoadProgress;
|
|
57
75
|
"document:change": {
|
|
58
76
|
documentId: string;
|
|
59
77
|
};
|
|
@@ -120,6 +138,7 @@ export interface DocumentEngine {
|
|
|
120
138
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
121
139
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
122
140
|
importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
|
|
141
|
+
importSvg(svg: string, options?: SvgImportOptions): Promise<SvgImportResult>;
|
|
123
142
|
createVersion(name?: string): Promise<VersionSummary>;
|
|
124
143
|
listVersions(documentId?: string): Promise<VersionSummary[]>;
|
|
125
144
|
restoreVersion(versionId: string): Promise<FabricDocument>;
|
|
@@ -3,10 +3,11 @@ import { AssetManifest } from "../assets/asset-manifest.js";
|
|
|
3
3
|
import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.js";
|
|
4
4
|
import { ExportFormat, ExportOptions } from "../export/export-options.js";
|
|
5
5
|
import { ExportPreflight } from "../export/preflight-export.js";
|
|
6
|
+
import { ContentLimits } from "../security/content-limits.js";
|
|
7
|
+
import { SvgImportOptions, SvgImportResult } from "../import/svg-import.js";
|
|
6
8
|
import { NewDocumentOptions } from "../document/create-document.js";
|
|
7
9
|
import { CustomObjectDefinition } from "../fabric/object-registry.js";
|
|
8
10
|
import { HistoryOptions, HistoryState } from "../history/create-history.js";
|
|
9
|
-
import { ContentLimits } from "../security/content-limits.js";
|
|
10
11
|
import { VersionOptions, VersionSummary } from "../versions/document-version.js";
|
|
11
12
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.js";
|
|
12
13
|
import { AutosaveOptions } from "../save/autosave-scheduler.js";
|
|
@@ -33,6 +34,22 @@ export interface DocumentEngineOptions {
|
|
|
33
34
|
export interface LoadOptions {
|
|
34
35
|
restoreCanvasSize?: boolean;
|
|
35
36
|
discardUnsavedChanges?: boolean;
|
|
37
|
+
/** Cancels the load. The canvas keeps its current content and the load rejects with `LOAD_ABORTED`. */
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
/** Called as the load moves through its stages. The `load:progress` event carries the same values. */
|
|
40
|
+
onProgress?: (progress: LoadProgress) => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where a load is. `prepare` covers validation and migration, `images` counts
|
|
44
|
+
* images checked, `objects` counts top-level objects created, and `done` comes
|
|
45
|
+
* once the canvas shows the document.
|
|
46
|
+
*/
|
|
47
|
+
export type LoadStage = "prepare" | "images" | "objects" | "done";
|
|
48
|
+
export interface LoadProgress {
|
|
49
|
+
documentId: string | undefined;
|
|
50
|
+
stage: LoadStage;
|
|
51
|
+
done: number;
|
|
52
|
+
total: number;
|
|
36
53
|
}
|
|
37
54
|
export interface NewDocumentRequest extends NewDocumentOptions {
|
|
38
55
|
discardUnsavedChanges?: boolean;
|
|
@@ -54,6 +71,7 @@ export interface DocumentEngineEvents {
|
|
|
54
71
|
"load:start": {
|
|
55
72
|
documentId: string | undefined;
|
|
56
73
|
};
|
|
74
|
+
"load:progress": LoadProgress;
|
|
57
75
|
"document:change": {
|
|
58
76
|
documentId: string;
|
|
59
77
|
};
|
|
@@ -120,6 +138,7 @@ export interface DocumentEngine {
|
|
|
120
138
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
121
139
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
122
140
|
importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
|
|
141
|
+
importSvg(svg: string, options?: SvgImportOptions): Promise<SvgImportResult>;
|
|
123
142
|
createVersion(name?: string): Promise<VersionSummary>;
|
|
124
143
|
listVersions(documentId?: string): Promise<VersionSummary[]>;
|
|
125
144
|
restoreVersion(versionId: string): Promise<FabricDocument>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildAssetManifest } from "../assets/asset-manifest.js";
|
|
2
2
|
import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
|
|
3
|
-
import { secureDocument } from "../security/content-limits.js";
|
|
3
|
+
import { isSafeImageUrl, secureDocument } from "../security/content-limits.js";
|
|
4
4
|
import { isSameUrl } from "../assets/image-check.js";
|
|
5
5
|
import { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave } from "../assets/asset-pipeline.js";
|
|
6
6
|
import { resolveExportArea } from "../export/export-area.js";
|
|
@@ -9,6 +9,8 @@ import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
|
|
|
9
9
|
import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
|
|
10
10
|
import { preflightExport } from "../export/preflight-export.js";
|
|
11
11
|
import { mimeTypes, renderRaster, renderSvg, withExportView } from "../export/render-export.js";
|
|
12
|
+
import { embedSvgAssets } from "../export/svg/embed-assets.js";
|
|
13
|
+
import { readSvg } from "../import/svg-import.js";
|
|
12
14
|
import "../document/document-format.js";
|
|
13
15
|
import { createId } from "../document/ids.js";
|
|
14
16
|
import { createDocumentInfo } from "../document/create-document.js";
|
|
@@ -143,8 +145,11 @@ function createDocumentEngine(options) {
|
|
|
143
145
|
if (serialized.background !== void 0) document.canvas.background = serialized.background;
|
|
144
146
|
return document;
|
|
145
147
|
}
|
|
146
|
-
function toLoadError(error,
|
|
147
|
-
if (signal.aborted)
|
|
148
|
+
function toLoadError(error, controller) {
|
|
149
|
+
if (controller.signal.aborted) {
|
|
150
|
+
const reason = cancelledByCaller.has(controller) ? "Loading was cancelled" : destroyed ? "Loading stopped because the engine was destroyed" : "Loading stopped because a newer load started";
|
|
151
|
+
return new DocumentEngineError("LOAD_ABORTED", reason, { cause: error });
|
|
152
|
+
}
|
|
148
153
|
if (isDocumentEngineError(error)) return error;
|
|
149
154
|
const reason = error instanceof Error ? error.message : String(error);
|
|
150
155
|
return new DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
|
|
@@ -167,19 +172,49 @@ function createDocumentEngine(options) {
|
|
|
167
172
|
if (!storage || discardUnsavedChanges || !saving.state().isDirty) return;
|
|
168
173
|
throw new DocumentEngineError("UNSAVED_CHANGES", "The current document has unsaved changes. Save first, or pass { discardUnsavedChanges: true } to replace it anyway.");
|
|
169
174
|
}
|
|
170
|
-
|
|
175
|
+
const cancelledByCaller = /* @__PURE__ */ new WeakSet();
|
|
176
|
+
const unlinkCallerSignal = /* @__PURE__ */ new WeakMap();
|
|
177
|
+
function startLoad(callerSignal) {
|
|
171
178
|
activeLoad?.abort();
|
|
172
179
|
const controller = new AbortController();
|
|
173
180
|
activeLoad = controller;
|
|
181
|
+
if (callerSignal) {
|
|
182
|
+
const cancel = () => {
|
|
183
|
+
cancelledByCaller.add(controller);
|
|
184
|
+
controller.abort();
|
|
185
|
+
};
|
|
186
|
+
if (callerSignal.aborted) cancel();
|
|
187
|
+
else {
|
|
188
|
+
callerSignal.addEventListener("abort", cancel, { once: true });
|
|
189
|
+
unlinkCallerSignal.set(controller, () => callerSignal.removeEventListener("abort", cancel));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
174
192
|
return controller;
|
|
175
193
|
}
|
|
194
|
+
function finishLoad(controller) {
|
|
195
|
+
unlinkCallerSignal.get(controller)?.();
|
|
196
|
+
unlinkCallerSignal.delete(controller);
|
|
197
|
+
}
|
|
176
198
|
async function loadMigratedDocument(input, loadOptions, importDetails, startedLoad) {
|
|
177
199
|
ensureUsable();
|
|
178
200
|
protectUnsavedChanges(loadOptions.discardUnsavedChanges);
|
|
179
|
-
const controller = startedLoad ?? startLoad();
|
|
201
|
+
const controller = startedLoad ?? startLoad(loadOptions.signal);
|
|
180
202
|
const documentId = describeDocumentId(input);
|
|
203
|
+
const report = (stage, done, total) => {
|
|
204
|
+
if (controller.signal.aborted) return;
|
|
205
|
+
const progress = {
|
|
206
|
+
documentId,
|
|
207
|
+
stage,
|
|
208
|
+
done,
|
|
209
|
+
total
|
|
210
|
+
};
|
|
211
|
+
loadOptions.onProgress?.(progress);
|
|
212
|
+
events.emit("load:progress", progress);
|
|
213
|
+
};
|
|
181
214
|
events.emit("load:start", { documentId });
|
|
182
215
|
try {
|
|
216
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
217
|
+
report("prepare", 0, 1);
|
|
183
218
|
await recovery?.markLoadStarted(documentId);
|
|
184
219
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
185
220
|
const { document: migrated, migratedFrom } = migrateDocument(secureDocument(input, options.limits), {
|
|
@@ -188,13 +223,17 @@ function createDocumentEngine(options) {
|
|
|
188
223
|
...importDetails
|
|
189
224
|
});
|
|
190
225
|
const checked = checkDocument(migrated);
|
|
191
|
-
|
|
226
|
+
report("prepare", 1, 1);
|
|
227
|
+
const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal, options.limits?.isAllowedUrl, (done, total) => report("images", done, total));
|
|
192
228
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
193
229
|
await history.withoutRecording(() => loadIntoCanvas(canvas, {
|
|
194
230
|
version: document.fabricVersion,
|
|
195
231
|
background: document.canvas.background,
|
|
196
232
|
objects: document.objects
|
|
197
|
-
},
|
|
233
|
+
}, {
|
|
234
|
+
signal: controller.signal,
|
|
235
|
+
onObjects: (done, total) => report("objects", done, total)
|
|
236
|
+
}));
|
|
198
237
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
199
238
|
if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
|
|
200
239
|
width: document.canvas.width,
|
|
@@ -211,6 +250,7 @@ function createDocumentEngine(options) {
|
|
|
211
250
|
recovery?.cancel();
|
|
212
251
|
saving.startSession(document.revision ?? 0);
|
|
213
252
|
canvas.requestRenderAll();
|
|
253
|
+
report("done", 1, 1);
|
|
214
254
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
215
255
|
events.emit("document:change", { documentId: document.id });
|
|
216
256
|
events.emit("load:success", {
|
|
@@ -220,10 +260,11 @@ function createDocumentEngine(options) {
|
|
|
220
260
|
});
|
|
221
261
|
return document;
|
|
222
262
|
} catch (error) {
|
|
223
|
-
const engineError = toLoadError(error, controller
|
|
263
|
+
const engineError = toLoadError(error, controller);
|
|
224
264
|
events.emit("load:error", { error: engineError });
|
|
225
265
|
throw engineError;
|
|
226
266
|
} finally {
|
|
267
|
+
finishLoad(controller);
|
|
227
268
|
if (activeLoad === controller) {
|
|
228
269
|
activeLoad = null;
|
|
229
270
|
await recovery?.markLoadFinished();
|
|
@@ -244,6 +285,16 @@ function createDocumentEngine(options) {
|
|
|
244
285
|
metadata
|
|
245
286
|
});
|
|
246
287
|
}
|
|
288
|
+
async function importSvg(svg, importOptions = {}) {
|
|
289
|
+
ensureUsable();
|
|
290
|
+
const result = await readSvg(svg, importOptions, options.limits);
|
|
291
|
+
ensureUsable();
|
|
292
|
+
if (result.objects.length > 0) {
|
|
293
|
+
history.transaction("Import SVG", () => canvas.add(...result.objects));
|
|
294
|
+
canvas.requestRenderAll();
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
247
298
|
let savesSinceAutomaticVersion = 0;
|
|
248
299
|
let lastVersionTime = 0;
|
|
249
300
|
function nextVersionTimestamp() {
|
|
@@ -322,15 +373,17 @@ function createDocumentEngine(options) {
|
|
|
322
373
|
ensureUsable();
|
|
323
374
|
const source = requireStorage();
|
|
324
375
|
protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
|
|
325
|
-
const controller = startLoad();
|
|
376
|
+
const controller = startLoad(loadOptions?.signal);
|
|
326
377
|
let stored;
|
|
327
378
|
try {
|
|
379
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
328
380
|
stored = await source.loadDocument(documentId);
|
|
329
381
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
330
382
|
} catch (error) {
|
|
383
|
+
finishLoad(controller);
|
|
331
384
|
if (activeLoad === controller) activeLoad = null;
|
|
332
385
|
if (controller.signal.aborted) {
|
|
333
|
-
const aborted = toLoadError(error, controller
|
|
386
|
+
const aborted = toLoadError(error, controller);
|
|
334
387
|
events.emit("load:error", { error: aborted });
|
|
335
388
|
throw aborted;
|
|
336
389
|
}
|
|
@@ -400,8 +453,8 @@ function createDocumentEngine(options) {
|
|
|
400
453
|
}
|
|
401
454
|
async function checkBeforeExport(exportOptions) {
|
|
402
455
|
ensureUsable();
|
|
403
|
-
const { format } = normalizeExportOptions(exportOptions);
|
|
404
|
-
return preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
|
|
456
|
+
const { format, svg } = normalizeExportOptions(exportOptions);
|
|
457
|
+
return preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions, svg);
|
|
405
458
|
}
|
|
406
459
|
function describeProblems(check) {
|
|
407
460
|
return check.problems.map((problem) => problem.message).join("; ");
|
|
@@ -430,24 +483,41 @@ function createDocumentEngine(options) {
|
|
|
430
483
|
if (!check.ok) throw new DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
|
|
431
484
|
const format = settings.format;
|
|
432
485
|
try {
|
|
486
|
+
const warnings = [...check.warnings];
|
|
433
487
|
const rendered = withExportView(canvas, settings.background, format, () => {
|
|
434
488
|
const area = resolveExportArea(canvas, settings.area, settings.padding);
|
|
435
489
|
return {
|
|
436
490
|
area,
|
|
437
|
-
content: format === "svg" ? Promise.resolve(
|
|
491
|
+
content: format === "svg" ? Promise.resolve(renderSvg(canvas, area, settings.scale, settings.svg)) : renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
438
492
|
};
|
|
439
493
|
});
|
|
440
|
-
|
|
494
|
+
let blob;
|
|
495
|
+
if (format === "svg") {
|
|
496
|
+
const embedded = await embedSvgAssets(await rendered.content, canvas, settings.svg, settings.signal, options.limits?.isAllowedUrl ?? isSafeImageUrl);
|
|
497
|
+
stopIfCancelled();
|
|
498
|
+
if (settings.svg.embedImages === "require" && embedded.problems.length > 0) {
|
|
499
|
+
const problems = embedded.warnings.filter((warning) => warning.code === "IMAGE_NOT_EMBEDDED").map((warning) => ({
|
|
500
|
+
code: "IMAGE_NOT_EMBEDDED",
|
|
501
|
+
message: warning.message,
|
|
502
|
+
url: warning.url,
|
|
503
|
+
objectIds: warning.objectIds
|
|
504
|
+
}));
|
|
505
|
+
throw new DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${embedded.problems.join("; ")}`, { problems });
|
|
506
|
+
}
|
|
507
|
+
warnings.push(...embedded.warnings);
|
|
508
|
+
blob = new Blob([embedded.svg], { type: mimeTypes.svg });
|
|
509
|
+
} else blob = await rendered.content;
|
|
441
510
|
return {
|
|
442
511
|
format,
|
|
443
512
|
mimeType: blob.type || mimeTypes[format],
|
|
444
513
|
blob,
|
|
445
514
|
width: Math.round(rendered.area.width * settings.scale),
|
|
446
515
|
height: Math.round(rendered.area.height * settings.scale),
|
|
447
|
-
warnings
|
|
516
|
+
warnings
|
|
448
517
|
};
|
|
449
518
|
} catch (error) {
|
|
450
519
|
if (isDocumentEngineError(error)) throw error;
|
|
520
|
+
if (settings.signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The export was cancelled", { cause: error });
|
|
451
521
|
if (error?.name === "SecurityError") {
|
|
452
522
|
const problem = {
|
|
453
523
|
code: "CROSS_ORIGIN_IMAGE",
|
|
@@ -542,6 +612,7 @@ function createDocumentEngine(options) {
|
|
|
542
612
|
loadDocument,
|
|
543
613
|
load,
|
|
544
614
|
importFabricJson,
|
|
615
|
+
importSvg,
|
|
545
616
|
createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
|
|
546
617
|
listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
|
|
547
618
|
restoreVersion,
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.cjs";
|
|
2
2
|
import { ExportProblem } from "../export/preflight-export.cjs";
|
|
3
3
|
//#region src/engine/errors.d.ts
|
|
4
|
-
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "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
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "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" | "SVG_IMPORT_FAILED" | "PDF_UNAVAILABLE" | "PDF_FAILED" | "ENGINE_DESTROYED";
|
|
5
5
|
export interface DocumentIssue {
|
|
6
6
|
code: DocumentErrorCode;
|
|
7
7
|
path: string;
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.js";
|
|
2
2
|
import { ExportProblem } from "../export/preflight-export.js";
|
|
3
3
|
//#region src/engine/errors.d.ts
|
|
4
|
-
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "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
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "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" | "SVG_IMPORT_FAILED" | "PDF_UNAVAILABLE" | "PDF_FAILED" | "ENGINE_DESTROYED";
|
|
5
5
|
export interface DocumentIssue {
|
|
6
6
|
code: DocumentErrorCode;
|
|
7
7
|
path: string;
|