fabricjs-document-engine 0.4.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.
- package/README.md +51 -2
- package/dist/engine/create-document-engine.cjs +94 -2
- package/dist/engine/create-document-engine.d.cts +22 -0
- package/dist/engine/create-document-engine.d.ts +22 -0
- package/dist/engine/create-document-engine.js +94 -2
- package/dist/engine/errors.cjs +1 -0
- package/dist/engine/errors.d.cts +4 -1
- package/dist/engine/errors.d.ts +4 -1
- package/dist/engine/errors.js +1 -0
- package/dist/export/download-export.cjs +14 -0
- package/dist/export/download-export.d.cts +7 -0
- package/dist/export/download-export.d.ts +7 -0
- package/dist/export/download-export.js +14 -0
- package/dist/export/export-area.cjs +46 -0
- package/dist/export/export-area.js +46 -0
- package/dist/export/export-options.cjs +47 -0
- package/dist/export/export-options.d.cts +20 -0
- package/dist/export/export-options.d.ts +20 -0
- package/dist/export/export-options.js +46 -0
- package/dist/export/preflight-export.cjs +105 -0
- package/dist/export/preflight-export.d.cts +17 -0
- package/dist/export/preflight-export.d.ts +17 -0
- package/dist/export/preflight-export.js +105 -0
- package/dist/export/render-export.cjs +69 -0
- package/dist/export/render-export.js +65 -0
- package/dist/history/apply-state.cjs +1 -1
- package/dist/history/apply-state.js +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Fabric already draws objects, handles interaction and serializes to JSON. This p
|
|
|
12
12
|
- **Your storage.** Plug in any backend with two functions, or use the built-in memory and localStorage adapters. No hosted service is needed.
|
|
13
13
|
- **Assets and fonts.** Documents record the images and fonts they need. When a document is opened, every image and font is checked first. You get the exact list of what is missing, can offer replacements, and tab-only images are uploaded when you save.
|
|
14
14
|
- **Recovery.** Unsaved work is copied to IndexedDB while the user edits, and again at the moment the tab is closed or refreshed. After a crash or refresh you can offer to restore it, including images that only existed in the old tab.
|
|
15
|
+
- **Dependable export.** PNG, JPEG, WebP, SVG and editable JSON. You choose the area, scale and background. A preflight check means an export either succeeds or tells you exactly which image or font prevents it.
|
|
15
16
|
- **Reliable undo and redo.** One user action is one undo step. Transactions group several code changes into one labelled step, and ids survive undo and redo.
|
|
16
17
|
- **Fabric 6 and 7.** Every release is tested against both.
|
|
17
18
|
|
|
@@ -176,6 +177,47 @@ await engine.replaceImage('/old-logo.png', '/new-logo.png');
|
|
|
176
177
|
|
|
177
178
|
`replaceImage` swaps every image that uses a URL. Each image keeps its size on the page, and the change is one undo step. `engine.getAssetManifest()` returns the manifest for the current canvas.
|
|
178
179
|
|
|
180
|
+
## Export
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { downloadExport } from 'fabricjs-document-engine';
|
|
184
|
+
|
|
185
|
+
const result = await engine.export({ format: 'png', scale: 2 });
|
|
186
|
+
downloadExport(result, 'poster.png');
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`result` is `{ format, mimeType, blob, width, height, warnings }`. JSON exports also include `document`.
|
|
190
|
+
|
|
191
|
+
| Option | Values | Default |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `format` | `'png'`, `'jpeg'`, `'webp'`, `'svg'` or `'json'` | required |
|
|
194
|
+
| `scale` | Output size multiplier, such as `2` for retina | `1` |
|
|
195
|
+
| `quality` | 0 to 1, for JPEG and WebP | `0.92` |
|
|
196
|
+
| `area` | `'canvas'`, `'content'` (every object), `'selection'`, or `{ left, top, width, height }` | `'canvas'` |
|
|
197
|
+
| `padding` | Extra space around `content` or `selection` | `0` |
|
|
198
|
+
| `background` | `'keep'`, `'transparent'` or any CSS color | `'keep'` |
|
|
199
|
+
| `signal` | An `AbortSignal` to cancel | |
|
|
200
|
+
|
|
201
|
+
- The current zoom and pan do not matter. Exports always use document coordinates, and the view is restored afterwards.
|
|
202
|
+
- A JPEG has no transparency, so an empty or transparent background becomes white instead of black.
|
|
203
|
+
- The export never changes the canvas, the history or the unsaved state.
|
|
204
|
+
- A JSON export is the same portable document a save produces, including uploaded images when `assets.upload` is set.
|
|
205
|
+
|
|
206
|
+
### Preflight and errors
|
|
207
|
+
|
|
208
|
+
Before rendering, the engine checks the objects on the canvas:
|
|
209
|
+
|
|
210
|
+
- **`MISSING_IMAGE`**: an image failed to load.
|
|
211
|
+
- **`CROSS_ORIGIN_IMAGE`**: an image from another site without CORS would make the browser block a PNG, JPEG or WebP export. SVG and JSON are not affected.
|
|
212
|
+
- **`MISSING_FONT`**: a font is not available and `assets.requireFonts` is on. Otherwise you get a `FONT_UNAVAILABLE` warning.
|
|
213
|
+
|
|
214
|
+
If any problem is found, `export` rejects with `EXPORT_BLOCKED`, and `error.problems` lists each `{ code, message, url?, family?, objectIds }`. You can run the same check first to show it in your UI:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const check = await engine.preflightExport({ format: 'png' });
|
|
218
|
+
if (!check.ok) showProblems(check.problems);
|
|
219
|
+
```
|
|
220
|
+
|
|
179
221
|
## Recovery
|
|
180
222
|
|
|
181
223
|
```ts
|
|
@@ -311,6 +353,9 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
311
353
|
| `engine.getAssetManifest()` | Lists the images and fonts used on the canvas. |
|
|
312
354
|
| `engine.checkAssets()` | Resolves to `{ manifest, missingImages, unavailableFonts, warnings }` for the current canvas. |
|
|
313
355
|
| `engine.replaceImage(oldUrl, newUrl)` | Replaces every image with that URL as one undo step. Resolves to the number of images replaced. |
|
|
356
|
+
| `engine.export(options)` | Exports PNG, JPEG, WebP, SVG or JSON. See [Export](#export). |
|
|
357
|
+
| `engine.preflightExport(options)` | Resolves to `{ ok, problems, warnings }` without exporting. |
|
|
358
|
+
| `downloadExport(result, fileName?)` | Starts a browser download of an export result. |
|
|
314
359
|
| `engine.getRecoverableDocuments()` | Lists recovery copies, newest first. |
|
|
315
360
|
| `engine.restoreRecovery(id?)` / `engine.discardRecovery(id?)` | Loads or deletes a recovery copy. The default is the current document. |
|
|
316
361
|
| `engine.getRecovery(id?)` / `engine.flushRecovery()` | Reads a copy, or writes one now. |
|
|
@@ -322,7 +367,7 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
322
367
|
| `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
|
|
323
368
|
| `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
|
|
324
369
|
| `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
|
|
325
|
-
| `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
|
|
370
|
+
| `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `recovery:checkpoint`, `recovery:restored`, `recovery:error`, `export:success`, `export:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
|
|
326
371
|
| `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
|
|
327
372
|
| `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
|
|
328
373
|
|
|
@@ -345,6 +390,10 @@ Every failure is a `DocumentEngineError` with a `code` you can switch on:
|
|
|
345
390
|
| `SAVE_CANCELLED` | A queued save was dropped because another document was opened. |
|
|
346
391
|
| `DOCUMENT_NOT_FOUND` | The built-in adapters have no document with that id. |
|
|
347
392
|
| `HISTORY_FAILED` | Undo or redo could not rebuild an object, for example because an image is gone. The step is kept and the canvas is unchanged. |
|
|
393
|
+
| `EXPORT_BLOCKED` | The preflight found problems. `error.problems` lists each one with the objects involved. |
|
|
394
|
+
| `INVALID_EXPORT_OPTIONS` | The format, scale, quality, area or padding is not valid, or the area is empty. |
|
|
395
|
+
| `EXPORT_ABORTED` | The export was cancelled with its `signal`. |
|
|
396
|
+
| `EXPORT_FAILED` | Fabric could not render the export. `error.cause` holds the original error. |
|
|
348
397
|
| `RECOVERY_MISSING` | A recovery method was called without `recovery: { store }`. |
|
|
349
398
|
| `RECOVERY_NOT_FOUND` | There is no recovery copy for that document. |
|
|
350
399
|
| `RECOVERY_FAILED` | Writing a recovery copy failed, for example because storage is full. It is delivered as a `recovery:error` event and never interrupts editing. |
|
|
@@ -363,7 +412,7 @@ A failed load never clears or half-fills your canvas.
|
|
|
363
412
|
| 3 | Safe saving: dirty state, autosave, stale-response protection | 0.2.0 |
|
|
364
413
|
| 4 | Assets and fonts | 0.3.0 |
|
|
365
414
|
| 5 | Recovery after a refresh or crash | 0.4.0 |
|
|
366
|
-
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | |
|
|
415
|
+
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
|
|
367
416
|
| 7 | Named versions and schema migrations | |
|
|
368
417
|
| 8 | React adapter and examples | |
|
|
369
418
|
| 9 | Hardening and benchmarks | |
|
|
@@ -2,13 +2,17 @@ const require_asset_manifest = require("../assets/asset-manifest.cjs");
|
|
|
2
2
|
const require_errors = require("./errors.cjs");
|
|
3
3
|
const require_image_check = require("../assets/image-check.cjs");
|
|
4
4
|
const require_asset_pipeline = require("../assets/asset-pipeline.cjs");
|
|
5
|
+
const require_export_area = require("../export/export-area.cjs");
|
|
6
|
+
const require_export_options = require("../export/export-options.cjs");
|
|
7
|
+
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
8
|
+
const require_walk_objects = require("../fabric/walk-objects.cjs");
|
|
9
|
+
const require_preflight_export = require("../export/preflight-export.cjs");
|
|
10
|
+
const require_render_export = require("../export/render-export.cjs");
|
|
5
11
|
require("../document/document-format.cjs");
|
|
6
12
|
const require_ids = require("../document/ids.cjs");
|
|
7
13
|
const require_create_document = require("../document/create-document.cjs");
|
|
8
14
|
const require_validate_document = require("../document/validate-document.cjs");
|
|
9
|
-
const require_walk_objects = require("../fabric/walk-objects.cjs");
|
|
10
15
|
const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
11
|
-
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
12
16
|
const require_object_registry = require("../fabric/object-registry.cjs");
|
|
13
17
|
const require_create_history = require("../history/create-history.cjs");
|
|
14
18
|
const require_recovery_controller = require("../recovery/recovery-controller.cjs");
|
|
@@ -270,6 +274,92 @@ function createDocumentEngine(options) {
|
|
|
270
274
|
canvas.requestRenderAll();
|
|
271
275
|
return images.length;
|
|
272
276
|
}
|
|
277
|
+
function fontsOnCanvas() {
|
|
278
|
+
rebuildIndex();
|
|
279
|
+
return require_asset_manifest.buildAssetManifest(require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
|
|
280
|
+
}
|
|
281
|
+
async function checkBeforeExport(exportOptions) {
|
|
282
|
+
ensureUsable();
|
|
283
|
+
const { format } = require_export_options.normalizeExportOptions(exportOptions);
|
|
284
|
+
return require_preflight_export.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 = require_export_options.normalizeExportOptions(exportOptions);
|
|
291
|
+
const stopIfCancelled = () => {
|
|
292
|
+
if (settings.signal?.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
|
|
293
|
+
};
|
|
294
|
+
stopIfCancelled();
|
|
295
|
+
if (settings.format === "json") {
|
|
296
|
+
const prepared = await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
|
|
297
|
+
stopIfCancelled();
|
|
298
|
+
return {
|
|
299
|
+
format: "json",
|
|
300
|
+
mimeType: require_render_export.mimeTypes.json,
|
|
301
|
+
blob: new Blob([JSON.stringify(prepared.document)], { type: require_render_export.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 require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
|
|
311
|
+
const format = settings.format;
|
|
312
|
+
try {
|
|
313
|
+
const rendered = require_render_export.withExportView(canvas, settings.background, format, () => {
|
|
314
|
+
const area = require_export_area.resolveExportArea(canvas, settings.area, settings.padding);
|
|
315
|
+
return {
|
|
316
|
+
area,
|
|
317
|
+
content: format === "svg" ? require_render_export.renderSvg(canvas, area, settings.scale) : require_render_export.renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
const blob = format === "svg" ? new Blob([rendered.content], { type: require_render_export.mimeTypes.svg }) : require_render_export.dataUrlToBlob(rendered.content);
|
|
321
|
+
return {
|
|
322
|
+
format,
|
|
323
|
+
mimeType: blob.type || require_render_export.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 (require_errors.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 require_errors.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 require_errors.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 = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
|
|
359
|
+
events.emit("export:error", { error: engineError });
|
|
360
|
+
throw engineError;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
273
363
|
function requireRecovery() {
|
|
274
364
|
ensureUsable();
|
|
275
365
|
if (!recovery) throw new require_errors.DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
@@ -343,6 +433,8 @@ function createDocumentEngine(options) {
|
|
|
343
433
|
},
|
|
344
434
|
checkAssets,
|
|
345
435
|
replaceImage,
|
|
436
|
+
export: exportDocument,
|
|
437
|
+
preflightExport: checkBeforeExport,
|
|
346
438
|
flushRecovery: async () => requireRecovery().flush(),
|
|
347
439
|
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
348
440
|
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DocumentInfo, FabricDocument } from "../document/document-format.cjs";
|
|
2
2
|
import { AssetManifest } from "../assets/asset-manifest.cjs";
|
|
3
3
|
import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.cjs";
|
|
4
|
+
import { ExportFormat, ExportOptions } from "../export/export-options.cjs";
|
|
5
|
+
import { ExportPreflight } from "../export/preflight-export.cjs";
|
|
4
6
|
import { NewDocumentOptions } from "../document/create-document.cjs";
|
|
5
7
|
import { CustomObjectDefinition } from "../fabric/object-registry.cjs";
|
|
6
8
|
import { HistoryOptions, HistoryState } from "../history/create-history.cjs";
|
|
@@ -27,6 +29,15 @@ export interface DocumentEngineOptions {
|
|
|
27
29
|
export interface LoadOptions {
|
|
28
30
|
restoreCanvasSize?: boolean;
|
|
29
31
|
}
|
|
32
|
+
export interface ExportResult {
|
|
33
|
+
format: ExportFormat;
|
|
34
|
+
mimeType: string;
|
|
35
|
+
blob: Blob;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
warnings: AssetWarning[];
|
|
39
|
+
document?: FabricDocument;
|
|
40
|
+
}
|
|
30
41
|
export interface DocumentEngineEvents {
|
|
31
42
|
"load:start": {
|
|
32
43
|
documentId: string | undefined;
|
|
@@ -66,6 +77,15 @@ export interface DocumentEngineEvents {
|
|
|
66
77
|
"recovery:error": {
|
|
67
78
|
error: DocumentEngineError;
|
|
68
79
|
};
|
|
80
|
+
"export:success": {
|
|
81
|
+
format: ExportFormat;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
warnings: AssetWarning[];
|
|
85
|
+
};
|
|
86
|
+
"export:error": {
|
|
87
|
+
error: DocumentEngineError;
|
|
88
|
+
};
|
|
69
89
|
}
|
|
70
90
|
export interface DocumentEngine {
|
|
71
91
|
readonly canvas: StaticCanvas;
|
|
@@ -94,6 +114,8 @@ export interface DocumentEngine {
|
|
|
94
114
|
getAssetManifest(): AssetManifest;
|
|
95
115
|
checkAssets(): Promise<AssetReport>;
|
|
96
116
|
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
117
|
+
export(options: ExportOptions): Promise<ExportResult>;
|
|
118
|
+
preflightExport(options: ExportOptions): Promise<ExportPreflight>;
|
|
97
119
|
flushRecovery(): Promise<void>;
|
|
98
120
|
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
99
121
|
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { DocumentInfo, FabricDocument } from "../document/document-format.js";
|
|
2
2
|
import { AssetManifest } from "../assets/asset-manifest.js";
|
|
3
3
|
import { AssetOptions, AssetReport, AssetWarning } from "../assets/asset-pipeline.js";
|
|
4
|
+
import { ExportFormat, ExportOptions } from "../export/export-options.js";
|
|
5
|
+
import { ExportPreflight } from "../export/preflight-export.js";
|
|
4
6
|
import { NewDocumentOptions } from "../document/create-document.js";
|
|
5
7
|
import { CustomObjectDefinition } from "../fabric/object-registry.js";
|
|
6
8
|
import { HistoryOptions, HistoryState } from "../history/create-history.js";
|
|
@@ -27,6 +29,15 @@ export interface DocumentEngineOptions {
|
|
|
27
29
|
export interface LoadOptions {
|
|
28
30
|
restoreCanvasSize?: boolean;
|
|
29
31
|
}
|
|
32
|
+
export interface ExportResult {
|
|
33
|
+
format: ExportFormat;
|
|
34
|
+
mimeType: string;
|
|
35
|
+
blob: Blob;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
warnings: AssetWarning[];
|
|
39
|
+
document?: FabricDocument;
|
|
40
|
+
}
|
|
30
41
|
export interface DocumentEngineEvents {
|
|
31
42
|
"load:start": {
|
|
32
43
|
documentId: string | undefined;
|
|
@@ -66,6 +77,15 @@ export interface DocumentEngineEvents {
|
|
|
66
77
|
"recovery:error": {
|
|
67
78
|
error: DocumentEngineError;
|
|
68
79
|
};
|
|
80
|
+
"export:success": {
|
|
81
|
+
format: ExportFormat;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
warnings: AssetWarning[];
|
|
85
|
+
};
|
|
86
|
+
"export:error": {
|
|
87
|
+
error: DocumentEngineError;
|
|
88
|
+
};
|
|
69
89
|
}
|
|
70
90
|
export interface DocumentEngine {
|
|
71
91
|
readonly canvas: StaticCanvas;
|
|
@@ -94,6 +114,8 @@ export interface DocumentEngine {
|
|
|
94
114
|
getAssetManifest(): AssetManifest;
|
|
95
115
|
checkAssets(): Promise<AssetReport>;
|
|
96
116
|
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
117
|
+
export(options: ExportOptions): Promise<ExportResult>;
|
|
118
|
+
preflightExport(options: ExportOptions): Promise<ExportPreflight>;
|
|
97
119
|
flushRecovery(): Promise<void>;
|
|
98
120
|
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
99
121
|
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
@@ -2,13 +2,17 @@ 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";
|
|
14
18
|
import { createRecoveryController, restoreRecordedFiles } from "../recovery/recovery-controller.js";
|
|
@@ -270,6 +274,92 @@ function createDocumentEngine(options) {
|
|
|
270
274
|
canvas.requestRenderAll();
|
|
271
275
|
return images.length;
|
|
272
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
|
+
}
|
|
273
363
|
function requireRecovery() {
|
|
274
364
|
ensureUsable();
|
|
275
365
|
if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
@@ -343,6 +433,8 @@ function createDocumentEngine(options) {
|
|
|
343
433
|
},
|
|
344
434
|
checkAssets,
|
|
345
435
|
replaceImage,
|
|
436
|
+
export: exportDocument,
|
|
437
|
+
preflightExport: checkBeforeExport,
|
|
346
438
|
flushRecovery: async () => requireRecovery().flush(),
|
|
347
439
|
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
348
440
|
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
package/dist/engine/errors.cjs
CHANGED
|
@@ -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
|
}
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -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" | "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);
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -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" | "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);
|
package/dist/engine/errors.js
CHANGED
|
@@ -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,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 };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const require_font_check = require("../assets/font-check.cjs");
|
|
2
|
+
const require_image_check = require("../assets/image-check.cjs");
|
|
3
|
+
const require_export_options = require("./export-options.cjs");
|
|
4
|
+
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
5
|
+
const require_walk_objects = require("../fabric/walk-objects.cjs");
|
|
6
|
+
//#region src/export/preflight-export.ts
|
|
7
|
+
function imageElementsOf(object) {
|
|
8
|
+
const elements = [];
|
|
9
|
+
const withElement = object;
|
|
10
|
+
if (typeof withElement.getElement === "function") elements.push(withElement.getElement());
|
|
11
|
+
for (const paint of [object.fill, object.stroke]) {
|
|
12
|
+
const source = paint?.source;
|
|
13
|
+
if (source !== void 0) elements.push(source);
|
|
14
|
+
}
|
|
15
|
+
return elements.filter((element) => typeof HTMLImageElement !== "undefined" && element instanceof HTMLImageElement);
|
|
16
|
+
}
|
|
17
|
+
function collectImageUses(objects) {
|
|
18
|
+
const uses = /* @__PURE__ */ new Map();
|
|
19
|
+
const pending = objects.map((object) => ({
|
|
20
|
+
object,
|
|
21
|
+
ownerId: require_object_ids.readObjectId(object)
|
|
22
|
+
}));
|
|
23
|
+
while (pending.length > 0) {
|
|
24
|
+
const { object, ownerId } = pending.pop();
|
|
25
|
+
const id = require_object_ids.readObjectId(object) ?? ownerId;
|
|
26
|
+
for (const element of imageElementsOf(object)) {
|
|
27
|
+
const url = element.currentSrc || element.src;
|
|
28
|
+
const use = uses.get(url) ?? {
|
|
29
|
+
element,
|
|
30
|
+
objectIds: /* @__PURE__ */ new Set()
|
|
31
|
+
};
|
|
32
|
+
if (id !== void 0) use.objectIds.add(id);
|
|
33
|
+
uses.set(url, use);
|
|
34
|
+
}
|
|
35
|
+
require_walk_objects.childrenOf(object).forEach((child) => pending.push({
|
|
36
|
+
object: child,
|
|
37
|
+
ownerId: id
|
|
38
|
+
}));
|
|
39
|
+
if (object.clipPath) pending.push({
|
|
40
|
+
object: object.clipPath,
|
|
41
|
+
ownerId: id
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return uses;
|
|
45
|
+
}
|
|
46
|
+
function isBroken(element) {
|
|
47
|
+
return element.complete && element.naturalWidth === 0;
|
|
48
|
+
}
|
|
49
|
+
function shortUrl(url) {
|
|
50
|
+
return url.length > 80 ? `${url.slice(0, 77)}...` : url;
|
|
51
|
+
}
|
|
52
|
+
async function preflightExport(canvas, format, fonts, assetOptions) {
|
|
53
|
+
const problems = [];
|
|
54
|
+
const warnings = [];
|
|
55
|
+
if (format === "json") return {
|
|
56
|
+
ok: true,
|
|
57
|
+
problems,
|
|
58
|
+
warnings
|
|
59
|
+
};
|
|
60
|
+
for (const [url, use] of collectImageUses(canvas.getObjects())) {
|
|
61
|
+
const objectIds = [...use.objectIds];
|
|
62
|
+
if (isBroken(use.element)) problems.push({
|
|
63
|
+
code: "MISSING_IMAGE",
|
|
64
|
+
message: `The image ${shortUrl(url)} did not load`,
|
|
65
|
+
url,
|
|
66
|
+
objectIds
|
|
67
|
+
});
|
|
68
|
+
else if (require_export_options.isRasterFormat(format) && require_image_check.isCrossOriginUrl(url) && !use.element.crossOrigin) problems.push({
|
|
69
|
+
code: "CROSS_ORIGIN_IMAGE",
|
|
70
|
+
message: `The image ${shortUrl(url)} comes from another site without crossOrigin, so the browser blocks exporting it. Load it with crossOrigin: 'anonymous' from a server that allows CORS.`,
|
|
71
|
+
url,
|
|
72
|
+
objectIds
|
|
73
|
+
});
|
|
74
|
+
else if (format === "svg" && !require_image_check.isPortableUrl(url)) warnings.push({
|
|
75
|
+
code: "ASSET_NOT_PORTABLE",
|
|
76
|
+
message: `The SVG links to ${shortUrl(url)}, which only exists in this tab`,
|
|
77
|
+
url,
|
|
78
|
+
objectIds
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const fontSet = typeof document === "undefined" ? void 0 : document.fonts;
|
|
82
|
+
if (fontSet) await fontSet.ready;
|
|
83
|
+
for (const font of await require_font_check.findUnavailableFonts(fonts, assetOptions.loadFont)) {
|
|
84
|
+
const message = `The font "${font.family}" (${font.weight}, ${font.style}) is not available`;
|
|
85
|
+
if (assetOptions.requireFonts) problems.push({
|
|
86
|
+
code: "MISSING_FONT",
|
|
87
|
+
message,
|
|
88
|
+
family: font.family,
|
|
89
|
+
objectIds: font.objectIds
|
|
90
|
+
});
|
|
91
|
+
else warnings.push({
|
|
92
|
+
code: "FONT_UNAVAILABLE",
|
|
93
|
+
message: `${message}, text will use a fallback font`,
|
|
94
|
+
family: font.family,
|
|
95
|
+
objectIds: font.objectIds
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: problems.length === 0,
|
|
100
|
+
problems,
|
|
101
|
+
warnings
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
exports.preflightExport = preflightExport;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AssetWarning } from "../assets/asset-pipeline.cjs";
|
|
2
|
+
import { StaticCanvas } from "fabric";
|
|
3
|
+
//#region src/export/preflight-export.d.ts
|
|
4
|
+
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT";
|
|
5
|
+
export interface ExportProblem {
|
|
6
|
+
code: ExportProblemCode;
|
|
7
|
+
message: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
family?: string;
|
|
10
|
+
objectIds: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ExportPreflight {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
problems: ExportProblem[];
|
|
15
|
+
warnings: AssetWarning[];
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AssetWarning } from "../assets/asset-pipeline.js";
|
|
2
|
+
import { StaticCanvas } from "fabric";
|
|
3
|
+
//#region src/export/preflight-export.d.ts
|
|
4
|
+
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT";
|
|
5
|
+
export interface ExportProblem {
|
|
6
|
+
code: ExportProblemCode;
|
|
7
|
+
message: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
family?: string;
|
|
10
|
+
objectIds: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface ExportPreflight {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
problems: ExportProblem[];
|
|
15
|
+
warnings: AssetWarning[];
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { findUnavailableFonts } from "../assets/font-check.js";
|
|
2
|
+
import { isCrossOriginUrl, isPortableUrl } from "../assets/image-check.js";
|
|
3
|
+
import { isRasterFormat } from "./export-options.js";
|
|
4
|
+
import { readObjectId } from "../fabric/object-ids.js";
|
|
5
|
+
import { childrenOf } from "../fabric/walk-objects.js";
|
|
6
|
+
//#region src/export/preflight-export.ts
|
|
7
|
+
function imageElementsOf(object) {
|
|
8
|
+
const elements = [];
|
|
9
|
+
const withElement = object;
|
|
10
|
+
if (typeof withElement.getElement === "function") elements.push(withElement.getElement());
|
|
11
|
+
for (const paint of [object.fill, object.stroke]) {
|
|
12
|
+
const source = paint?.source;
|
|
13
|
+
if (source !== void 0) elements.push(source);
|
|
14
|
+
}
|
|
15
|
+
return elements.filter((element) => typeof HTMLImageElement !== "undefined" && element instanceof HTMLImageElement);
|
|
16
|
+
}
|
|
17
|
+
function collectImageUses(objects) {
|
|
18
|
+
const uses = /* @__PURE__ */ new Map();
|
|
19
|
+
const pending = objects.map((object) => ({
|
|
20
|
+
object,
|
|
21
|
+
ownerId: readObjectId(object)
|
|
22
|
+
}));
|
|
23
|
+
while (pending.length > 0) {
|
|
24
|
+
const { object, ownerId } = pending.pop();
|
|
25
|
+
const id = readObjectId(object) ?? ownerId;
|
|
26
|
+
for (const element of imageElementsOf(object)) {
|
|
27
|
+
const url = element.currentSrc || element.src;
|
|
28
|
+
const use = uses.get(url) ?? {
|
|
29
|
+
element,
|
|
30
|
+
objectIds: /* @__PURE__ */ new Set()
|
|
31
|
+
};
|
|
32
|
+
if (id !== void 0) use.objectIds.add(id);
|
|
33
|
+
uses.set(url, use);
|
|
34
|
+
}
|
|
35
|
+
childrenOf(object).forEach((child) => pending.push({
|
|
36
|
+
object: child,
|
|
37
|
+
ownerId: id
|
|
38
|
+
}));
|
|
39
|
+
if (object.clipPath) pending.push({
|
|
40
|
+
object: object.clipPath,
|
|
41
|
+
ownerId: id
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return uses;
|
|
45
|
+
}
|
|
46
|
+
function isBroken(element) {
|
|
47
|
+
return element.complete && element.naturalWidth === 0;
|
|
48
|
+
}
|
|
49
|
+
function shortUrl(url) {
|
|
50
|
+
return url.length > 80 ? `${url.slice(0, 77)}...` : url;
|
|
51
|
+
}
|
|
52
|
+
async function preflightExport(canvas, format, fonts, assetOptions) {
|
|
53
|
+
const problems = [];
|
|
54
|
+
const warnings = [];
|
|
55
|
+
if (format === "json") return {
|
|
56
|
+
ok: true,
|
|
57
|
+
problems,
|
|
58
|
+
warnings
|
|
59
|
+
};
|
|
60
|
+
for (const [url, use] of collectImageUses(canvas.getObjects())) {
|
|
61
|
+
const objectIds = [...use.objectIds];
|
|
62
|
+
if (isBroken(use.element)) problems.push({
|
|
63
|
+
code: "MISSING_IMAGE",
|
|
64
|
+
message: `The image ${shortUrl(url)} did not load`,
|
|
65
|
+
url,
|
|
66
|
+
objectIds
|
|
67
|
+
});
|
|
68
|
+
else if (isRasterFormat(format) && isCrossOriginUrl(url) && !use.element.crossOrigin) problems.push({
|
|
69
|
+
code: "CROSS_ORIGIN_IMAGE",
|
|
70
|
+
message: `The image ${shortUrl(url)} comes from another site without crossOrigin, so the browser blocks exporting it. Load it with crossOrigin: 'anonymous' from a server that allows CORS.`,
|
|
71
|
+
url,
|
|
72
|
+
objectIds
|
|
73
|
+
});
|
|
74
|
+
else if (format === "svg" && !isPortableUrl(url)) warnings.push({
|
|
75
|
+
code: "ASSET_NOT_PORTABLE",
|
|
76
|
+
message: `The SVG links to ${shortUrl(url)}, which only exists in this tab`,
|
|
77
|
+
url,
|
|
78
|
+
objectIds
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const fontSet = typeof document === "undefined" ? void 0 : document.fonts;
|
|
82
|
+
if (fontSet) await fontSet.ready;
|
|
83
|
+
for (const font of await findUnavailableFonts(fonts, assetOptions.loadFont)) {
|
|
84
|
+
const message = `The font "${font.family}" (${font.weight}, ${font.style}) is not available`;
|
|
85
|
+
if (assetOptions.requireFonts) problems.push({
|
|
86
|
+
code: "MISSING_FONT",
|
|
87
|
+
message,
|
|
88
|
+
family: font.family,
|
|
89
|
+
objectIds: font.objectIds
|
|
90
|
+
});
|
|
91
|
+
else warnings.push({
|
|
92
|
+
code: "FONT_UNAVAILABLE",
|
|
93
|
+
message: `${message}, text will use a fallback font`,
|
|
94
|
+
family: font.family,
|
|
95
|
+
objectIds: font.objectIds
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
ok: problems.length === 0,
|
|
100
|
+
problems,
|
|
101
|
+
warnings
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { preflightExport };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/export/render-export.ts
|
|
2
|
+
const identity = [
|
|
3
|
+
1,
|
|
4
|
+
0,
|
|
5
|
+
0,
|
|
6
|
+
1,
|
|
7
|
+
0,
|
|
8
|
+
0
|
|
9
|
+
];
|
|
10
|
+
const mimeTypes = {
|
|
11
|
+
png: "image/png",
|
|
12
|
+
jpeg: "image/jpeg",
|
|
13
|
+
webp: "image/webp",
|
|
14
|
+
svg: "image/svg+xml",
|
|
15
|
+
json: "application/json"
|
|
16
|
+
};
|
|
17
|
+
function chooseBackground(background, format, current) {
|
|
18
|
+
if (background === "transparent") return format === "jpeg" ? "white" : "";
|
|
19
|
+
if (background !== "keep") return background;
|
|
20
|
+
return format === "jpeg" && !(current !== void 0 && current !== null && current !== "") ? "white" : current;
|
|
21
|
+
}
|
|
22
|
+
function withExportView(canvas, background, format, work) {
|
|
23
|
+
const savedTransform = [...canvas.viewportTransform];
|
|
24
|
+
const savedBackground = canvas.backgroundColor;
|
|
25
|
+
canvas.setViewportTransform(identity);
|
|
26
|
+
canvas.backgroundColor = chooseBackground(background, format, savedBackground);
|
|
27
|
+
try {
|
|
28
|
+
return work();
|
|
29
|
+
} finally {
|
|
30
|
+
canvas.backgroundColor = savedBackground;
|
|
31
|
+
canvas.setViewportTransform(savedTransform);
|
|
32
|
+
canvas.requestRenderAll();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function renderRaster(canvas, area, format, scale, quality) {
|
|
36
|
+
return canvas.toDataURL({
|
|
37
|
+
...area,
|
|
38
|
+
format,
|
|
39
|
+
quality,
|
|
40
|
+
multiplier: scale,
|
|
41
|
+
enableRetinaScaling: false
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function renderSvg(canvas, area, scale) {
|
|
45
|
+
return canvas.toSVG({
|
|
46
|
+
viewBox: {
|
|
47
|
+
x: area.left,
|
|
48
|
+
y: area.top,
|
|
49
|
+
width: area.width,
|
|
50
|
+
height: area.height
|
|
51
|
+
},
|
|
52
|
+
width: String(area.width * scale),
|
|
53
|
+
height: String(area.height * scale)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function dataUrlToBlob(dataUrl) {
|
|
57
|
+
const [header, payload = ""] = dataUrl.split(",");
|
|
58
|
+
const mimeType = /data:([^;]+)/.exec(header ?? "")?.[1] ?? "application/octet-stream";
|
|
59
|
+
const binary = atob(payload);
|
|
60
|
+
const bytes = new Uint8Array(binary.length);
|
|
61
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
62
|
+
return new Blob([bytes], { type: mimeType });
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
exports.dataUrlToBlob = dataUrlToBlob;
|
|
66
|
+
exports.mimeTypes = mimeTypes;
|
|
67
|
+
exports.renderRaster = renderRaster;
|
|
68
|
+
exports.renderSvg = renderSvg;
|
|
69
|
+
exports.withExportView = withExportView;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/export/render-export.ts
|
|
2
|
+
const identity = [
|
|
3
|
+
1,
|
|
4
|
+
0,
|
|
5
|
+
0,
|
|
6
|
+
1,
|
|
7
|
+
0,
|
|
8
|
+
0
|
|
9
|
+
];
|
|
10
|
+
const mimeTypes = {
|
|
11
|
+
png: "image/png",
|
|
12
|
+
jpeg: "image/jpeg",
|
|
13
|
+
webp: "image/webp",
|
|
14
|
+
svg: "image/svg+xml",
|
|
15
|
+
json: "application/json"
|
|
16
|
+
};
|
|
17
|
+
function chooseBackground(background, format, current) {
|
|
18
|
+
if (background === "transparent") return format === "jpeg" ? "white" : "";
|
|
19
|
+
if (background !== "keep") return background;
|
|
20
|
+
return format === "jpeg" && !(current !== void 0 && current !== null && current !== "") ? "white" : current;
|
|
21
|
+
}
|
|
22
|
+
function withExportView(canvas, background, format, work) {
|
|
23
|
+
const savedTransform = [...canvas.viewportTransform];
|
|
24
|
+
const savedBackground = canvas.backgroundColor;
|
|
25
|
+
canvas.setViewportTransform(identity);
|
|
26
|
+
canvas.backgroundColor = chooseBackground(background, format, savedBackground);
|
|
27
|
+
try {
|
|
28
|
+
return work();
|
|
29
|
+
} finally {
|
|
30
|
+
canvas.backgroundColor = savedBackground;
|
|
31
|
+
canvas.setViewportTransform(savedTransform);
|
|
32
|
+
canvas.requestRenderAll();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function renderRaster(canvas, area, format, scale, quality) {
|
|
36
|
+
return canvas.toDataURL({
|
|
37
|
+
...area,
|
|
38
|
+
format,
|
|
39
|
+
quality,
|
|
40
|
+
multiplier: scale,
|
|
41
|
+
enableRetinaScaling: false
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function renderSvg(canvas, area, scale) {
|
|
45
|
+
return canvas.toSVG({
|
|
46
|
+
viewBox: {
|
|
47
|
+
x: area.left,
|
|
48
|
+
y: area.top,
|
|
49
|
+
width: area.width,
|
|
50
|
+
height: area.height
|
|
51
|
+
},
|
|
52
|
+
width: String(area.width * scale),
|
|
53
|
+
height: String(area.height * scale)
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function dataUrlToBlob(dataUrl) {
|
|
57
|
+
const [header, payload = ""] = dataUrl.split(",");
|
|
58
|
+
const mimeType = /data:([^;]+)/.exec(header ?? "")?.[1] ?? "application/octet-stream";
|
|
59
|
+
const binary = atob(payload);
|
|
60
|
+
const bytes = new Uint8Array(binary.length);
|
|
61
|
+
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
|
|
62
|
+
return new Blob([bytes], { type: mimeType });
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { dataUrlToBlob, mimeTypes, renderRaster, renderSvg, withExportView };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
2
1
|
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
2
|
+
const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
3
3
|
//#region src/history/apply-state.ts
|
|
4
4
|
function arrangeObjects(current, change, created) {
|
|
5
5
|
const arranged = [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createObjects } from "../fabric/fabric-adapter.js";
|
|
2
1
|
import { readObjectId } from "../fabric/object-ids.js";
|
|
2
|
+
import { createObjects } from "../fabric/fabric-adapter.js";
|
|
3
3
|
//#region src/history/apply-state.ts
|
|
4
4
|
function arrangeObjects(current, change, created) {
|
|
5
5
|
const arranged = [];
|
package/dist/index.cjs
CHANGED
|
@@ -5,11 +5,13 @@ const require_validate_document = require("./document/validate-document.cjs");
|
|
|
5
5
|
const require_create_document_engine = require("./engine/create-document-engine.cjs");
|
|
6
6
|
const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
|
|
7
7
|
const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
|
|
8
|
+
const require_download_export = require("./export/download-export.cjs");
|
|
8
9
|
exports.CURRENT_SCHEMA_VERSION = require_document_format.CURRENT_SCHEMA_VERSION;
|
|
9
10
|
exports.DocumentEngineError = require_errors.DocumentEngineError;
|
|
10
11
|
exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts;
|
|
11
12
|
exports.bindUnsavedChangesWarning = require_unsaved_changes_warning.bindUnsavedChangesWarning;
|
|
12
13
|
exports.createConflictError = require_errors.createConflictError;
|
|
13
14
|
exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
|
|
15
|
+
exports.downloadExport = require_download_export.downloadExport;
|
|
14
16
|
exports.isDocumentEngineError = require_errors.isDocumentEngineError;
|
|
15
17
|
exports.validateDocument = require_validate_document.validateDocument;
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,8 @@ import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, S
|
|
|
2
2
|
import { AssetManifest, FontAsset, ImageAsset } from "./assets/asset-manifest.cjs";
|
|
3
3
|
import { FontLoader } from "./assets/font-check.cjs";
|
|
4
4
|
import { AssetOptions, AssetReport, AssetWarning, AssetWarningCode, UploadRequest } from "./assets/asset-pipeline.cjs";
|
|
5
|
+
import { ExportArea, ExportBackground, ExportFormat, ExportOptions, ExportRect } from "./export/export-options.cjs";
|
|
6
|
+
import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/preflight-export.cjs";
|
|
5
7
|
import { NewDocumentOptions } from "./document/create-document.cjs";
|
|
6
8
|
import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
|
|
7
9
|
import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
|
|
@@ -13,8 +15,9 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
13
15
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
14
16
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
|
|
15
17
|
import { Unsubscribe } from "./engine/event-emitter.cjs";
|
|
16
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
18
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
17
19
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
18
20
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
19
21
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
|
|
20
|
-
|
|
22
|
+
import { DownloadableExport, downloadExport } from "./export/download-export.cjs";
|
|
23
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, downloadExport, isDocumentEngineError, validateDocument };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, S
|
|
|
2
2
|
import { AssetManifest, FontAsset, ImageAsset } from "./assets/asset-manifest.js";
|
|
3
3
|
import { FontLoader } from "./assets/font-check.js";
|
|
4
4
|
import { AssetOptions, AssetReport, AssetWarning, AssetWarningCode, UploadRequest } from "./assets/asset-pipeline.js";
|
|
5
|
+
import { ExportArea, ExportBackground, ExportFormat, ExportOptions, ExportRect } from "./export/export-options.js";
|
|
6
|
+
import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/preflight-export.js";
|
|
5
7
|
import { NewDocumentOptions } from "./document/create-document.js";
|
|
6
8
|
import { CustomObjectDefinition } from "./fabric/object-registry.js";
|
|
7
9
|
import { HistoryOptions, HistoryState } from "./history/create-history.js";
|
|
@@ -13,8 +15,9 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
13
15
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.js";
|
|
14
16
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.js";
|
|
15
17
|
import { Unsubscribe } from "./engine/event-emitter.js";
|
|
16
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
18
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, LoadOptions, createDocumentEngine } from "./engine/create-document-engine.js";
|
|
17
19
|
import { validateDocument } from "./document/validate-document.js";
|
|
18
20
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
19
21
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
20
|
-
|
|
22
|
+
import { DownloadableExport, downloadExport } from "./export/download-export.js";
|
|
23
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CURRENT_SCHEMA_VERSION, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentStorage, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type HistoryOptions, type HistoryState, type ImageAsset, type InterruptedLoad, type KeyboardShortcutOptions, type LoadOptions, type NewDocumentOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, downloadExport, isDocumentEngineError, validateDocument };
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,5 @@ import { validateDocument } from "./document/validate-document.js";
|
|
|
4
4
|
import { createDocumentEngine } from "./engine/create-document-engine.js";
|
|
5
5
|
import { bindKeyboardShortcuts } from "./history/keyboard-shortcuts.js";
|
|
6
6
|
import { bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.js";
|
|
7
|
-
|
|
7
|
+
import { downloadExport } from "./export/download-export.js";
|
|
8
|
+
export { CURRENT_SCHEMA_VERSION, DocumentEngineError, bindKeyboardShortcuts, bindUnsavedChangesWarning, createConflictError, createDocumentEngine, downloadExport, isDocumentEngineError, validateDocument };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabricjs-document-engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Turn an existing Fabric.js canvas into a dependable editable document: stable object ids, versioned save and load, undo and redo, safe autosave with conflict protection, custom objects and clear errors.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabric",
|