fabricjs-document-engine 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -3
- package/dist/assets/asset-pipeline.cjs +1 -0
- package/dist/assets/asset-pipeline.js +1 -1
- package/dist/engine/create-document-engine.cjs +150 -7
- package/dist/engine/create-document-engine.d.cts +40 -0
- package/dist/engine/create-document-engine.d.ts +40 -0
- package/dist/engine/create-document-engine.js +150 -7
- 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 +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +2 -1
- package/dist/recovery/indexed-db-recovery.cjs +80 -0
- package/dist/recovery/indexed-db-recovery.d.cts +8 -0
- package/dist/recovery/indexed-db-recovery.d.ts +8 -0
- package/dist/recovery/indexed-db-recovery.js +80 -0
- package/dist/recovery/recovery-controller.cjs +159 -0
- package/dist/recovery/recovery-controller.d.cts +19 -0
- package/dist/recovery/recovery-controller.d.ts +19 -0
- package/dist/recovery/recovery-controller.js +158 -0
- package/dist/recovery/recovery-store.cjs +19 -0
- package/dist/recovery/recovery-store.d.cts +10 -0
- package/dist/recovery/recovery-store.d.ts +10 -0
- package/dist/recovery/recovery-store.js +19 -0
- package/dist/recovery.cjs +5 -0
- package/dist/recovery.d.cts +4 -0
- package/dist/recovery.d.ts +4 -0
- package/dist/recovery.js +3 -0
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ Fabric already draws objects, handles interaction and serializes to JSON. This p
|
|
|
11
11
|
- **Safe saving.** It tracks unsaved changes and can autosave. Only one save runs at a time, so a slow older save can never overwrite newer work. Revision checks catch another tab or device saving the same document, and failed saves are retried with backoff.
|
|
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
|
+
- **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.
|
|
14
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.
|
|
15
17
|
- **Fabric 6 and 7.** Every release is tested against both.
|
|
16
18
|
|
|
@@ -175,6 +177,75 @@ await engine.replaceImage('/old-logo.png', '/new-logo.png');
|
|
|
175
177
|
|
|
176
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.
|
|
177
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
|
+
|
|
221
|
+
## Recovery
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { createIndexedDbRecovery } from 'fabricjs-document-engine/recovery';
|
|
225
|
+
|
|
226
|
+
const engine = createDocumentEngine({
|
|
227
|
+
canvas,
|
|
228
|
+
storage,
|
|
229
|
+
recovery: { store: createIndexedDbRecovery(), interval: 2000 },
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const [latest] = await engine.getRecoverableDocuments();
|
|
233
|
+
if (latest && confirm(`Restore unsaved work from ${new Date(latest.savedAt).toLocaleString()}?`)) {
|
|
234
|
+
await engine.restoreRecovery(latest.documentId);
|
|
235
|
+
} else if (latest) {
|
|
236
|
+
await engine.discardRecovery(latest.documentId);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- **Checkpoints.** While there are unsaved changes, a copy is written at most once every `interval` ms (default 2000). Nothing is written while the document is saved.
|
|
241
|
+
- **Closing or refreshing.** Browsers do not let IndexedDB finish writing while a page unloads. So when the tab is hidden or closed, the engine also writes an immediate copy to localStorage. The newest copy wins when you read it back.
|
|
242
|
+
- **Tab-only images.** Images with `blob:` URLs vanish on refresh. Checkpoints keep the image data, and restoring creates fresh URLs for it.
|
|
243
|
+
- **After a save.** When a save covers every change, the copy is removed. If the tab closes during a save, the copy stays, so work is never lost between the edit and the server.
|
|
244
|
+
- **Restoring.** The restored document is marked as unsaved and keeps the revision it was based on. If the server moved on meanwhile, the next save reports `SAVE_CONFLICT` instead of overwriting newer work.
|
|
245
|
+
- **Interrupted loads.** A marker is kept while a document loads. If the tab crashes during loading, `engine.getInterruptedLoad()` returns `{ documentId, startedAt }` on the next start, so you can skip or discard that document instead of crashing again.
|
|
246
|
+
- `engine.flushRecovery()` writes a copy right now. `engine.getRecovery(id?)` reads one.
|
|
247
|
+
- `createMemoryRecovery()` keeps copies in memory, which is useful for tests. To use your own storage, implement `{ get, set, delete, keys }`, plus an optional synchronous `setNow` for the moment the page closes.
|
|
248
|
+
|
|
178
249
|
## Custom objects
|
|
179
250
|
|
|
180
251
|
```ts
|
|
@@ -282,6 +353,13 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
282
353
|
| `engine.getAssetManifest()` | Lists the images and fonts used on the canvas. |
|
|
283
354
|
| `engine.checkAssets()` | Resolves to `{ manifest, missingImages, unavailableFonts, warnings }` for the current canvas. |
|
|
284
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. |
|
|
359
|
+
| `engine.getRecoverableDocuments()` | Lists recovery copies, newest first. |
|
|
360
|
+
| `engine.restoreRecovery(id?)` / `engine.discardRecovery(id?)` | Loads or deletes a recovery copy. The default is the current document. |
|
|
361
|
+
| `engine.getRecovery(id?)` / `engine.flushRecovery()` | Reads a copy, or writes one now. |
|
|
362
|
+
| `engine.getInterruptedLoad()` | Returns the load that was running when the tab last crashed, if any. |
|
|
285
363
|
| `engine.transaction(label, work)` | Runs `work` and records everything it changed as one undo step. Returns what `work` returns. |
|
|
286
364
|
| `engine.commit(label?)` | Records changes made since the last step. Returns `false` when nothing changed. |
|
|
287
365
|
| `engine.undo()` / `engine.redo()` | Resolves to `true` when a step was applied. Calls run one after another. |
|
|
@@ -289,7 +367,7 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
289
367
|
| `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
|
|
290
368
|
| `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
|
|
291
369
|
| `bindKeyboardShortcuts(engine, { target? })` | Adds the undo and redo shortcuts. Returns an unbind function. |
|
|
292
|
-
| `engine.on(event, handler)` | Listens to `load:start`, `load:success`, `load:error`, `save:start`, `save:success`, `save:error`, `save:retry`, `save:status`, `assets:warning`, `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. |
|
|
293
371
|
| `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
|
|
294
372
|
| `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
|
|
295
373
|
|
|
@@ -312,6 +390,13 @@ Every failure is a `DocumentEngineError` with a `code` you can switch on:
|
|
|
312
390
|
| `SAVE_CANCELLED` | A queued save was dropped because another document was opened. |
|
|
313
391
|
| `DOCUMENT_NOT_FOUND` | The built-in adapters have no document with that id. |
|
|
314
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. |
|
|
397
|
+
| `RECOVERY_MISSING` | A recovery method was called without `recovery: { store }`. |
|
|
398
|
+
| `RECOVERY_NOT_FOUND` | There is no recovery copy for that document. |
|
|
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. |
|
|
315
400
|
| `STORAGE_MISSING` | `load` or `save` was called without a storage adapter. |
|
|
316
401
|
| `INVALID_CUSTOM_OBJECT` | A registered class has no static `type`, or it does not extend a Fabric class. |
|
|
317
402
|
| `ENGINE_DESTROYED` | The engine was used after `destroy()`. |
|
|
@@ -326,8 +411,8 @@ A failed load never clears or half-fills your canvas.
|
|
|
326
411
|
| 2 | Undo and redo with transactions | 0.1.0 |
|
|
327
412
|
| 3 | Safe saving: dirty state, autosave, stale-response protection | 0.2.0 |
|
|
328
413
|
| 4 | Assets and fonts | 0.3.0 |
|
|
329
|
-
| 5 | Recovery after a refresh or crash | |
|
|
330
|
-
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | |
|
|
414
|
+
| 5 | Recovery after a refresh or crash | 0.4.0 |
|
|
415
|
+
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
|
|
331
416
|
| 7 | Named versions and schema migrations | |
|
|
332
417
|
| 8 | React adapter and examples | |
|
|
333
418
|
| 9 | Hardening and benchmarks | |
|
|
@@ -172,3 +172,4 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
|
|
|
172
172
|
exports.inspectAssets = inspectAssets;
|
|
173
173
|
exports.prepareAssetsForLoad = prepareAssetsForLoad;
|
|
174
174
|
exports.prepareAssetsForSave = prepareAssetsForSave;
|
|
175
|
+
exports.rewriteUrls = rewriteUrls;
|
|
@@ -2,15 +2,20 @@ 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");
|
|
18
|
+
const require_recovery_controller = require("../recovery/recovery-controller.cjs");
|
|
14
19
|
const require_save_controller = require("../save/save-controller.cjs");
|
|
15
20
|
const require_event_emitter = require("./event-emitter.cjs");
|
|
16
21
|
//#region src/engine/create-document-engine.ts
|
|
@@ -70,7 +75,7 @@ function createDocumentEngine(options) {
|
|
|
70
75
|
return require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects;
|
|
71
76
|
},
|
|
72
77
|
onChange: (state) => events.emit("history:change", state),
|
|
73
|
-
onContentChange: () =>
|
|
78
|
+
onContentChange: () => noteContentChange()
|
|
74
79
|
});
|
|
75
80
|
const saving = require_save_controller.createSaveController({
|
|
76
81
|
getStorage: () => requireStorage(),
|
|
@@ -83,10 +88,30 @@ function createDocumentEngine(options) {
|
|
|
83
88
|
autosave: options.autosave === true ? {} : options.autosave ?? false,
|
|
84
89
|
onStateChange: (state) => events.emit("save:status", state),
|
|
85
90
|
onStart: (document) => events.emit("save:start", { document }),
|
|
86
|
-
onSuccess: (document) =>
|
|
91
|
+
onSuccess: (document) => {
|
|
92
|
+
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
93
|
+
events.emit("save:success", { document });
|
|
94
|
+
},
|
|
87
95
|
onError: (error) => events.emit("save:error", { error }),
|
|
88
96
|
onRetry: (event) => events.emit("save:retry", event)
|
|
89
97
|
});
|
|
98
|
+
const recovery = options.recovery ? require_recovery_controller.createRecoveryController({
|
|
99
|
+
...options.recovery,
|
|
100
|
+
createDocument: () => toDocument(),
|
|
101
|
+
shouldWrite: () => !destroyed && saving.state().isDirty,
|
|
102
|
+
onCheckpoint: (record) => events.emit("recovery:checkpoint", {
|
|
103
|
+
documentId: record.documentId,
|
|
104
|
+
savedAt: record.savedAt
|
|
105
|
+
}),
|
|
106
|
+
onError: (error) => {
|
|
107
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
108
|
+
events.emit("recovery:error", { error: new require_errors.DocumentEngineError("RECOVERY_FAILED", `Could not write the recovery copy: ${reason}`, { cause: error }) });
|
|
109
|
+
}
|
|
110
|
+
}) : void 0;
|
|
111
|
+
function noteContentChange() {
|
|
112
|
+
saving.noteContentChange();
|
|
113
|
+
recovery?.schedule();
|
|
114
|
+
}
|
|
90
115
|
function toDocument() {
|
|
91
116
|
ensureUsable();
|
|
92
117
|
rebuildIndex();
|
|
@@ -135,8 +160,11 @@ function createDocumentEngine(options) {
|
|
|
135
160
|
activeLoad?.abort();
|
|
136
161
|
const controller = new AbortController();
|
|
137
162
|
activeLoad = controller;
|
|
138
|
-
|
|
163
|
+
const documentId = describeDocumentId(input);
|
|
164
|
+
events.emit("load:start", { documentId });
|
|
139
165
|
try {
|
|
166
|
+
await recovery?.markLoadStarted(documentId);
|
|
167
|
+
if (controller.signal.aborted) throw new Error("aborted");
|
|
140
168
|
const checked = checkDocument(input);
|
|
141
169
|
const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
142
170
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
@@ -158,6 +186,7 @@ function createDocumentEngine(options) {
|
|
|
158
186
|
};
|
|
159
187
|
rebuildIndex();
|
|
160
188
|
history.reset();
|
|
189
|
+
recovery?.cancel();
|
|
161
190
|
saving.startSession(document.revision ?? 0);
|
|
162
191
|
canvas.requestRenderAll();
|
|
163
192
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
@@ -171,7 +200,10 @@ function createDocumentEngine(options) {
|
|
|
171
200
|
events.emit("load:error", { error: engineError });
|
|
172
201
|
throw engineError;
|
|
173
202
|
} finally {
|
|
174
|
-
if (activeLoad === controller)
|
|
203
|
+
if (activeLoad === controller) {
|
|
204
|
+
activeLoad = null;
|
|
205
|
+
await recovery?.markLoadFinished();
|
|
206
|
+
}
|
|
175
207
|
}
|
|
176
208
|
}
|
|
177
209
|
function requireStorage() {
|
|
@@ -208,6 +240,7 @@ function createDocumentEngine(options) {
|
|
|
208
240
|
documentInfo = require_create_document.createDocumentInfo(newOptions);
|
|
209
241
|
uploadedUrls.clear();
|
|
210
242
|
history.reset();
|
|
243
|
+
recovery?.cancel();
|
|
211
244
|
saving.startSession(0);
|
|
212
245
|
}
|
|
213
246
|
function getObjectById(id) {
|
|
@@ -241,6 +274,107 @@ function createDocumentEngine(options) {
|
|
|
241
274
|
canvas.requestRenderAll();
|
|
242
275
|
return images.length;
|
|
243
276
|
}
|
|
277
|
+
function fontsOnCanvas() {
|
|
278
|
+
rebuildIndex();
|
|
279
|
+
return 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
|
+
}
|
|
363
|
+
function requireRecovery() {
|
|
364
|
+
ensureUsable();
|
|
365
|
+
if (!recovery) throw new require_errors.DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
366
|
+
return recovery;
|
|
367
|
+
}
|
|
368
|
+
async function restoreRecovery(documentId, loadOptions) {
|
|
369
|
+
const source = requireRecovery();
|
|
370
|
+
const id = documentId ?? documentInfo.id;
|
|
371
|
+
const record = await source.read(id);
|
|
372
|
+
if (!record) throw new require_errors.DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
|
|
373
|
+
const loaded = await loadDocument(await require_recovery_controller.restoreRecordedFiles(record), loadOptions);
|
|
374
|
+
noteContentChange();
|
|
375
|
+
events.emit("recovery:restored", { document: loaded });
|
|
376
|
+
return loaded;
|
|
377
|
+
}
|
|
244
378
|
async function travelThroughHistory(direction) {
|
|
245
379
|
ensureUsable();
|
|
246
380
|
try {
|
|
@@ -258,6 +392,7 @@ function createDocumentEngine(options) {
|
|
|
258
392
|
activeLoad?.abort();
|
|
259
393
|
history.destroy();
|
|
260
394
|
saving.destroy();
|
|
395
|
+
recovery?.destroy();
|
|
261
396
|
canvas.off("object:added", handleObjectAdded);
|
|
262
397
|
canvas.off("object:removed", handleObjectRemoved);
|
|
263
398
|
objectsById.clear();
|
|
@@ -278,7 +413,7 @@ function createDocumentEngine(options) {
|
|
|
278
413
|
...changes
|
|
279
414
|
}
|
|
280
415
|
};
|
|
281
|
-
|
|
416
|
+
noteContentChange();
|
|
282
417
|
},
|
|
283
418
|
newDocument,
|
|
284
419
|
toDocument,
|
|
@@ -298,6 +433,14 @@ function createDocumentEngine(options) {
|
|
|
298
433
|
},
|
|
299
434
|
checkAssets,
|
|
300
435
|
replaceImage,
|
|
436
|
+
export: exportDocument,
|
|
437
|
+
preflightExport: checkBeforeExport,
|
|
438
|
+
flushRecovery: async () => requireRecovery().flush(),
|
|
439
|
+
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
440
|
+
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
441
|
+
restoreRecovery,
|
|
442
|
+
discardRecovery: async (documentId) => requireRecovery().remove(documentId ?? documentInfo.id),
|
|
443
|
+
getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
|
|
301
444
|
transaction(label, work) {
|
|
302
445
|
ensureUsable();
|
|
303
446
|
return history.transaction(label, work);
|
|
@@ -1,9 +1,12 @@
|
|
|
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";
|
|
9
|
+
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.cjs";
|
|
7
10
|
import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
|
|
8
11
|
import { RetryOptions } from "../save/retry.cjs";
|
|
9
12
|
import { DocumentEngineError } from "./errors.cjs";
|
|
@@ -21,10 +24,20 @@ export interface DocumentEngineOptions {
|
|
|
21
24
|
autosave?: boolean | AutosaveOptions;
|
|
22
25
|
saveRetry?: RetryOptions;
|
|
23
26
|
assets?: AssetOptions;
|
|
27
|
+
recovery?: RecoveryOptions;
|
|
24
28
|
}
|
|
25
29
|
export interface LoadOptions {
|
|
26
30
|
restoreCanvasSize?: boolean;
|
|
27
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
|
+
}
|
|
28
41
|
export interface DocumentEngineEvents {
|
|
29
42
|
"load:start": {
|
|
30
43
|
documentId: string | undefined;
|
|
@@ -54,6 +67,25 @@ export interface DocumentEngineEvents {
|
|
|
54
67
|
"history:error": {
|
|
55
68
|
error: DocumentEngineError;
|
|
56
69
|
};
|
|
70
|
+
"recovery:checkpoint": {
|
|
71
|
+
documentId: string;
|
|
72
|
+
savedAt: string;
|
|
73
|
+
};
|
|
74
|
+
"recovery:restored": {
|
|
75
|
+
document: FabricDocument;
|
|
76
|
+
};
|
|
77
|
+
"recovery:error": {
|
|
78
|
+
error: DocumentEngineError;
|
|
79
|
+
};
|
|
80
|
+
"export:success": {
|
|
81
|
+
format: ExportFormat;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
warnings: AssetWarning[];
|
|
85
|
+
};
|
|
86
|
+
"export:error": {
|
|
87
|
+
error: DocumentEngineError;
|
|
88
|
+
};
|
|
57
89
|
}
|
|
58
90
|
export interface DocumentEngine {
|
|
59
91
|
readonly canvas: StaticCanvas;
|
|
@@ -82,6 +114,14 @@ export interface DocumentEngine {
|
|
|
82
114
|
getAssetManifest(): AssetManifest;
|
|
83
115
|
checkAssets(): Promise<AssetReport>;
|
|
84
116
|
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
117
|
+
export(options: ExportOptions): Promise<ExportResult>;
|
|
118
|
+
preflightExport(options: ExportOptions): Promise<ExportPreflight>;
|
|
119
|
+
flushRecovery(): Promise<void>;
|
|
120
|
+
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
121
|
+
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
122
|
+
restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
123
|
+
discardRecovery(documentId?: string): Promise<void>;
|
|
124
|
+
getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
|
|
85
125
|
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
86
126
|
destroy(): void;
|
|
87
127
|
}
|
|
@@ -1,9 +1,12 @@
|
|
|
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";
|
|
9
|
+
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.js";
|
|
7
10
|
import { AutosaveOptions } from "../save/autosave-scheduler.js";
|
|
8
11
|
import { RetryOptions } from "../save/retry.js";
|
|
9
12
|
import { DocumentEngineError } from "./errors.js";
|
|
@@ -21,10 +24,20 @@ export interface DocumentEngineOptions {
|
|
|
21
24
|
autosave?: boolean | AutosaveOptions;
|
|
22
25
|
saveRetry?: RetryOptions;
|
|
23
26
|
assets?: AssetOptions;
|
|
27
|
+
recovery?: RecoveryOptions;
|
|
24
28
|
}
|
|
25
29
|
export interface LoadOptions {
|
|
26
30
|
restoreCanvasSize?: boolean;
|
|
27
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
|
+
}
|
|
28
41
|
export interface DocumentEngineEvents {
|
|
29
42
|
"load:start": {
|
|
30
43
|
documentId: string | undefined;
|
|
@@ -54,6 +67,25 @@ export interface DocumentEngineEvents {
|
|
|
54
67
|
"history:error": {
|
|
55
68
|
error: DocumentEngineError;
|
|
56
69
|
};
|
|
70
|
+
"recovery:checkpoint": {
|
|
71
|
+
documentId: string;
|
|
72
|
+
savedAt: string;
|
|
73
|
+
};
|
|
74
|
+
"recovery:restored": {
|
|
75
|
+
document: FabricDocument;
|
|
76
|
+
};
|
|
77
|
+
"recovery:error": {
|
|
78
|
+
error: DocumentEngineError;
|
|
79
|
+
};
|
|
80
|
+
"export:success": {
|
|
81
|
+
format: ExportFormat;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
warnings: AssetWarning[];
|
|
85
|
+
};
|
|
86
|
+
"export:error": {
|
|
87
|
+
error: DocumentEngineError;
|
|
88
|
+
};
|
|
57
89
|
}
|
|
58
90
|
export interface DocumentEngine {
|
|
59
91
|
readonly canvas: StaticCanvas;
|
|
@@ -82,6 +114,14 @@ export interface DocumentEngine {
|
|
|
82
114
|
getAssetManifest(): AssetManifest;
|
|
83
115
|
checkAssets(): Promise<AssetReport>;
|
|
84
116
|
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
117
|
+
export(options: ExportOptions): Promise<ExportResult>;
|
|
118
|
+
preflightExport(options: ExportOptions): Promise<ExportPreflight>;
|
|
119
|
+
flushRecovery(): Promise<void>;
|
|
120
|
+
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
121
|
+
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
122
|
+
restoreRecovery(documentId?: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
123
|
+
discardRecovery(documentId?: string): Promise<void>;
|
|
124
|
+
getInterruptedLoad(): Promise<InterruptedLoad | undefined>;
|
|
85
125
|
on<Name extends keyof DocumentEngineEvents>(name: Name, handler: (payload: DocumentEngineEvents[Name]) => void): Unsubscribe;
|
|
86
126
|
destroy(): void;
|
|
87
127
|
}
|