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.
Files changed (89) hide show
  1. package/README.md +277 -36
  2. package/README.zh-CN.md +697 -0
  3. package/dist/assets/asset-manifest.d.cts +3 -0
  4. package/dist/assets/asset-manifest.d.ts +3 -0
  5. package/dist/assets/asset-pipeline.cjs +22 -10
  6. package/dist/assets/asset-pipeline.d.cts +5 -1
  7. package/dist/assets/asset-pipeline.d.ts +5 -1
  8. package/dist/assets/asset-pipeline.js +22 -10
  9. package/dist/assets/font-check.cjs +9 -1
  10. package/dist/assets/font-check.js +9 -1
  11. package/dist/assets/image-check.cjs +88 -6
  12. package/dist/assets/image-check.d.cts +17 -0
  13. package/dist/assets/image-check.d.ts +17 -0
  14. package/dist/assets/image-check.js +88 -7
  15. package/dist/commands/clipboard.cjs +136 -0
  16. package/dist/commands/clipboard.d.cts +47 -0
  17. package/dist/commands/clipboard.d.ts +47 -0
  18. package/dist/commands/clipboard.js +135 -0
  19. package/dist/commands/layers.cjs +97 -0
  20. package/dist/commands/layers.d.cts +39 -0
  21. package/dist/commands/layers.d.ts +39 -0
  22. package/dist/commands/layers.js +92 -0
  23. package/dist/engine/create-document-engine.cjs +85 -14
  24. package/dist/engine/create-document-engine.d.cts +20 -1
  25. package/dist/engine/create-document-engine.d.ts +20 -1
  26. package/dist/engine/create-document-engine.js +86 -15
  27. package/dist/engine/errors.d.cts +1 -1
  28. package/dist/engine/errors.d.ts +1 -1
  29. package/dist/export/batch-render.cjs +172 -0
  30. package/dist/export/batch-render.d.cts +43 -0
  31. package/dist/export/batch-render.d.ts +43 -0
  32. package/dist/export/batch-render.js +171 -0
  33. package/dist/export/export-options.cjs +18 -1
  34. package/dist/export/export-options.d.cts +23 -0
  35. package/dist/export/export-options.d.ts +23 -0
  36. package/dist/export/export-options.js +18 -2
  37. package/dist/export/preflight-export.cjs +11 -2
  38. package/dist/export/preflight-export.d.cts +1 -1
  39. package/dist/export/preflight-export.d.ts +1 -1
  40. package/dist/export/preflight-export.js +11 -2
  41. package/dist/export/render-export.cjs +18 -3
  42. package/dist/export/render-export.js +18 -4
  43. package/dist/export/svg/embed-assets.cjs +249 -0
  44. package/dist/export/svg/embed-assets.js +249 -0
  45. package/dist/export/svg/overrides.cjs +32 -0
  46. package/dist/export/svg/overrides.js +32 -0
  47. package/dist/export/svg/text-on-path.cjs +252 -0
  48. package/dist/export/svg/text-on-path.js +249 -0
  49. package/dist/fabric/fabric-adapter.cjs +46 -5
  50. package/dist/fabric/fabric-adapter.js +46 -5
  51. package/dist/fabric/object-registry.cjs +1 -1
  52. package/dist/fabric/object-registry.js +1 -1
  53. package/dist/import/svg-import.cjs +256 -0
  54. package/dist/import/svg-import.d.cts +50 -0
  55. package/dist/import/svg-import.d.ts +50 -0
  56. package/dist/import/svg-import.js +256 -0
  57. package/dist/index.cjs +12 -0
  58. package/dist/index.d.cts +9 -4
  59. package/dist/index.d.ts +9 -4
  60. package/dist/index.js +4 -1
  61. package/dist/pdf/export-pdf.cjs +440 -0
  62. package/dist/pdf/export-pdf.d.cts +73 -0
  63. package/dist/pdf/export-pdf.d.ts +73 -0
  64. package/dist/pdf/export-pdf.js +440 -0
  65. package/dist/pdf/fonts.cjs +115 -0
  66. package/dist/pdf/fonts.d.cts +12 -0
  67. package/dist/pdf/fonts.d.ts +12 -0
  68. package/dist/pdf/fonts.js +112 -0
  69. package/dist/pdf/page-layout.cjs +79 -0
  70. package/dist/pdf/page-layout.d.cts +51 -0
  71. package/dist/pdf/page-layout.d.ts +51 -0
  72. package/dist/pdf/page-layout.js +76 -0
  73. package/dist/pdf/text-decorations.cjs +108 -0
  74. package/dist/pdf/text-decorations.js +107 -0
  75. package/dist/pdf.cjs +7 -0
  76. package/dist/pdf.d.cts +4 -0
  77. package/dist/pdf.d.ts +4 -0
  78. package/dist/pdf.js +3 -0
  79. package/dist/react/use-layers.cjs +53 -0
  80. package/dist/react/use-layers.d.cts +10 -0
  81. package/dist/react/use-layers.d.ts +10 -0
  82. package/dist/react/use-layers.js +53 -0
  83. package/dist/react.cjs +2 -0
  84. package/dist/react.d.cts +3 -1
  85. package/dist/react.d.ts +3 -1
  86. package/dist/react.js +2 -1
  87. package/dist/util/concurrency.cjs +28 -0
  88. package/dist/util/concurrency.js +27 -0
  89. 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, signal) {
147
- if (signal.aborted) return new require_errors.DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
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
- function startLoad() {
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
- const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal, options.limits?.isAllowedUrl);
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
- }, controller.signal));
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.signal);
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.signal);
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(new Blob([require_render_export.renderSvg(canvas, area, settings.scale)], { type: require_render_export.mimeTypes.svg })) : require_render_export.renderRaster(canvas, area, format, settings.scale, settings.quality)
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
- const blob = await rendered.content;
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: check.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, signal) {
147
- if (signal.aborted) return new DocumentEngineError("LOAD_ABORTED", "Loading stopped because a newer load started", { cause: error });
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
- function startLoad() {
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
- const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal, options.limits?.isAllowedUrl);
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
- }, controller.signal));
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.signal);
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.signal);
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(new Blob([renderSvg(canvas, area, settings.scale)], { type: mimeTypes.svg })) : renderRaster(canvas, area, format, settings.scale, settings.quality)
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
- const blob = await rendered.content;
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: check.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,
@@ -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;
@@ -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;