fabricjs-document-engine 1.0.2 → 1.2.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 (118) hide show
  1. package/README.md +220 -11
  2. package/README.zh-CN.md +220 -11
  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 +31 -17
  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 +31 -17
  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 +89 -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 +89 -7
  15. package/dist/commands/clipboard.cjs +141 -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 +140 -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/document/document-format.d.cts +15 -0
  24. package/dist/document/document-format.d.ts +15 -0
  25. package/dist/document/validate-document.cjs +8 -0
  26. package/dist/document/validate-document.js +8 -0
  27. package/dist/engine/create-document-engine.cjs +196 -31
  28. package/dist/engine/create-document-engine.d.cts +44 -7
  29. package/dist/engine/create-document-engine.d.ts +44 -7
  30. package/dist/engine/create-document-engine.js +197 -32
  31. package/dist/engine/errors.d.cts +1 -1
  32. package/dist/engine/errors.d.ts +1 -1
  33. package/dist/export/batch-render.cjs +172 -0
  34. package/dist/export/batch-render.d.cts +43 -0
  35. package/dist/export/batch-render.d.ts +43 -0
  36. package/dist/export/batch-render.js +171 -0
  37. package/dist/export/export-options.cjs +21 -1
  38. package/dist/export/export-options.d.cts +29 -0
  39. package/dist/export/export-options.d.ts +29 -0
  40. package/dist/export/export-options.js +21 -2
  41. package/dist/export/preflight-export.cjs +11 -2
  42. package/dist/export/preflight-export.d.cts +1 -1
  43. package/dist/export/preflight-export.d.ts +1 -1
  44. package/dist/export/preflight-export.js +11 -2
  45. package/dist/export/render-export.cjs +57 -4
  46. package/dist/export/render-export.js +57 -5
  47. package/dist/export/svg/clip-masks.cjs +39 -0
  48. package/dist/export/svg/clip-masks.js +37 -0
  49. package/dist/export/svg/embed-assets.cjs +250 -0
  50. package/dist/export/svg/embed-assets.js +250 -0
  51. package/dist/export/svg/overrides.cjs +32 -0
  52. package/dist/export/svg/overrides.js +32 -0
  53. package/dist/export/svg/raster-object.cjs +82 -0
  54. package/dist/export/svg/raster-object.js +81 -0
  55. package/dist/export/svg/text-decorations.cjs +108 -0
  56. package/dist/export/svg/text-decorations.js +107 -0
  57. package/dist/export/svg/text-on-path.cjs +242 -0
  58. package/dist/export/svg/text-on-path.js +240 -0
  59. package/dist/fabric/fabric-adapter.cjs +49 -6
  60. package/dist/fabric/fabric-adapter.js +49 -6
  61. package/dist/fabric/object-registry.cjs +1 -1
  62. package/dist/fabric/object-registry.js +1 -1
  63. package/dist/fabric/page-state.cjs +69 -0
  64. package/dist/fabric/page-state.js +64 -0
  65. package/dist/history/apply-state.cjs +19 -2
  66. package/dist/history/apply-state.js +19 -2
  67. package/dist/history/create-history.cjs +53 -13
  68. package/dist/history/create-history.d.cts +8 -0
  69. package/dist/history/create-history.d.ts +8 -0
  70. package/dist/history/create-history.js +53 -13
  71. package/dist/history/snapshot.cjs +10 -1
  72. package/dist/history/snapshot.js +10 -2
  73. package/dist/import/svg-import.cjs +256 -0
  74. package/dist/import/svg-import.d.cts +50 -0
  75. package/dist/import/svg-import.d.ts +50 -0
  76. package/dist/import/svg-import.js +256 -0
  77. package/dist/index.cjs +12 -0
  78. package/dist/index.d.cts +10 -5
  79. package/dist/index.d.ts +10 -5
  80. package/dist/index.js +4 -1
  81. package/dist/migrations/migrate-document.cjs +2 -1
  82. package/dist/migrations/migrate-document.js +2 -1
  83. package/dist/pdf/export-pdf.cjs +381 -0
  84. package/dist/pdf/export-pdf.d.cts +73 -0
  85. package/dist/pdf/export-pdf.d.ts +73 -0
  86. package/dist/pdf/export-pdf.js +381 -0
  87. package/dist/pdf/fonts.cjs +115 -0
  88. package/dist/pdf/fonts.d.cts +12 -0
  89. package/dist/pdf/fonts.d.ts +12 -0
  90. package/dist/pdf/fonts.js +112 -0
  91. package/dist/pdf/page-layout.cjs +79 -0
  92. package/dist/pdf/page-layout.d.cts +51 -0
  93. package/dist/pdf/page-layout.d.ts +51 -0
  94. package/dist/pdf/page-layout.js +76 -0
  95. package/dist/pdf.cjs +7 -0
  96. package/dist/pdf.d.cts +4 -0
  97. package/dist/pdf.d.ts +4 -0
  98. package/dist/pdf.js +3 -0
  99. package/dist/react/use-layers.cjs +53 -0
  100. package/dist/react/use-layers.d.cts +10 -0
  101. package/dist/react/use-layers.d.ts +10 -0
  102. package/dist/react/use-layers.js +53 -0
  103. package/dist/react.cjs +2 -0
  104. package/dist/react.d.cts +3 -1
  105. package/dist/react.d.ts +3 -1
  106. package/dist/react.js +2 -1
  107. package/dist/recovery/recovery-controller.cjs +148 -29
  108. package/dist/recovery/recovery-controller.d.cts +8 -0
  109. package/dist/recovery/recovery-controller.d.ts +8 -0
  110. package/dist/recovery/recovery-controller.js +148 -29
  111. package/dist/security/content-limits.cjs +43 -3
  112. package/dist/security/content-limits.d.cts +8 -0
  113. package/dist/security/content-limits.d.ts +8 -0
  114. package/dist/security/content-limits.js +39 -4
  115. package/dist/util/concurrency.cjs +41 -0
  116. package/dist/util/concurrency.js +40 -0
  117. package/package.json +30 -3
  118. package/schema/document-v1.schema.json +5 -1
@@ -1,4 +1,5 @@
1
1
  const require_asset_manifest = require("../assets/asset-manifest.cjs");
2
+ const require_page_state = require("../fabric/page-state.cjs");
2
3
  const require_errors = require("./errors.cjs");
3
4
  const require_content_limits = require("../security/content-limits.cjs");
4
5
  const require_image_check = require("../assets/image-check.cjs");
@@ -9,6 +10,8 @@ const require_object_ids = require("../fabric/object-ids.cjs");
9
10
  const require_walk_objects = require("../fabric/walk-objects.cjs");
10
11
  const require_preflight_export = require("../export/preflight-export.cjs");
11
12
  const require_render_export = require("../export/render-export.cjs");
13
+ const require_embed_assets = require("../export/svg/embed-assets.cjs");
14
+ const require_svg_import = require("../import/svg-import.cjs");
12
15
  require("../document/document-format.cjs");
13
16
  const require_ids = require("../document/ids.cjs");
14
17
  const require_create_document = require("../document/create-document.cjs");
@@ -36,11 +39,21 @@ function createDocumentEngine(options) {
36
39
  const registry = require_object_registry.createObjectRegistry(options.customObjects);
37
40
  const events = require_event_emitter.createEventEmitter();
38
41
  const objectsById = /* @__PURE__ */ new Map();
39
- const assetOptions = options.assets ?? {};
42
+ const assetOptions = {
43
+ ...options.assets,
44
+ maxImagePixels: options.limits?.maxImagePixels ?? 67108864
45
+ };
40
46
  const uploadedUrls = /* @__PURE__ */ new Map();
41
47
  let documentInfo = require_create_document.createDocumentInfo(options.document);
42
48
  let activeLoad = null;
43
49
  let destroyed = false;
50
+ let mutations = 0;
51
+ let session = 0;
52
+ /** Throws when async work started under `startedIn` would now change another document. */
53
+ function ensureSameSession(startedIn, what) {
54
+ ensureUsable();
55
+ if (session !== startedIn) throw new require_errors.DocumentEngineError("DOCUMENT_CHANGED", `${what} was dropped because another document was opened while it ran`);
56
+ }
44
57
  function ensureUsable() {
45
58
  if (destroyed) throw new require_errors.DocumentEngineError("ENGINE_DESTROYED", "This document engine was destroyed");
46
59
  }
@@ -65,21 +78,41 @@ function createDocumentEngine(options) {
65
78
  objectsById.clear();
66
79
  canvas.getObjects().forEach(indexObjectTree);
67
80
  }
68
- const handleObjectAdded = ({ target }) => indexObjectTree(target);
69
- const handleObjectRemoved = ({ target }) => removeObjectTreeFromIndex(target);
81
+ const handleObjectAdded = ({ target }) => {
82
+ mutations += 1;
83
+ indexObjectTree(target);
84
+ };
85
+ const handleObjectRemoved = ({ target }) => {
86
+ mutations += 1;
87
+ removeObjectTreeFromIndex(target);
88
+ };
89
+ const countMutation = () => {
90
+ mutations += 1;
91
+ };
70
92
  canvas.on("object:added", handleObjectAdded);
71
93
  canvas.on("object:removed", handleObjectRemoved);
94
+ canvas.on("object:modified", countMutation);
95
+ canvas.on("text:changed", countMutation);
72
96
  rebuildIndex();
73
97
  const history = require_create_history.createHistory({
74
98
  canvas,
75
99
  limit: options.history?.limit,
76
100
  maxBytes: options.history?.maxBytes,
77
- serializeObjects: () => {
101
+ serializeState: () => {
78
102
  rebuildIndex();
79
- return require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects;
103
+ const serialized = require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude());
104
+ return {
105
+ objects: serialized.objects,
106
+ page: {
107
+ width: canvas.getWidth(),
108
+ height: canvas.getHeight(),
109
+ ...require_page_state.pickPageFields(serialized)
110
+ }
111
+ };
80
112
  },
81
113
  onChange: (state) => events.emit("history:change", state),
82
- onContentChange: () => noteContentChange()
114
+ onContentChange: () => noteContentChange(),
115
+ onLiveChange: () => noteContentChange()
83
116
  });
84
117
  const saving = require_save_controller.createSaveController({
85
118
  getStorage: () => requireStorage(),
@@ -114,6 +147,7 @@ function createDocumentEngine(options) {
114
147
  }
115
148
  }) : void 0;
116
149
  function noteContentChange() {
150
+ mutations += 1;
117
151
  saving.noteContentChange();
118
152
  recovery?.schedule();
119
153
  }
@@ -136,15 +170,21 @@ function createDocumentEngine(options) {
136
170
  height: canvas.getHeight()
137
171
  },
138
172
  objects: serialized.objects,
139
- assets: require_asset_manifest.buildAssetManifest(serialized.objects),
173
+ assets: require_asset_manifest.buildAssetManifest(require_page_state.documentObjects({
174
+ objects: serialized.objects,
175
+ canvas: serialized
176
+ })),
140
177
  metadata: { ...documentInfo.metadata }
141
178
  };
142
179
  if (serialized.version !== void 0) document.fabricVersion = serialized.version;
143
- if (serialized.background !== void 0) document.canvas.background = serialized.background;
180
+ Object.assign(document.canvas, require_page_state.pickPageFields(serialized));
144
181
  return document;
145
182
  }
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 });
183
+ function toLoadError(error, controller) {
184
+ if (controller.signal.aborted) {
185
+ const reason = cancelledByCaller.has(controller) ? "Loading was cancelled" : destroyed ? "Loading stopped because the engine was destroyed" : "Loading stopped because a newer load started";
186
+ return new require_errors.DocumentEngineError("LOAD_ABORTED", reason, { cause: error });
187
+ }
148
188
  if (require_errors.isDocumentEngineError(error)) return error;
149
189
  const reason = error instanceof Error ? error.message : String(error);
150
190
  return new require_errors.DocumentEngineError("LOAD_FAILED", `Fabric could not load the document: ${reason}`, { cause: error });
@@ -156,7 +196,7 @@ function createDocumentEngine(options) {
156
196
  throw new require_errors.DocumentEngineError(issues[0].code, `The document is not valid: ${summary}`, { issues });
157
197
  }
158
198
  const document = input;
159
- const unknownTypes = registry.findUnknownTypes(require_walk_objects.collectSerializedTypes(document.objects));
199
+ const unknownTypes = registry.findUnknownTypes(require_walk_objects.collectSerializedTypes(require_page_state.documentObjects(document)));
160
200
  if (unknownTypes.length > 0) throw new require_errors.DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
161
201
  return document;
162
202
  }
@@ -167,19 +207,50 @@ function createDocumentEngine(options) {
167
207
  if (!storage || discardUnsavedChanges || !saving.state().isDirty) return;
168
208
  throw new require_errors.DocumentEngineError("UNSAVED_CHANGES", "The current document has unsaved changes. Save first, or pass { discardUnsavedChanges: true } to replace it anyway.");
169
209
  }
170
- function startLoad() {
210
+ const cancelledByCaller = /* @__PURE__ */ new WeakSet();
211
+ const unlinkCallerSignal = /* @__PURE__ */ new WeakMap();
212
+ function startLoad(callerSignal) {
171
213
  activeLoad?.abort();
172
214
  const controller = new AbortController();
173
215
  activeLoad = controller;
216
+ if (callerSignal) {
217
+ const cancel = () => {
218
+ cancelledByCaller.add(controller);
219
+ controller.abort();
220
+ };
221
+ if (callerSignal.aborted) cancel();
222
+ else {
223
+ callerSignal.addEventListener("abort", cancel, { once: true });
224
+ unlinkCallerSignal.set(controller, () => callerSignal.removeEventListener("abort", cancel));
225
+ }
226
+ }
174
227
  return controller;
175
228
  }
229
+ function finishLoad(controller) {
230
+ unlinkCallerSignal.get(controller)?.();
231
+ unlinkCallerSignal.delete(controller);
232
+ }
176
233
  async function loadMigratedDocument(input, loadOptions, importDetails, startedLoad) {
177
234
  ensureUsable();
178
235
  protectUnsavedChanges(loadOptions.discardUnsavedChanges);
179
- const controller = startedLoad ?? startLoad();
236
+ const controller = startedLoad ?? startLoad(loadOptions.signal);
237
+ const mutationsAtStart = mutations;
180
238
  const documentId = describeDocumentId(input);
239
+ const report = (stage, done, total) => {
240
+ if (controller.signal.aborted) return;
241
+ const progress = {
242
+ documentId,
243
+ stage,
244
+ done,
245
+ total
246
+ };
247
+ loadOptions.onProgress?.(progress);
248
+ events.emit("load:progress", progress);
249
+ };
181
250
  events.emit("load:start", { documentId });
182
251
  try {
252
+ if (controller.signal.aborted) throw new Error("aborted");
253
+ report("prepare", 0, 1);
183
254
  await recovery?.markLoadStarted(documentId);
184
255
  if (controller.signal.aborted) throw new Error("aborted");
185
256
  const { document: migrated, migratedFrom } = require_migrate_document.migrateDocument(require_content_limits.secureDocument(input, options.limits), {
@@ -188,13 +259,21 @@ function createDocumentEngine(options) {
188
259
  ...importDetails
189
260
  });
190
261
  const checked = checkDocument(migrated);
191
- const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal, options.limits?.isAllowedUrl);
262
+ report("prepare", 1, 1);
263
+ const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal, options.limits?.isAllowedUrl, (done, total) => report("images", done, total));
192
264
  if (controller.signal.aborted) throw new Error("aborted");
193
265
  await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
194
266
  version: document.fabricVersion,
195
- background: document.canvas.background,
267
+ ...require_page_state.pickPageFields(document.canvas),
196
268
  objects: document.objects
197
- }, controller.signal));
269
+ }, {
270
+ signal: controller.signal,
271
+ onObjects: (done, total) => report("objects", done, total),
272
+ beforeSwap: () => {
273
+ if (mutations !== mutationsAtStart && !loadOptions.discardUnsavedChanges) throw new require_errors.DocumentEngineError("LOAD_CONFLICT", "The canvas was edited while the document loaded, so the load was stopped to keep the edits. Pass { discardUnsavedChanges: true } to replace them.");
274
+ session += 1;
275
+ }
276
+ }));
198
277
  if (controller.signal.aborted) throw new Error("aborted");
199
278
  if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
200
279
  width: document.canvas.width,
@@ -211,6 +290,7 @@ function createDocumentEngine(options) {
211
290
  recovery?.cancel();
212
291
  saving.startSession(document.revision ?? 0);
213
292
  canvas.requestRenderAll();
293
+ report("done", 1, 1);
214
294
  if (warnings.length > 0) events.emit("assets:warning", { warnings });
215
295
  events.emit("document:change", { documentId: document.id });
216
296
  events.emit("load:success", {
@@ -220,10 +300,11 @@ function createDocumentEngine(options) {
220
300
  });
221
301
  return document;
222
302
  } catch (error) {
223
- const engineError = toLoadError(error, controller.signal);
303
+ const engineError = toLoadError(error, controller);
224
304
  events.emit("load:error", { error: engineError });
225
305
  throw engineError;
226
306
  } finally {
307
+ finishLoad(controller);
227
308
  if (activeLoad === controller) {
228
309
  activeLoad = null;
229
310
  await recovery?.markLoadFinished();
@@ -244,6 +325,43 @@ function createDocumentEngine(options) {
244
325
  metadata
245
326
  });
246
327
  }
328
+ function setPage(changes, label = "Change page") {
329
+ ensureUsable();
330
+ const problem = require_content_limits.canvasSizeProblem(changes.width ?? canvas.getWidth(), changes.height ?? canvas.getHeight(), options.limits);
331
+ if (problem) throw new require_errors.DocumentEngineError("UNSAFE_DOCUMENT", `The page was not resized: it would be ${problem}`);
332
+ history.transaction(label, () => {
333
+ const width = changes.width ?? canvas.getWidth();
334
+ const height = changes.height ?? canvas.getHeight();
335
+ if (width !== canvas.getWidth() || height !== canvas.getHeight()) canvas.setDimensions({
336
+ width,
337
+ height
338
+ });
339
+ const live = {};
340
+ if (changes.background !== void 0) live.backgroundColor = changes.background ?? "";
341
+ if (changes.backgroundImage !== void 0) live.backgroundImage = changes.backgroundImage ?? void 0;
342
+ if (changes.overlay !== void 0) live.overlayColor = changes.overlay ?? "";
343
+ if (changes.overlayImage !== void 0) live.overlayImage = changes.overlayImage ?? void 0;
344
+ if (changes.clipPath !== void 0) live.clipPath = changes.clipPath ?? void 0;
345
+ canvas.set(live);
346
+ });
347
+ canvas.requestRenderAll();
348
+ }
349
+ async function importSvg(svg, importOptions = {}) {
350
+ ensureUsable();
351
+ const startedIn = session;
352
+ const result = await require_svg_import.readSvg(svg, importOptions, options.limits);
353
+ try {
354
+ ensureSameSession(startedIn, "The SVG import");
355
+ } catch (error) {
356
+ result.objects.forEach((object) => object.dispose?.());
357
+ throw error;
358
+ }
359
+ if (result.objects.length > 0) {
360
+ history.transaction("Import SVG", () => canvas.add(...result.objects));
361
+ canvas.requestRenderAll();
362
+ }
363
+ return result;
364
+ }
247
365
  let savesSinceAutomaticVersion = 0;
248
366
  let lastVersionTime = 0;
249
367
  function nextVersionTimestamp() {
@@ -322,15 +440,17 @@ function createDocumentEngine(options) {
322
440
  ensureUsable();
323
441
  const source = requireStorage();
324
442
  protectUnsavedChanges(loadOptions?.discardUnsavedChanges);
325
- const controller = startLoad();
443
+ const controller = startLoad(loadOptions?.signal);
326
444
  let stored;
327
445
  try {
446
+ if (controller.signal.aborted) throw new Error("aborted");
328
447
  stored = await source.loadDocument(documentId);
329
448
  if (controller.signal.aborted) throw new Error("aborted");
330
449
  } catch (error) {
450
+ finishLoad(controller);
331
451
  if (activeLoad === controller) activeLoad = null;
332
452
  if (controller.signal.aborted) {
333
- const aborted = toLoadError(error, controller.signal);
453
+ const aborted = toLoadError(error, controller);
334
454
  events.emit("load:error", { error: aborted });
335
455
  throw aborted;
336
456
  }
@@ -355,6 +475,7 @@ function createDocumentEngine(options) {
355
475
  protectUnsavedChanges(newOptions.discardUnsavedChanges);
356
476
  activeLoad?.abort();
357
477
  history.withoutRecording(() => canvas.clear());
478
+ session += 1;
358
479
  objectsById.clear();
359
480
  documentInfo = require_create_document.createDocumentInfo(newOptions);
360
481
  uploadedUrls.clear();
@@ -381,10 +502,12 @@ function createDocumentEngine(options) {
381
502
  if (isReplaceableImage(object) && require_image_check.isSameUrl(object.getSrc(), oldUrl)) images.push(object);
382
503
  });
383
504
  if (images.length === 0) return 0;
505
+ const startedIn = session;
384
506
  await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
385
507
  const displayedWidth = image.width * image.scaleX;
386
508
  const displayedHeight = image.height * image.scaleY;
387
509
  await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
510
+ ensureSameSession(startedIn, "Replacing the image");
388
511
  image.set({
389
512
  scaleX: displayedWidth / image.width,
390
513
  scaleY: displayedHeight / image.height
@@ -400,8 +523,8 @@ function createDocumentEngine(options) {
400
523
  }
401
524
  async function checkBeforeExport(exportOptions) {
402
525
  ensureUsable();
403
- const { format } = require_export_options.normalizeExportOptions(exportOptions);
404
- return require_preflight_export.preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
526
+ const { format, svg } = require_export_options.normalizeExportOptions(exportOptions);
527
+ return require_preflight_export.preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions, svg);
405
528
  }
406
529
  function describeProblems(check) {
407
530
  return check.problems.map((problem) => problem.message).join("; ");
@@ -430,24 +553,58 @@ function createDocumentEngine(options) {
430
553
  if (!check.ok) throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
431
554
  const format = settings.format;
432
555
  try {
556
+ const warnings = [...check.warnings];
433
557
  const rendered = require_render_export.withExportView(canvas, settings.background, format, () => {
434
558
  const area = require_export_area.resolveExportArea(canvas, settings.area, settings.padding);
559
+ if (format !== "svg") {
560
+ const problem = require_content_limits.canvasSizeProblem(area.width * settings.scale, area.height * settings.scale, options.limits);
561
+ if (problem) {
562
+ const message = `The export would be ${problem}. Lower the scale or export a smaller area.`;
563
+ throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", message, { problems: [{
564
+ code: "TOO_LARGE",
565
+ message,
566
+ objectIds: []
567
+ }] });
568
+ }
569
+ }
435
570
  return {
436
571
  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)
572
+ content: format === "svg" ? Promise.resolve(require_render_export.renderSvg(canvas, area, settings.scale, settings.svg, options.limits)) : require_render_export.renderRaster(canvas, area, format, settings.scale, settings.quality)
438
573
  };
439
574
  });
440
- const blob = await rendered.content;
575
+ let blob;
576
+ if (format === "svg") {
577
+ const { svg, rasterizedObjectIds } = await rendered.content;
578
+ if (rasterizedObjectIds.length > 0) warnings.push({
579
+ code: "CLIP_PATH_RASTERIZED",
580
+ message: "Objects whose clip path has a clip path of its own were drawn as pictures: Fabric cannot write nested clip paths as SVG.",
581
+ objectIds: rasterizedObjectIds
582
+ });
583
+ const embedded = await require_embed_assets.embedSvgAssets(svg, canvas, settings.svg, settings.signal, options.limits?.isAllowedUrl ?? require_content_limits.isSafeImageUrl);
584
+ stopIfCancelled();
585
+ if (settings.svg.embedImages === "require" && embedded.problems.length > 0) {
586
+ const problems = embedded.warnings.filter((warning) => warning.code === "IMAGE_NOT_EMBEDDED").map((warning) => ({
587
+ code: "IMAGE_NOT_EMBEDDED",
588
+ message: warning.message,
589
+ url: warning.url,
590
+ objectIds: warning.objectIds
591
+ }));
592
+ throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${embedded.problems.join("; ")}`, { problems });
593
+ }
594
+ warnings.push(...embedded.warnings);
595
+ blob = new Blob([embedded.svg], { type: require_render_export.mimeTypes.svg });
596
+ } else blob = await rendered.content;
441
597
  return {
442
598
  format,
443
599
  mimeType: blob.type || require_render_export.mimeTypes[format],
444
600
  blob,
445
601
  width: Math.round(rendered.area.width * settings.scale),
446
602
  height: Math.round(rendered.area.height * settings.scale),
447
- warnings: check.warnings
603
+ warnings
448
604
  };
449
605
  } catch (error) {
450
606
  if (require_errors.isDocumentEngineError(error)) throw error;
607
+ if (settings.signal?.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "The export was cancelled", { cause: error });
451
608
  if (error?.name === "SecurityError") {
452
609
  const problem = {
453
610
  code: "CROSS_ORIGIN_IMAGE",
@@ -485,12 +642,14 @@ function createDocumentEngine(options) {
485
642
  if (!recovery) throw new require_errors.DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
486
643
  return recovery;
487
644
  }
488
- async function restoreRecovery(documentId, loadOptions) {
645
+ async function restoreRecovery(documentId, restoreOptions = {}) {
489
646
  const source = requireRecovery();
490
647
  const id = documentId ?? documentInfo.id;
491
- const record = await source.read(id);
648
+ const { sessionId: branch, ...loadOptions } = restoreOptions;
649
+ const record = await source.read(id, branch);
492
650
  if (!record) throw new require_errors.DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
493
651
  const loaded = await loadDocument(await require_recovery_controller.restoreRecordedFiles(record), loadOptions);
652
+ source.adopt(loaded.id, record.sessionId);
494
653
  noteContentChange();
495
654
  events.emit("recovery:restored", { document: loaded });
496
655
  return loaded;
@@ -511,12 +670,15 @@ function createDocumentEngine(options) {
511
670
  const { disposed } = canvas;
512
671
  if (recovery && saving.state().isDirty && !disposed) recovery.writeNow();
513
672
  destroyed = true;
673
+ session += 1;
514
674
  activeLoad?.abort();
515
675
  history.destroy();
516
676
  saving.destroy();
517
677
  recovery?.destroy();
518
678
  canvas.off("object:added", handleObjectAdded);
519
679
  canvas.off("object:removed", handleObjectRemoved);
680
+ canvas.off("object:modified", countMutation);
681
+ canvas.off("text:changed", countMutation);
520
682
  objectsById.clear();
521
683
  events.clear();
522
684
  }
@@ -524,7 +686,8 @@ function createDocumentEngine(options) {
524
686
  canvas,
525
687
  getDocumentInfo: () => ({
526
688
  ...documentInfo,
527
- metadata: { ...documentInfo.metadata }
689
+ metadata: { ...documentInfo.metadata },
690
+ session
528
691
  }),
529
692
  updateMetadata(changes) {
530
693
  ensureUsable();
@@ -542,6 +705,7 @@ function createDocumentEngine(options) {
542
705
  loadDocument,
543
706
  load,
544
707
  importFabricJson,
708
+ importSvg,
545
709
  createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
546
710
  listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
547
711
  restoreVersion,
@@ -564,14 +728,15 @@ function createDocumentEngine(options) {
564
728
  preflightExport: checkBeforeExport,
565
729
  flushRecovery: async () => requireRecovery().flush(),
566
730
  getRecoverableDocuments: async () => requireRecovery().list(),
567
- getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
731
+ getRecovery: async (documentId, sessionId) => requireRecovery().read(documentId ?? documentInfo.id, sessionId),
568
732
  restoreRecovery,
569
- discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
733
+ discardRecovery: async (documentId, sessionId) => requireRecovery().discard(documentId ?? documentInfo.id, sessionId),
570
734
  getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
571
- transaction(label, work) {
735
+ transaction(label, work, transactionOptions) {
572
736
  ensureUsable();
573
- return history.transaction(label, work);
737
+ return history.transaction(label, work, transactionOptions);
574
738
  },
739
+ setPage,
575
740
  commit(label = "Edit") {
576
741
  ensureUsable();
577
742
  return history.commit(label);
@@ -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
- import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
9
- import { ContentLimits } from "../security/content-limits.cjs";
10
+ import { HistoryOptions, HistoryState, TransactionOptions } from "../history/create-history.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";
@@ -15,7 +16,7 @@ import { DocumentEngineError } from "./errors.cjs";
15
16
  import { DocumentStorage } from "../storage/storage-contract.cjs";
16
17
  import { SaveOptions, SaveRetryEvent, SaveState } from "../save/save-controller.cjs";
17
18
  import { Unsubscribe } from "./event-emitter.cjs";
18
- import { FabricObject, StaticCanvas } from "fabric";
19
+ import { FabricObject, StaticCanvas, TFiller } from "fabric";
19
20
  //#region src/engine/create-document-engine.d.ts
20
21
  export interface DocumentEngineOptions {
21
22
  canvas: StaticCanvas;
@@ -33,6 +34,39 @@ 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;
53
+ }
54
+ /**
55
+ * Page settings for `setPage`. Images and the mask are live Fabric objects;
56
+ * `null` removes one.
57
+ */
58
+ export interface PageChanges {
59
+ width?: number;
60
+ height?: number;
61
+ background?: string | TFiller | null;
62
+ backgroundImage?: FabricObject | null;
63
+ overlay?: string | TFiller | null;
64
+ overlayImage?: FabricObject | null;
65
+ clipPath?: FabricObject | null;
66
+ }
67
+ export interface RestoreRecoveryOptions extends LoadOptions {
68
+ /** Restore the copy one session wrote. By default the newest copy of the document is used. */
69
+ sessionId?: string;
36
70
  }
37
71
  export interface NewDocumentRequest extends NewDocumentOptions {
38
72
  discardUnsavedChanges?: boolean;
@@ -54,6 +88,7 @@ export interface DocumentEngineEvents {
54
88
  "load:start": {
55
89
  documentId: string | undefined;
56
90
  };
91
+ "load:progress": LoadProgress;
57
92
  "document:change": {
58
93
  documentId: string;
59
94
  };
@@ -120,6 +155,7 @@ export interface DocumentEngine {
120
155
  loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
121
156
  load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
122
157
  importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
158
+ importSvg(svg: string, options?: SvgImportOptions): Promise<SvgImportResult>;
123
159
  createVersion(name?: string): Promise<VersionSummary>;
124
160
  listVersions(documentId?: string): Promise<VersionSummary[]>;
125
161
  restoreVersion(versionId: string): Promise<FabricDocument>;
@@ -128,7 +164,8 @@ export interface DocumentEngine {
128
164
  isDirty(): boolean;
129
165
  getSaveState(): SaveState;
130
166
  registerObject(definition: CustomObjectDefinition): void;
131
- transaction<Result>(label: string, work: () => Result): Result;
167
+ transaction<Result>(label: string, work: () => Result, options?: TransactionOptions): Result;
168
+ setPage(changes: PageChanges, label?: string): void;
132
169
  commit(label?: string): boolean;
133
170
  undo(): Promise<boolean>;
134
171
  redo(): Promise<boolean>;
@@ -147,9 +184,9 @@ export interface DocumentEngine {
147
184
  preflightExport(options: ExportOptions): Promise<ExportPreflight>;
148
185
  flushRecovery(): Promise<void>;
149
186
  getRecoverableDocuments(): Promise<RecoveryRecord[]>;
150
- getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
151
- restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
152
- discardRecovery(documentId?: string): Promise<void>;
187
+ getRecovery(documentId?: string, sessionId?: string): Promise<RecoveryRecord | undefined>;
188
+ restoreRecovery(documentId?: string, options?: RestoreRecoveryOptions): Promise<FabricDocument>;
189
+ discardRecovery(documentId?: string, sessionId?: string): Promise<void>;
153
190
  getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
154
191
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
155
192
  destroy(): void;
@@ -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
- import { HistoryOptions, HistoryState } from "../history/create-history.js";
9
- import { ContentLimits } from "../security/content-limits.js";
10
+ import { HistoryOptions, HistoryState, TransactionOptions } from "../history/create-history.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";
@@ -15,7 +16,7 @@ import { DocumentEngineError } from "./errors.js";
15
16
  import { DocumentStorage } from "../storage/storage-contract.js";
16
17
  import { SaveOptions, SaveRetryEvent, SaveState } from "../save/save-controller.js";
17
18
  import { Unsubscribe } from "./event-emitter.js";
18
- import { FabricObject, StaticCanvas } from "fabric";
19
+ import { FabricObject, StaticCanvas, TFiller } from "fabric";
19
20
  //#region src/engine/create-document-engine.d.ts
20
21
  export interface DocumentEngineOptions {
21
22
  canvas: StaticCanvas;
@@ -33,6 +34,39 @@ 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;
53
+ }
54
+ /**
55
+ * Page settings for `setPage`. Images and the mask are live Fabric objects;
56
+ * `null` removes one.
57
+ */
58
+ export interface PageChanges {
59
+ width?: number;
60
+ height?: number;
61
+ background?: string | TFiller | null;
62
+ backgroundImage?: FabricObject | null;
63
+ overlay?: string | TFiller | null;
64
+ overlayImage?: FabricObject | null;
65
+ clipPath?: FabricObject | null;
66
+ }
67
+ export interface RestoreRecoveryOptions extends LoadOptions {
68
+ /** Restore the copy one session wrote. By default the newest copy of the document is used. */
69
+ sessionId?: string;
36
70
  }
37
71
  export interface NewDocumentRequest extends NewDocumentOptions {
38
72
  discardUnsavedChanges?: boolean;
@@ -54,6 +88,7 @@ export interface DocumentEngineEvents {
54
88
  "load:start": {
55
89
  documentId: string | undefined;
56
90
  };
91
+ "load:progress": LoadProgress;
57
92
  "document:change": {
58
93
  documentId: string;
59
94
  };
@@ -120,6 +155,7 @@ export interface DocumentEngine {
120
155
  loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
121
156
  load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
122
157
  importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
158
+ importSvg(svg: string, options?: SvgImportOptions): Promise<SvgImportResult>;
123
159
  createVersion(name?: string): Promise<VersionSummary>;
124
160
  listVersions(documentId?: string): Promise<VersionSummary[]>;
125
161
  restoreVersion(versionId: string): Promise<FabricDocument>;
@@ -128,7 +164,8 @@ export interface DocumentEngine {
128
164
  isDirty(): boolean;
129
165
  getSaveState(): SaveState;
130
166
  registerObject(definition: CustomObjectDefinition): void;
131
- transaction<Result>(label: string, work: () => Result): Result;
167
+ transaction<Result>(label: string, work: () => Result, options?: TransactionOptions): Result;
168
+ setPage(changes: PageChanges, label?: string): void;
132
169
  commit(label?: string): boolean;
133
170
  undo(): Promise<boolean>;
134
171
  redo(): Promise<boolean>;
@@ -147,9 +184,9 @@ export interface DocumentEngine {
147
184
  preflightExport(options: ExportOptions): Promise<ExportPreflight>;
148
185
  flushRecovery(): Promise<void>;
149
186
  getRecoverableDocuments(): Promise<RecoveryRecord[]>;
150
- getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
151
- restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
152
- discardRecovery(documentId?: string): Promise<void>;
187
+ getRecovery(documentId?: string, sessionId?: string): Promise<RecoveryRecord | undefined>;
188
+ restoreRecovery(documentId?: string, options?: RestoreRecoveryOptions): Promise<FabricDocument>;
189
+ discardRecovery(documentId?: string, sessionId?: string): Promise<void>;
153
190
  getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
154
191
  on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
155
192
  destroy(): void;