fabricjs-document-engine 0.4.0 → 0.6.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 (46) hide show
  1. package/README.md +87 -3
  2. package/dist/engine/create-document-engine.cjs +199 -6
  3. package/dist/engine/create-document-engine.d.cts +42 -0
  4. package/dist/engine/create-document-engine.d.ts +42 -0
  5. package/dist/engine/create-document-engine.js +199 -6
  6. package/dist/engine/errors.cjs +2 -0
  7. package/dist/engine/errors.d.cts +6 -1
  8. package/dist/engine/errors.d.ts +6 -1
  9. package/dist/engine/errors.js +2 -0
  10. package/dist/export/download-export.cjs +14 -0
  11. package/dist/export/download-export.d.cts +7 -0
  12. package/dist/export/download-export.d.ts +7 -0
  13. package/dist/export/download-export.js +14 -0
  14. package/dist/export/export-area.cjs +46 -0
  15. package/dist/export/export-area.js +46 -0
  16. package/dist/export/export-options.cjs +47 -0
  17. package/dist/export/export-options.d.cts +20 -0
  18. package/dist/export/export-options.d.ts +20 -0
  19. package/dist/export/export-options.js +46 -0
  20. package/dist/export/preflight-export.cjs +105 -0
  21. package/dist/export/preflight-export.d.cts +17 -0
  22. package/dist/export/preflight-export.d.ts +17 -0
  23. package/dist/export/preflight-export.js +105 -0
  24. package/dist/export/render-export.cjs +69 -0
  25. package/dist/export/render-export.js +65 -0
  26. package/dist/history/apply-state.cjs +1 -1
  27. package/dist/history/apply-state.js +1 -1
  28. package/dist/index.cjs +5 -0
  29. package/dist/index.d.cts +7 -2
  30. package/dist/index.d.ts +7 -2
  31. package/dist/index.js +3 -1
  32. package/dist/migrations/migrate-document.cjs +65 -0
  33. package/dist/migrations/migrate-document.d.cts +15 -0
  34. package/dist/migrations/migrate-document.d.ts +15 -0
  35. package/dist/migrations/migrate-document.js +63 -0
  36. package/dist/storage/key-value-storage.cjs +27 -1
  37. package/dist/storage/key-value-storage.d.cts +2 -1
  38. package/dist/storage/key-value-storage.d.ts +2 -1
  39. package/dist/storage/key-value-storage.js +27 -1
  40. package/dist/storage.d.cts +2 -1
  41. package/dist/storage.d.ts +2 -1
  42. package/dist/versions/document-version.cjs +20 -0
  43. package/dist/versions/document-version.d.cts +25 -0
  44. package/dist/versions/document-version.d.ts +25 -0
  45. package/dist/versions/document-version.js +17 -0
  46. package/package.json +1 -1
@@ -2,16 +2,22 @@ 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 { migrateDocument } from "../migrations/migrate-document.js";
14
19
  import { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
20
+ import { summarize, supportsVersions, versionsToPrune } from "../versions/document-version.js";
15
21
  import { createSaveController } from "../save/save-controller.js";
16
22
  import { createEventEmitter } from "./event-emitter.js";
17
23
  //#region src/engine/create-document-engine.ts
@@ -87,6 +93,7 @@ function createDocumentEngine(options) {
87
93
  onSuccess: (document) => {
88
94
  if (recovery && !saving.state().isDirty) recovery.remove(document.id);
89
95
  events.emit("save:success", { document });
96
+ createAutomaticVersionAfterSave(document);
90
97
  },
91
98
  onError: (error) => events.emit("save:error", { error }),
92
99
  onRetry: (event) => events.emit("save:retry", event)
@@ -151,7 +158,10 @@ function createDocumentEngine(options) {
151
158
  if (unknownTypes.length > 0) throw new 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 });
152
159
  return document;
153
160
  }
154
- async function loadDocument(input, loadOptions = {}) {
161
+ function loadDocument(input, loadOptions = {}) {
162
+ return loadMigratedDocument(input, loadOptions, {});
163
+ }
164
+ async function loadMigratedDocument(input, loadOptions, importDetails) {
155
165
  ensureUsable();
156
166
  activeLoad?.abort();
157
167
  const controller = new AbortController();
@@ -161,7 +171,12 @@ function createDocumentEngine(options) {
161
171
  try {
162
172
  await recovery?.markLoadStarted(documentId);
163
173
  if (controller.signal.aborted) throw new Error("aborted");
164
- const checked = checkDocument(input);
174
+ const { document: migrated, migratedFrom } = migrateDocument(input, {
175
+ canvasWidth: canvas.getWidth(),
176
+ canvasHeight: canvas.getHeight(),
177
+ ...importDetails
178
+ });
179
+ const checked = checkDocument(migrated);
165
180
  const { document, warnings } = await prepareAssetsForLoad(checked, assetOptions, controller.signal);
166
181
  if (controller.signal.aborted) throw new Error("aborted");
167
182
  await history.withoutRecording(() => loadIntoCanvas(canvas, {
@@ -188,7 +203,8 @@ function createDocumentEngine(options) {
188
203
  if (warnings.length > 0) events.emit("assets:warning", { warnings });
189
204
  events.emit("load:success", {
190
205
  document,
191
- warnings
206
+ warnings,
207
+ migratedFrom
192
208
  });
193
209
  return document;
194
210
  } catch (error) {
@@ -202,6 +218,90 @@ function createDocumentEngine(options) {
202
218
  }
203
219
  }
204
220
  }
221
+ function importFabricJson(json, importOptions = {}) {
222
+ let parsed = json;
223
+ if (typeof json === "string") try {
224
+ parsed = JSON.parse(json);
225
+ } catch (error) {
226
+ const reason = error instanceof Error ? error.message : String(error);
227
+ return Promise.reject(new DocumentEngineError("INVALID_DOCUMENT", `The text is not valid JSON: ${reason}`, { cause: error }));
228
+ }
229
+ const { id, metadata, ...loadOptions } = importOptions;
230
+ return loadMigratedDocument(parsed, loadOptions, {
231
+ id,
232
+ metadata
233
+ });
234
+ }
235
+ let savesSinceAutomaticVersion = 0;
236
+ let lastVersionTime = 0;
237
+ function nextVersionTimestamp() {
238
+ lastVersionTime = Math.max(Date.now(), lastVersionTime + 1);
239
+ return new Date(lastVersionTime).toISOString();
240
+ }
241
+ function requireVersions() {
242
+ ensureUsable();
243
+ if (!supportsVersions(storage)) throw new DocumentEngineError("VERSIONS_UNSUPPORTED", "The storage adapter needs saveVersion, listVersions, loadVersion and deleteVersion to keep versions");
244
+ return storage;
245
+ }
246
+ async function pruneAutomaticVersions(versionStorage, documentId) {
247
+ const keepAuto = options.versions?.keepAuto ?? 20;
248
+ const existing = await versionStorage.listVersions(documentId);
249
+ await Promise.all(versionsToPrune(existing, keepAuto).map((version) => versionStorage.deleteVersion(documentId, version.id)));
250
+ }
251
+ async function storeVersion(name, kind, document) {
252
+ const versionStorage = requireVersions();
253
+ const content = document ?? (await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls)).document;
254
+ const version = {
255
+ id: createId(),
256
+ documentId: content.id,
257
+ name,
258
+ kind,
259
+ createdAt: nextVersionTimestamp(),
260
+ revision: content.revision ?? saving.state().revision,
261
+ document: content
262
+ };
263
+ await versionStorage.saveVersion(version);
264
+ await pruneAutomaticVersions(versionStorage, content.id);
265
+ const summary = summarize(version);
266
+ events.emit("version:created", summary);
267
+ return summary;
268
+ }
269
+ function reportVersionError(error) {
270
+ const reason = error instanceof Error ? error.message : String(error);
271
+ const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("VERSION_FAILED", `Could not keep an automatic version: ${reason}`, { cause: error });
272
+ events.emit("version:error", { error: engineError });
273
+ }
274
+ function createAutomaticVersionAfterSave(document) {
275
+ const every = options.versions?.autoEvery ?? 0;
276
+ if (every <= 0 || !supportsVersions(storage)) return;
277
+ savesSinceAutomaticVersion += 1;
278
+ if (savesSinceAutomaticVersion < every) return;
279
+ savesSinceAutomaticVersion = 0;
280
+ storeVersion(`Autosave ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "auto", document).catch(reportVersionError);
281
+ }
282
+ async function restoreVersion(versionId) {
283
+ const versionStorage = requireVersions();
284
+ const current = documentInfo;
285
+ const version = await versionStorage.loadVersion(current.id, versionId);
286
+ await storeVersion(`Before restoring "${version.name}"`, "auto");
287
+ const { document: migrated } = migrateDocument(version.document, {
288
+ canvasWidth: canvas.getWidth(),
289
+ canvasHeight: canvas.getHeight()
290
+ });
291
+ const baseRevision = saving.state().revision;
292
+ const loaded = await loadDocument({
293
+ ...migrated,
294
+ id: current.id,
295
+ createdAt: current.createdAt,
296
+ revision: baseRevision
297
+ });
298
+ noteContentChange();
299
+ events.emit("version:restored", {
300
+ version: summarize(version),
301
+ document: loaded
302
+ });
303
+ return loaded;
304
+ }
205
305
  function requireStorage() {
206
306
  if (!storage) throw new DocumentEngineError("STORAGE_MISSING", "Pass a storage adapter to createDocumentEngine to use load and save");
207
307
  return storage;
@@ -218,7 +318,7 @@ function createDocumentEngine(options) {
218
318
  events.emit("load:error", { error: engineError });
219
319
  throw engineError;
220
320
  }
221
- return loadDocument(stored, loadOptions);
321
+ return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
222
322
  }
223
323
  function save(saveOptions) {
224
324
  try {
@@ -270,6 +370,92 @@ function createDocumentEngine(options) {
270
370
  canvas.requestRenderAll();
271
371
  return images.length;
272
372
  }
373
+ function fontsOnCanvas() {
374
+ rebuildIndex();
375
+ return buildAssetManifest(serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
376
+ }
377
+ async function checkBeforeExport(exportOptions) {
378
+ ensureUsable();
379
+ const { format } = normalizeExportOptions(exportOptions);
380
+ return preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
381
+ }
382
+ function describeProblems(check) {
383
+ return check.problems.map((problem) => problem.message).join("; ");
384
+ }
385
+ async function exportContent(exportOptions) {
386
+ const settings = normalizeExportOptions(exportOptions);
387
+ const stopIfCancelled = () => {
388
+ if (settings.signal?.aborted) throw new DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
389
+ };
390
+ stopIfCancelled();
391
+ if (settings.format === "json") {
392
+ const prepared = await prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
393
+ stopIfCancelled();
394
+ return {
395
+ format: "json",
396
+ mimeType: mimeTypes.json,
397
+ blob: new Blob([JSON.stringify(prepared.document)], { type: mimeTypes.json }),
398
+ width: prepared.document.canvas.width,
399
+ height: prepared.document.canvas.height,
400
+ warnings: prepared.warnings,
401
+ document: prepared.document
402
+ };
403
+ }
404
+ const check = await checkBeforeExport(settings);
405
+ stopIfCancelled();
406
+ if (!check.ok) throw new DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
407
+ const format = settings.format;
408
+ try {
409
+ const rendered = withExportView(canvas, settings.background, format, () => {
410
+ const area = resolveExportArea(canvas, settings.area, settings.padding);
411
+ return {
412
+ area,
413
+ content: format === "svg" ? renderSvg(canvas, area, settings.scale) : renderRaster(canvas, area, format, settings.scale, settings.quality)
414
+ };
415
+ });
416
+ const blob = format === "svg" ? new Blob([rendered.content], { type: mimeTypes.svg }) : dataUrlToBlob(rendered.content);
417
+ return {
418
+ format,
419
+ mimeType: blob.type || mimeTypes[format],
420
+ blob,
421
+ width: Math.round(rendered.area.width * settings.scale),
422
+ height: Math.round(rendered.area.height * settings.scale),
423
+ warnings: check.warnings
424
+ };
425
+ } catch (error) {
426
+ if (isDocumentEngineError(error)) throw error;
427
+ if (error?.name === "SecurityError") {
428
+ const problem = {
429
+ code: "CROSS_ORIGIN_IMAGE",
430
+ message: "The browser blocked the export because the canvas shows an image from another site without CORS",
431
+ objectIds: []
432
+ };
433
+ throw new DocumentEngineError("EXPORT_BLOCKED", problem.message, {
434
+ problems: [problem],
435
+ cause: error
436
+ });
437
+ }
438
+ const reason = error instanceof Error ? error.message : String(error);
439
+ throw new DocumentEngineError("EXPORT_FAILED", `Fabric could not export the canvas: ${reason}`, { cause: error });
440
+ }
441
+ }
442
+ async function exportDocument(exportOptions) {
443
+ ensureUsable();
444
+ try {
445
+ const result = await exportContent(exportOptions);
446
+ events.emit("export:success", {
447
+ format: result.format,
448
+ width: result.width,
449
+ height: result.height,
450
+ warnings: result.warnings
451
+ });
452
+ return result;
453
+ } catch (error) {
454
+ const engineError = isDocumentEngineError(error) ? error : new DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
455
+ events.emit("export:error", { error: engineError });
456
+ throw engineError;
457
+ }
458
+ }
273
459
  function requireRecovery() {
274
460
  ensureUsable();
275
461
  if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
@@ -329,6 +515,11 @@ function createDocumentEngine(options) {
329
515
  toDocument,
330
516
  loadDocument,
331
517
  load,
518
+ importFabricJson,
519
+ createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
520
+ listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
521
+ restoreVersion,
522
+ deleteVersion: async (versionId) => requireVersions().deleteVersion(documentInfo.id, versionId),
332
523
  save,
333
524
  isDirty: () => saving.state().isDirty,
334
525
  getSaveState: () => saving.state(),
@@ -343,6 +534,8 @@ function createDocumentEngine(options) {
343
534
  },
344
535
  checkAssets,
345
536
  replaceImage,
537
+ export: exportDocument,
538
+ preflightExport: checkBeforeExport,
346
539
  flushRecovery: async () => requireRecovery().flush(),
347
540
  getRecoverableDocuments: async () => requireRecovery().list(),
348
541
  getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
@@ -8,6 +8,8 @@ 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 ?? [];
12
+ this.migrationFrom = details.migrationFrom;
11
13
  this.cause = details.cause;
12
14
  this.retryable = details.retryable ?? false;
13
15
  }
@@ -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" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_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" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_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,8 @@ export interface DocumentEngineErrorDetails {
11
12
  unknownTypes?: string[];
12
13
  missingAssets?: ImageAsset[];
13
14
  missingFonts?: FontAsset[];
15
+ problems?: ExportProblem[];
16
+ migrationFrom?: number;
14
17
  cause?: unknown;
15
18
  retryable?: boolean;
16
19
  }
@@ -20,6 +23,8 @@ export declare class DocumentEngineError extends Error {
20
23
  readonly unknownTypes: string[];
21
24
  readonly missingAssets: ImageAsset[];
22
25
  readonly missingFonts: FontAsset[];
26
+ readonly problems: ExportProblem[];
27
+ readonly migrationFrom: number | undefined;
23
28
  readonly cause: unknown;
24
29
  readonly retryable: boolean;
25
30
  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" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_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" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_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,8 @@ export interface DocumentEngineErrorDetails {
11
12
  unknownTypes?: string[];
12
13
  missingAssets?: ImageAsset[];
13
14
  missingFonts?: FontAsset[];
15
+ problems?: ExportProblem[];
16
+ migrationFrom?: number;
14
17
  cause?: unknown;
15
18
  retryable?: boolean;
16
19
  }
@@ -20,6 +23,8 @@ export declare class DocumentEngineError extends Error {
20
23
  readonly unknownTypes: string[];
21
24
  readonly missingAssets: ImageAsset[];
22
25
  readonly missingFonts: FontAsset[];
26
+ readonly problems: ExportProblem[];
27
+ readonly migrationFrom: number | undefined;
23
28
  readonly cause: unknown;
24
29
  readonly retryable: boolean;
25
30
  constructor(code: DocumentErrorCode, message: string, details?: DocumentEngineErrorDetails);
@@ -8,6 +8,8 @@ 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 ?? [];
12
+ this.migrationFrom = details.migrationFrom;
11
13
  this.cause = details.cause;
12
14
  this.retryable = details.retryable ?? false;
13
15
  }
@@ -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 };