fabricjs-document-engine 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +88 -3
  2. package/dist/assets/asset-pipeline.cjs +1 -0
  3. package/dist/assets/asset-pipeline.js +1 -1
  4. package/dist/engine/create-document-engine.cjs +150 -7
  5. package/dist/engine/create-document-engine.d.cts +40 -0
  6. package/dist/engine/create-document-engine.d.ts +40 -0
  7. package/dist/engine/create-document-engine.js +150 -7
  8. package/dist/engine/errors.cjs +1 -0
  9. package/dist/engine/errors.d.cts +4 -1
  10. package/dist/engine/errors.d.ts +4 -1
  11. package/dist/engine/errors.js +1 -0
  12. package/dist/export/download-export.cjs +14 -0
  13. package/dist/export/download-export.d.cts +7 -0
  14. package/dist/export/download-export.d.ts +7 -0
  15. package/dist/export/download-export.js +14 -0
  16. package/dist/export/export-area.cjs +46 -0
  17. package/dist/export/export-area.js +46 -0
  18. package/dist/export/export-options.cjs +47 -0
  19. package/dist/export/export-options.d.cts +20 -0
  20. package/dist/export/export-options.d.ts +20 -0
  21. package/dist/export/export-options.js +46 -0
  22. package/dist/export/preflight-export.cjs +105 -0
  23. package/dist/export/preflight-export.d.cts +17 -0
  24. package/dist/export/preflight-export.d.ts +17 -0
  25. package/dist/export/preflight-export.js +105 -0
  26. package/dist/export/render-export.cjs +69 -0
  27. package/dist/export/render-export.js +65 -0
  28. package/dist/history/apply-state.cjs +1 -1
  29. package/dist/history/apply-state.js +1 -1
  30. package/dist/index.cjs +2 -0
  31. package/dist/index.d.cts +7 -2
  32. package/dist/index.d.ts +7 -2
  33. package/dist/index.js +2 -1
  34. package/dist/recovery/indexed-db-recovery.cjs +80 -0
  35. package/dist/recovery/indexed-db-recovery.d.cts +8 -0
  36. package/dist/recovery/indexed-db-recovery.d.ts +8 -0
  37. package/dist/recovery/indexed-db-recovery.js +80 -0
  38. package/dist/recovery/recovery-controller.cjs +159 -0
  39. package/dist/recovery/recovery-controller.d.cts +19 -0
  40. package/dist/recovery/recovery-controller.d.ts +19 -0
  41. package/dist/recovery/recovery-controller.js +158 -0
  42. package/dist/recovery/recovery-store.cjs +19 -0
  43. package/dist/recovery/recovery-store.d.cts +10 -0
  44. package/dist/recovery/recovery-store.d.ts +10 -0
  45. package/dist/recovery/recovery-store.js +19 -0
  46. package/dist/recovery.cjs +5 -0
  47. package/dist/recovery.d.cts +4 -0
  48. package/dist/recovery.d.ts +4 -0
  49. package/dist/recovery.js +3 -0
  50. package/package.json +13 -2
@@ -2,15 +2,20 @@ import { buildAssetManifest } from "../assets/asset-manifest.js";
2
2
  import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
3
3
  import { isSameUrl } from "../assets/image-check.js";
4
4
  import { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave } from "../assets/asset-pipeline.js";
5
+ import { resolveExportArea } from "../export/export-area.js";
6
+ import { normalizeExportOptions } from "../export/export-options.js";
7
+ import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
8
+ import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
9
+ import { preflightExport } from "../export/preflight-export.js";
10
+ import { dataUrlToBlob, mimeTypes, renderRaster, renderSvg, withExportView } from "../export/render-export.js";
5
11
  import "../document/document-format.js";
6
12
  import { createId } from "../document/ids.js";
7
13
  import { createDocumentInfo } from "../document/create-document.js";
8
14
  import { validateDocument } from "../document/validate-document.js";
9
- import { collectSerializedTypes, walkObjects } from "../fabric/walk-objects.js";
10
15
  import { loadIntoCanvas, serializeCanvas } from "../fabric/fabric-adapter.js";
11
- import { readObjectId, writeObjectId } from "../fabric/object-ids.js";
12
16
  import { createObjectRegistry } from "../fabric/object-registry.js";
13
17
  import { createHistory } from "../history/create-history.js";
18
+ import { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
14
19
  import { createSaveController } from "../save/save-controller.js";
15
20
  import { createEventEmitter } from "./event-emitter.js";
16
21
  //#region src/engine/create-document-engine.ts
@@ -70,7 +75,7 @@ function createDocumentEngine(options) {
70
75
  return serializeCanvas(canvas, registry.propertiesToInclude()).objects;
71
76
  },
72
77
  onChange: (state) => events.emit("history:change", state),
73
- onContentChange: () => saving.noteContentChange()
78
+ onContentChange: () => noteContentChange()
74
79
  });
75
80
  const saving = createSaveController({
76
81
  getStorage: () => requireStorage(),
@@ -83,10 +88,30 @@ function createDocumentEngine(options) {
83
88
  autosave: options.autosave === true ? {} : options.autosave ?? false,
84
89
  onStateChange: (state) => events.emit("save:status", state),
85
90
  onStart: (document) => events.emit("save:start", { document }),
86
- onSuccess: (document) => events.emit("save:success", { document }),
91
+ onSuccess: (document) => {
92
+ if (recovery && !saving.state().isDirty) recovery.remove(document.id);
93
+ events.emit("save:success", { document });
94
+ },
87
95
  onError: (error) => events.emit("save:error", { error }),
88
96
  onRetry: (event) => events.emit("save:retry", event)
89
97
  });
98
+ const recovery = options.recovery ? createRecoveryController({
99
+ ...options.recovery,
100
+ createDocument: () => toDocument(),
101
+ shouldWrite: () => !destroyed && saving.state().isDirty,
102
+ onCheckpoint: (record) => events.emit("recovery:checkpoint", {
103
+ documentId: record.documentId,
104
+ savedAt: record.savedAt
105
+ }),
106
+ onError: (error) => {
107
+ const reason = error instanceof Error ? error.message : String(error);
108
+ events.emit("recovery:error", { error: new DocumentEngineError("RECOVERY_FAILED", `Could not write the recovery copy: ${reason}`, { cause: error }) });
109
+ }
110
+ }) : void 0;
111
+ function noteContentChange() {
112
+ saving.noteContentChange();
113
+ recovery?.schedule();
114
+ }
90
115
  function toDocument() {
91
116
  ensureUsable();
92
117
  rebuildIndex();
@@ -135,8 +160,11 @@ function createDocumentEngine(options) {
135
160
  activeLoad?.abort();
136
161
  const controller = new AbortController();
137
162
  activeLoad = controller;
138
- events.emit("load:start", { documentId: describeDocumentId(input) });
163
+ const documentId = describeDocumentId(input);
164
+ events.emit("load:start", { documentId });
139
165
  try {
166
+ await recovery?.markLoadStarted(documentId);
167
+ if (controller.signal.aborted) throw new Error("aborted");
140
168
  const checked = checkDocument(input);
141
169
  const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
142
170
  if (controller.signal.aborted) throw new Error("aborted");
@@ -158,6 +186,7 @@ function createDocumentEngine(options) {
158
186
  };
159
187
  rebuildIndex();
160
188
  history.reset();
189
+ recovery?.cancel();
161
190
  saving.startSession(document.revision ?? 0);
162
191
  canvas.requestRenderAll();
163
192
  if (warnings.length > 0) events.emit("assets:warning", { warnings });
@@ -171,7 +200,10 @@ function createDocumentEngine(options) {
171
200
  events.emit("load:error", { error: engineError });
172
201
  throw engineError;
173
202
  } finally {
174
- if (activeLoad === controller) activeLoad = null;
203
+ if (activeLoad === controller) {
204
+ activeLoad = null;
205
+ await recovery?.markLoadFinished();
206
+ }
175
207
  }
176
208
  }
177
209
  function requireStorage() {
@@ -208,6 +240,7 @@ function createDocumentEngine(options) {
208
240
  documentInfo = createDocumentInfo(newOptions);
209
241
  uploadedUrls.clear();
210
242
  history.reset();
243
+ recovery?.cancel();
211
244
  saving.startSession(0);
212
245
  }
213
246
  function getObjectById(id) {
@@ -241,6 +274,107 @@ function createDocumentEngine(options) {
241
274
  canvas.requestRenderAll();
242
275
  return images.length;
243
276
  }
277
+ function fontsOnCanvas() {
278
+ rebuildIndex();
279
+ return buildAssetManifest(serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
280
+ }
281
+ async function checkBeforeExport(exportOptions) {
282
+ ensureUsable();
283
+ const { format } = normalizeExportOptions(exportOptions);
284
+ return preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
285
+ }
286
+ function describeProblems(check) {
287
+ return check.problems.map((problem) => problem.message).join("; ");
288
+ }
289
+ async function exportContent(exportOptions) {
290
+ const settings = normalizeExportOptions(exportOptions);
291
+ const stopIfCancelled = () => {
292
+ if (settings.signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
293
+ };
294
+ stopIfCancelled();
295
+ if (settings.format === "json") {
296
+ const prepared = await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
297
+ stopIfCancelled();
298
+ return {
299
+ format: "json",
300
+ mimeType: mimeTypes.json,
301
+ blob: new Blob([JSON.stringify(prepared.document)], { type: mimeTypes.json }),
302
+ width: prepared.document.canvas.width,
303
+ height: prepared.document.canvas.height,
304
+ warnings: prepared.warnings,
305
+ document: prepared.document
306
+ };
307
+ }
308
+ const check = await checkBeforeExport(settings);
309
+ stopIfCancelled();
310
+ if (!check.ok) throw new DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
311
+ const format = settings.format;
312
+ try {
313
+ const rendered = withExportView(canvas, settings.background, format, () => {
314
+ const area = resolveExportArea(canvas, settings.area, settings.padding);
315
+ return {
316
+ area,
317
+ content: format === "svg" ? renderSvg(canvas, area, settings.scale) : renderRaster(canvas, area, format, settings.scale, settings.quality)
318
+ };
319
+ });
320
+ const blob = format === "svg" ? new Blob([rendered.content], { type: mimeTypes.svg }) : dataUrlToBlob(rendered.content);
321
+ return {
322
+ format,
323
+ mimeType: blob.type || mimeTypes[format],
324
+ blob,
325
+ width: Math.round(rendered.area.width * settings.scale),
326
+ height: Math.round(rendered.area.height * settings.scale),
327
+ warnings: check.warnings
328
+ };
329
+ } catch (error) {
330
+ if (isDocumentEngineError(error)) throw error;
331
+ if (error?.name === "SecurityError") {
332
+ const problem = {
333
+ code: "CROSS_ORIGIN_IMAGE",
334
+ message: "The browser blocked the export because the canvas shows an image from another site without CORS",
335
+ objectIds: []
336
+ };
337
+ throw new DocumentEngineError("EXPORT_BLOCKED", problem.message, {
338
+ problems: [problem],
339
+ cause: error
340
+ });
341
+ }
342
+ const reason = error instanceof Error ? error.message : String(error);
343
+ throw new DocumentEngineError("EXPORT_FAILED", `Fabric could not export the canvas: ${reason}`, { cause: error });
344
+ }
345
+ }
346
+ async function exportDocument(exportOptions) {
347
+ ensureUsable();
348
+ try {
349
+ const result = await exportContent(exportOptions);
350
+ events.emit("export:success", {
351
+ format: result.format,
352
+ width: result.width,
353
+ height: result.height,
354
+ warnings: result.warnings
355
+ });
356
+ return result;
357
+ } catch (error) {
358
+ const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
359
+ events.emit("export:error", { error: engineError });
360
+ throw engineError;
361
+ }
362
+ }
363
+ function requireRecovery() {
364
+ ensureUsable();
365
+ if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
366
+ return recovery;
367
+ }
368
+ async function restoreRecovery(documentId, loadOptions) {
369
+ const source = requireRecovery();
370
+ const id = documentId ?? documentInfo.id;
371
+ const record = await source.read(id);
372
+ if (!record) throw new DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
373
+ const loaded = await loadDocument(await restoreRecordedFiles(record), loadOptions);
374
+ noteContentChange();
375
+ events.emit("recovery:restored", { document: loaded });
376
+ return loaded;
377
+ }
244
378
  async function travelThroughHistory(direction) {
245
379
  ensureUsable();
246
380
  try {
@@ -258,6 +392,7 @@ function createDocumentEngine(options) {
258
392
  activeLoad?.abort();
259
393
  history.destroy();
260
394
  saving.destroy();
395
+ recovery?.destroy();
261
396
  canvas.off("object:added", handleObjectAdded);
262
397
  canvas.off("object:removed", handleObjectRemoved);
263
398
  objectsById.clear();
@@ -278,7 +413,7 @@ function createDocumentEngine(options) {
278
413
  ...changes
279
414
  }
280
415
  };
281
- saving.noteContentChange();
416
+ noteContentChange();
282
417
  },
283
418
  newDocument,
284
419
  toDocument,
@@ -298,6 +433,14 @@ function createDocumentEngine(options) {
298
433
  },
299
434
  checkAssets,
300
435
  replaceImage,
436
+ export: exportDocument,
437
+ preflightExport: checkBeforeExport,
438
+ flushRecovery: async () => requireRecovery().flush(),
439
+ getRecoverableDocuments: async () => requireRecovery().list(),
440
+ getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
441
+ restoreRecovery,
442
+ discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
443
+ getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
301
444
  transaction(label, work) {
302
445
  ensureUsable();
303
446
  return history.transaction(label, work);
@@ -8,6 +8,7 @@ var DocumentEngineError = class extends Error {
8
8
  this.unknownTypes = details.unknownTypes ?? [];
9
9
  this.missingAssets = details.missingAssets ?? [];
10
10
  this.missingFonts = details.missingFonts ?? [];
11
+ this.problems = details.problems ?? [];
11
12
  this.cause = details.cause;
12
13
  this.retryable = details.retryable ?? false;
13
14
  }
@@ -1,6 +1,7 @@
1
1
  import { FontAsset, ImageAsset } from "../assets/asset-manifest.cjs";
2
+ import { ExportProblem } from "../export/preflight-export.cjs";
2
3
  //#region src/engine/errors.d.ts
3
- export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
4
+ export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
4
5
  export interface DocumentIssue {
5
6
  code: DocumentErrorCode;
6
7
  path: string;
@@ -11,6 +12,7 @@ export interface DocumentEngineErrorDetails {
11
12
  unknownTypes?: string[];
12
13
  missingAssets?: ImageAsset[];
13
14
  missingFonts?: FontAsset[];
15
+ problems?: ExportProblem[];
14
16
  cause?: unknown;
15
17
  retryable?: boolean;
16
18
  }
@@ -20,6 +22,7 @@ export declare class DocumentEngineError extends Error {
20
22
  readonly unknownTypes: string[];
21
23
  readonly missingAssets: ImageAsset[];
22
24
  readonly missingFonts: FontAsset[];
25
+ readonly problems: ExportProblem[];
23
26
  readonly cause: unknown;
24
27
  readonly retryable: boolean;
25
28
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
@@ -1,6 +1,7 @@
1
1
  import { FontAsset, ImageAsset } from "../assets/asset-manifest.js";
2
+ import { ExportProblem } from "../export/preflight-export.js";
2
3
  //#region src/engine/errors.d.ts
3
- export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "ENGINE_DESTROYED";
4
+ export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "ENGINE_DESTROYED";
4
5
  export interface DocumentIssue {
5
6
  code: DocumentErrorCode;
6
7
  path: string;
@@ -11,6 +12,7 @@ export interface DocumentEngineErrorDetails {
11
12
  unknownTypes?: string[];
12
13
  missingAssets?: ImageAsset[];
13
14
  missingFonts?: FontAsset[];
15
+ problems?: ExportProblem[];
14
16
  cause?: unknown;
15
17
  retryable?: boolean;
16
18
  }
@@ -20,6 +22,7 @@ export declare class DocumentEngineError extends Error {
20
22
  readonly unknownTypes: string[];
21
23
  readonly missingAssets: ImageAsset[];
22
24
  readonly missingFonts: FontAsset[];
25
+ readonly problems: ExportProblem[];
23
26
  readonly cause: unknown;
24
27
  readonly retryable: boolean;
25
28
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
@@ -8,6 +8,7 @@ var DocumentEngineError = class extends Error {
8
8
  this.unknownTypes = details.unknownTypes ?? [];
9
9
  this.missingAssets = details.missingAssets ?? [];
10
10
  this.missingFonts = details.missingFonts ?? [];
11
+ this.problems = details.problems ?? [];
11
12
  this.cause = details.cause;
12
13
  this.retryable = details.retryable ?? false;
13
14
  }
@@ -0,0 +1,14 @@
1
+ //#region src/export/download-export.ts
2
+ function downloadExport(result, fileName = `document.${result.format === "jpeg" ? "jpg" : result.format}`) {
3
+ const url = URL.createObjectURL(result.blob);
4
+ const link = document.createElement("a");
5
+ link.href = url;
6
+ link.download = fileName;
7
+ link.style.display = "none";
8
+ document.body.append(link);
9
+ link.click();
10
+ link.remove();
11
+ setTimeout(() => URL.revokeObjectURL(url), 0);
12
+ }
13
+ //#endregion
14
+ exports.downloadExport = downloadExport;
@@ -0,0 +1,7 @@
1
+ //#region src/export/download-export.d.ts
2
+ export interface DownloadableExport {
3
+ blob: Blob;
4
+ format: string;
5
+ }
6
+ export declare function downloadExport(result: DownloadableExport, fileName?: string): void;
7
+ //#endregion
@@ -0,0 +1,7 @@
1
+ //#region src/export/download-export.d.ts
2
+ export interface DownloadableExport {
3
+ blob: Blob;
4
+ format: string;
5
+ }
6
+ export declare function downloadExport(result: DownloadableExport, fileName?: string): void;
7
+ //#endregion
@@ -0,0 +1,14 @@
1
+ //#region src/export/download-export.ts
2
+ function downloadExport(result, fileName = `document.${result.format === "jpeg" ? "jpg" : result.format}`) {
3
+ const url = URL.createObjectURL(result.blob);
4
+ const link = document.createElement("a");
5
+ link.href = url;
6
+ link.download = fileName;
7
+ link.style.display = "none";
8
+ document.body.append(link);
9
+ link.click();
10
+ link.remove();
11
+ setTimeout(() => URL.revokeObjectURL(url), 0);
12
+ }
13
+ //#endregion
14
+ export { downloadExport };
@@ -0,0 +1,46 @@
1
+ const require_errors = require("../engine/errors.cjs");
2
+ //#region src/export/export-area.ts
3
+ function boundsOf(objects) {
4
+ if (objects.length === 0) return null;
5
+ let left = Infinity;
6
+ let top = Infinity;
7
+ let right = -Infinity;
8
+ let bottom = -Infinity;
9
+ for (const object of objects) {
10
+ const box = object.getBoundingRect();
11
+ left = Math.min(left, box.left);
12
+ top = Math.min(top, box.top);
13
+ right = Math.max(right, box.left + box.width);
14
+ bottom = Math.max(bottom, box.top + box.height);
15
+ }
16
+ return {
17
+ left,
18
+ top,
19
+ width: right - left,
20
+ height: bottom - top
21
+ };
22
+ }
23
+ function padAndRound(rect, padding) {
24
+ const left = Math.floor(rect.left - padding);
25
+ const top = Math.floor(rect.top - padding);
26
+ return {
27
+ left,
28
+ top,
29
+ width: Math.max(1, Math.ceil(rect.left + rect.width + padding) - left),
30
+ height: Math.max(1, Math.ceil(rect.top + rect.height + padding) - top)
31
+ };
32
+ }
33
+ function resolveExportArea(canvas, area, padding) {
34
+ if (typeof area === "object") return padAndRound(area, padding);
35
+ if (area === "canvas") return {
36
+ left: 0,
37
+ top: 0,
38
+ width: canvas.getWidth(),
39
+ height: canvas.getHeight()
40
+ };
41
+ const bounds = boundsOf(area === "selection" ? canvas.getActiveObjects?.() ?? [] : canvas.getObjects());
42
+ if (bounds === null) throw new require_errors.DocumentEngineError("INVALID_EXPORT_OPTIONS", `Cannot export the ${area} because ${area === "selection" ? "nothing is selected" : "the canvas is empty"}`);
43
+ return padAndRound(bounds, padding);
44
+ }
45
+ //#endregion
46
+ exports.resolveExportArea = resolveExportArea;
@@ -0,0 +1,46 @@
1
+ import { DocumentEngineError } from "../engine/errors.js";
2
+ //#region src/export/export-area.ts
3
+ function boundsOf(objects) {
4
+ if (objects.length === 0) return null;
5
+ let left = Infinity;
6
+ let top = Infinity;
7
+ let right = -Infinity;
8
+ let bottom = -Infinity;
9
+ for (const object of objects) {
10
+ const box = object.getBoundingRect();
11
+ left = Math.min(left, box.left);
12
+ top = Math.min(top, box.top);
13
+ right = Math.max(right, box.left + box.width);
14
+ bottom = Math.max(bottom, box.top + box.height);
15
+ }
16
+ return {
17
+ left,
18
+ top,
19
+ width: right - left,
20
+ height: bottom - top
21
+ };
22
+ }
23
+ function padAndRound(rect, padding) {
24
+ const left = Math.floor(rect.left - padding);
25
+ const top = Math.floor(rect.top - padding);
26
+ return {
27
+ left,
28
+ top,
29
+ width: Math.max(1, Math.ceil(rect.left + rect.width + padding) - left),
30
+ height: Math.max(1, Math.ceil(rect.top + rect.height + padding) - top)
31
+ };
32
+ }
33
+ function resolveExportArea(canvas, area, padding) {
34
+ if (typeof area === "object") return padAndRound(area, padding);
35
+ if (area === "canvas") return {
36
+ left: 0,
37
+ top: 0,
38
+ width: canvas.getWidth(),
39
+ height: canvas.getHeight()
40
+ };
41
+ const bounds = boundsOf(area === "selection" ? canvas.getActiveObjects?.() ?? [] : canvas.getObjects());
42
+ if (bounds === null) throw new DocumentEngineError("INVALID_EXPORT_OPTIONS", `Cannot export the ${area} because ${area === "selection" ? "nothing is selected" : "the canvas is empty"}`);
43
+ return padAndRound(bounds, padding);
44
+ }
45
+ //#endregion
46
+ export { resolveExportArea };
@@ -0,0 +1,47 @@
1
+ const require_errors = require("../engine/errors.cjs");
2
+ //#region src/export/export-options.ts
3
+ const formats = /* @__PURE__ */ new Set([
4
+ "png",
5
+ "jpeg",
6
+ "webp",
7
+ "svg",
8
+ "json"
9
+ ]);
10
+ function invalid(message) {
11
+ return new require_errors.DocumentEngineError("INVALID_EXPORT_OPTIONS", message);
12
+ }
13
+ function isPositive(value) {
14
+ return Number.isFinite(value) && value > 0;
15
+ }
16
+ function isRasterFormat(format) {
17
+ return format === "png" || format === "jpeg" || format === "webp";
18
+ }
19
+ function normalizeExportOptions(options) {
20
+ if (!formats.has(options.format)) throw invalid(`Unknown export format "${String(options.format)}", use png, jpeg, webp, svg or json`);
21
+ const scale = options.scale ?? 1;
22
+ if (!isPositive(scale)) throw invalid("scale must be a number greater than zero");
23
+ const quality = options.quality ?? .92;
24
+ if (!(quality >= 0 && quality <= 1)) throw invalid("quality must be between 0 and 1");
25
+ const padding = options.padding ?? 0;
26
+ if (!(Number.isFinite(padding) && padding >= 0)) throw invalid("padding must be zero or more");
27
+ const area = options.area ?? "canvas";
28
+ if (typeof area === "object") {
29
+ if (![area.left, area.top].every(Number.isFinite) || !isPositive(area.width) || !isPositive(area.height)) throw invalid("area needs finite left and top and a positive width and height");
30
+ } else if (![
31
+ "canvas",
32
+ "content",
33
+ "selection"
34
+ ].includes(area)) throw invalid(`Unknown export area "${String(area)}", use canvas, content, selection or a rectangle`);
35
+ return {
36
+ format: options.format,
37
+ scale,
38
+ quality,
39
+ area,
40
+ padding,
41
+ background: options.background ?? "keep",
42
+ signal: options.signal
43
+ };
44
+ }
45
+ //#endregion
46
+ exports.isRasterFormat = isRasterFormat;
47
+ exports.normalizeExportOptions = normalizeExportOptions;
@@ -0,0 +1,20 @@
1
+ //#region src/export/export-options.d.ts
2
+ export type ExportFormat = "png" | "jpeg" | "webp" | "svg" | "json";
3
+ export interface ExportRect {
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export type ExportArea = "canvas" | "content" | "selection" | ExportRect;
10
+ export type ExportBackground = "keep" | "transparent" | (string & {});
11
+ export interface ExportOptions {
12
+ format: ExportFormat;
13
+ scale?: number;
14
+ quality?: number;
15
+ area?: ExportArea;
16
+ padding?: number;
17
+ background?: ExportBackground;
18
+ signal?: AbortSignal;
19
+ }
20
+ //#endregion
@@ -0,0 +1,20 @@
1
+ //#region src/export/export-options.d.ts
2
+ export type ExportFormat = "png" | "jpeg" | "webp" | "svg" | "json";
3
+ export interface ExportRect {
4
+ left: number;
5
+ top: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export type ExportArea = "canvas" | "content" | "selection" | ExportRect;
10
+ export type ExportBackground = "keep" | "transparent" | (string & {});
11
+ export interface ExportOptions {
12
+ format: ExportFormat;
13
+ scale?: number;
14
+ quality?: number;
15
+ area?: ExportArea;
16
+ padding?: number;
17
+ background?: ExportBackground;
18
+ signal?: AbortSignal;
19
+ }
20
+ //#endregion
@@ -0,0 +1,46 @@
1
+ import { DocumentEngineError } from "../engine/errors.js";
2
+ //#region src/export/export-options.ts
3
+ const formats = /* @__PURE__ */ new Set([
4
+ "png",
5
+ "jpeg",
6
+ "webp",
7
+ "svg",
8
+ "json"
9
+ ]);
10
+ function invalid(message) {
11
+ return new DocumentEngineError("INVALID_EXPORT_OPTIONS", message);
12
+ }
13
+ function isPositive(value) {
14
+ return Number.isFinite(value) && value > 0;
15
+ }
16
+ function isRasterFormat(format) {
17
+ return format === "png" || format === "jpeg" || format === "webp";
18
+ }
19
+ function normalizeExportOptions(options) {
20
+ if (!formats.has(options.format)) throw invalid(`Unknown export format "${String(options.format)}", use png, jpeg, webp, svg or json`);
21
+ const scale = options.scale ?? 1;
22
+ if (!isPositive(scale)) throw invalid("scale must be a number greater than zero");
23
+ const quality = options.quality ?? .92;
24
+ if (!(quality >= 0 && quality <= 1)) throw invalid("quality must be between 0 and 1");
25
+ const padding = options.padding ?? 0;
26
+ if (!(Number.isFinite(padding) && padding >= 0)) throw invalid("padding must be zero or more");
27
+ const area = options.area ?? "canvas";
28
+ if (typeof area === "object") {
29
+ if (![area.left, area.top].every(Number.isFinite) || !isPositive(area.width) || !isPositive(area.height)) throw invalid("area needs finite left and top and a positive width and height");
30
+ } else if (![
31
+ "canvas",
32
+ "content",
33
+ "selection"
34
+ ].includes(area)) throw invalid(`Unknown export area "${String(area)}", use canvas, content, selection or a rectangle`);
35
+ return {
36
+ format: options.format,
37
+ scale,
38
+ quality,
39
+ area,
40
+ padding,
41
+ background: options.background ?? "keep",
42
+ signal: options.signal
43
+ };
44
+ }
45
+ //#endregion
46
+ export { isRasterFormat, normalizeExportOptions };