fabricjs-document-engine 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -3
- package/dist/engine/create-document-engine.cjs +199 -6
- package/dist/engine/create-document-engine.d.cts +42 -0
- package/dist/engine/create-document-engine.d.ts +42 -0
- package/dist/engine/create-document-engine.js +199 -6
- package/dist/engine/errors.cjs +2 -0
- package/dist/engine/errors.d.cts +6 -1
- package/dist/engine/errors.d.ts +6 -1
- package/dist/engine/errors.js +2 -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 +5 -0
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +3 -1
- package/dist/migrations/migrate-document.cjs +65 -0
- package/dist/migrations/migrate-document.d.cts +15 -0
- package/dist/migrations/migrate-document.d.ts +15 -0
- package/dist/migrations/migrate-document.js +63 -0
- package/dist/storage/key-value-storage.cjs +27 -1
- package/dist/storage/key-value-storage.d.cts +2 -1
- package/dist/storage/key-value-storage.d.ts +2 -1
- package/dist/storage/key-value-storage.js +27 -1
- package/dist/storage.d.cts +2 -1
- package/dist/storage.d.ts +2 -1
- package/dist/versions/document-version.cjs +20 -0
- package/dist/versions/document-version.d.cts +25 -0
- package/dist/versions/document-version.d.ts +25 -0
- package/dist/versions/document-version.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ 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.
|
|
16
|
+
- **Versions and migration.** Keep named versions, restore any of them as a new revision, and open plain Fabric JSON or documents saved by older versions of this package.
|
|
15
17
|
- **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
18
|
- **Fabric 6 and 7.** Every release is tested against both.
|
|
17
19
|
|
|
@@ -176,6 +178,74 @@ await engine.replaceImage('/old-logo.png', '/new-logo.png');
|
|
|
176
178
|
|
|
177
179
|
`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
180
|
|
|
181
|
+
## Export
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { downloadExport } from 'fabricjs-document-engine';
|
|
185
|
+
|
|
186
|
+
const result = await engine.export({ format: 'png', scale: 2 });
|
|
187
|
+
downloadExport(result, 'poster.png');
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`result` is `{ format, mimeType, blob, width, height, warnings }`. JSON exports also include `document`.
|
|
191
|
+
|
|
192
|
+
| Option | Values | Default |
|
|
193
|
+
| --- | --- | --- |
|
|
194
|
+
| `format` | `'png'`, `'jpeg'`, `'webp'`, `'svg'` or `'json'` | required |
|
|
195
|
+
| `scale` | Output size multiplier, such as `2` for retina | `1` |
|
|
196
|
+
| `quality` | 0 to 1, for JPEG and WebP | `0.92` |
|
|
197
|
+
| `area` | `'canvas'`, `'content'` (every object), `'selection'`, or `{ left, top, width, height }` | `'canvas'` |
|
|
198
|
+
| `padding` | Extra space around `content` or `selection` | `0` |
|
|
199
|
+
| `background` | `'keep'`, `'transparent'` or any CSS color | `'keep'` |
|
|
200
|
+
| `signal` | An `AbortSignal` to cancel | |
|
|
201
|
+
|
|
202
|
+
- The current zoom and pan do not matter. Exports always use document coordinates, and the view is restored afterwards.
|
|
203
|
+
- A JPEG has no transparency, so an empty or transparent background becomes white instead of black.
|
|
204
|
+
- The export never changes the canvas, the history or the unsaved state.
|
|
205
|
+
- A JSON export is the same portable document a save produces, including uploaded images when `assets.upload` is set.
|
|
206
|
+
|
|
207
|
+
### Preflight and errors
|
|
208
|
+
|
|
209
|
+
Before rendering, the engine checks the objects on the canvas:
|
|
210
|
+
|
|
211
|
+
- **`MISSING_IMAGE`**: an image failed to load.
|
|
212
|
+
- **`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.
|
|
213
|
+
- **`MISSING_FONT`**: a font is not available and `assets.requireFonts` is on. Otherwise you get a `FONT_UNAVAILABLE` warning.
|
|
214
|
+
|
|
215
|
+
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:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const check = await engine.preflightExport({ format: 'png' });
|
|
219
|
+
if (!check.ok) showProblems(check.problems);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Versions
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const version = await engine.createVersion('Sent to client');
|
|
226
|
+
const versions = await engine.listVersions();
|
|
227
|
+
await engine.restoreVersion(version.id);
|
|
228
|
+
await engine.deleteVersion(version.id);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- Versions are full copies of the document, kept in your storage adapter. The built-in adapters support them. A custom adapter adds four methods: `saveVersion(version)`, `listVersions(documentId)`, `loadVersion(documentId, versionId)` and `deleteVersion(documentId, versionId)`.
|
|
232
|
+
- `listVersions` returns summaries, newest first: `{ id, documentId, name, kind, createdAt, revision }`. `kind` is `named` or `auto`.
|
|
233
|
+
- **Restoring never loses work.** The engine first keeps an automatic version named `Before restoring "..."`. It then loads the old content as a new, unsaved revision of the same document. The next save stores it as the newest revision, and history stays linear. To undo a restore, restore the automatic version.
|
|
234
|
+
- **Automatic versions.** Use `versions: { autoEvery: 10, keepAuto: 20 }` to keep a version after every 10 successful saves. Named versions are never pruned. Only the newest `keepAuto` automatic versions are kept, 20 by default.
|
|
235
|
+
- Undo and redo cover recent edits in this session. Versions preserve chosen states for later.
|
|
236
|
+
|
|
237
|
+
## Migration and importing Fabric JSON
|
|
238
|
+
|
|
239
|
+
Plain Fabric JSON, such as the output of `canvas.toJSON()` from Fabric 5, 6 or 7, opens directly:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
await engine.importFabricJson(savedJsonText, { id: 'plan-42', metadata: { source: 'old editor' } });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`loadDocument` and `load(id)` also recognise plain Fabric JSON, so projects stored by an existing Fabric app open without a separate import step. A document loaded with `load(id)` keeps that id, and its next save stores it in the current format.
|
|
246
|
+
|
|
247
|
+
Every document records its `schemaVersion`. When the package format changes, older documents are upgraded step by step when they are opened. `load:success` reports `migratedFrom` when that happened. A failed step rejects with `MIGRATION_FAILED`, and `error.migrationFrom` names the version it started from. A document from a newer version of the package is refused with `UNSUPPORTED_SCHEMA` rather than being misread. `migrateDocument(value, context)` and `detectSchemaVersion(value)` are exported for tooling such as server-side batch upgrades.
|
|
248
|
+
|
|
179
249
|
## Recovery
|
|
180
250
|
|
|
181
251
|
```ts
|
|
@@ -300,6 +370,9 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
300
370
|
| `engine.toDocument()` | Serializes the canvas into the versioned document format. |
|
|
301
371
|
| `engine.loadDocument(document, { restoreCanvasSize? })` | Validates and loads a document. Resolves when the objects are on the canvas. |
|
|
302
372
|
| `engine.load(id)` / `engine.save({ overwrite? })` | Reads from or writes to your storage adapter. |
|
|
373
|
+
| `engine.importFabricJson(json, { id?, metadata? })` | Opens plain Fabric JSON, as text or an object. |
|
|
374
|
+
| `engine.createVersion(name?)` / `engine.listVersions()` | Keeps a named version, or lists versions newest first. |
|
|
375
|
+
| `engine.restoreVersion(id)` / `engine.deleteVersion(id)` | Restores a version as a new unsaved revision, or deletes it. |
|
|
303
376
|
| `engine.isDirty()` | Tells you whether there are unsaved changes. |
|
|
304
377
|
| `engine.getSaveState()` | Returns `{ status, isDirty, isSaving, revision, lastSavedAt, error }`. |
|
|
305
378
|
| `bindUnsavedChangesWarning(engine)` | Asks the browser to confirm before closing a page with unsaved changes. Returns an unbind function. |
|
|
@@ -311,6 +384,9 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
311
384
|
| `engine.getAssetManifest()` | Lists the images and fonts used on the canvas. |
|
|
312
385
|
| `engine.checkAssets()` | Resolves to `{ manifest, missingImages, unavailableFonts, warnings }` for the current canvas. |
|
|
313
386
|
| `engine.replaceImage(oldUrl, newUrl)` | Replaces every image with that URL as one undo step. Resolves to the number of images replaced. |
|
|
387
|
+
| `engine.export(options)` | Exports PNG, JPEG, WebP, SVG or JSON. See [Export](#export). |
|
|
388
|
+
| `engine.preflightExport(options)` | Resolves to `{ ok, problems, warnings }` without exporting. |
|
|
389
|
+
| `downloadExport(result, fileName?)` | Starts a browser download of an export result. |
|
|
314
390
|
| `engine.getRecoverableDocuments()` | Lists recovery copies, newest first. |
|
|
315
391
|
| `engine.restoreRecovery(id?)` / `engine.discardRecovery(id?)` | Loads or deletes a recovery copy. The default is the current document. |
|
|
316
392
|
| `engine.getRecovery(id?)` / `engine.flushRecovery()` | Reads a copy, or writes one now. |
|
|
@@ -322,7 +398,7 @@ Keep project data such as titles, owners and tags in `metadata` with `engine.upd
|
|
|
322
398
|
| `engine.getHistory()` | Returns `{ undo, redo }` label lists, newest first. |
|
|
323
399
|
| `engine.clearHistory()` | Forgets all steps. Loading a document or starting a new one also does this. |
|
|
324
400
|
| `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. |
|
|
401
|
+
| `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`, `version:created`, `version:restored`, `version:error`, `history:change` or `history:error`. Returns an unsubscribe function. |
|
|
326
402
|
| `engine.destroy()` | Stops listening to the canvas and cancels a running load. |
|
|
327
403
|
| `validateDocument(value)` | Returns a list of issues with the exact path of each problem. |
|
|
328
404
|
|
|
@@ -345,6 +421,14 @@ Every failure is a `DocumentEngineError` with a `code` you can switch on:
|
|
|
345
421
|
| `SAVE_CANCELLED` | A queued save was dropped because another document was opened. |
|
|
346
422
|
| `DOCUMENT_NOT_FOUND` | The built-in adapters have no document with that id. |
|
|
347
423
|
| `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. |
|
|
424
|
+
| `MIGRATION_FAILED` | An older document could not be upgraded. `error.migrationFrom` is the schema version it started from. |
|
|
425
|
+
| `VERSIONS_UNSUPPORTED` | The storage adapter has no version methods. |
|
|
426
|
+
| `VERSION_NOT_FOUND` | There is no version with that id. |
|
|
427
|
+
| `VERSION_FAILED` | An automatic version could not be kept. It is delivered as a `version:error` event. |
|
|
428
|
+
| `EXPORT_BLOCKED` | The preflight found problems. `error.problems` lists each one with the objects involved. |
|
|
429
|
+
| `INVALID_EXPORT_OPTIONS` | The format, scale, quality, area or padding is not valid, or the area is empty. |
|
|
430
|
+
| `EXPORT_ABORTED` | The export was cancelled with its `signal`. |
|
|
431
|
+
| `EXPORT_FAILED` | Fabric could not render the export. `error.cause` holds the original error. |
|
|
348
432
|
| `RECOVERY_MISSING` | A recovery method was called without `recovery: { store }`. |
|
|
349
433
|
| `RECOVERY_NOT_FOUND` | There is no recovery copy for that document. |
|
|
350
434
|
| `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,8 +447,8 @@ A failed load never clears or half-fills your canvas.
|
|
|
363
447
|
| 3 | Safe saving: dirty state, autosave, stale-response protection | 0.2.0 |
|
|
364
448
|
| 4 | Assets and fonts | 0.3.0 |
|
|
365
449
|
| 5 | Recovery after a refresh or crash | 0.4.0 |
|
|
366
|
-
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | |
|
|
367
|
-
| 7 | Named versions and schema migrations | |
|
|
450
|
+
| 6 | PNG, JPEG, SVG and JSON export with preflight checks | 0.5.0 |
|
|
451
|
+
| 7 | Named versions and schema migrations | 0.6.0 |
|
|
368
452
|
| 8 | React adapter and examples | |
|
|
369
453
|
| 9 | Hardening and benchmarks | |
|
|
370
454
|
| 10 | Stable API | 1.0.0 |
|
|
@@ -2,16 +2,22 @@ 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_migrate_document = require("../migrations/migrate-document.cjs");
|
|
14
19
|
const require_recovery_controller = require("../recovery/recovery-controller.cjs");
|
|
20
|
+
const require_document_version = require("../versions/document-version.cjs");
|
|
15
21
|
const require_save_controller = require("../save/save-controller.cjs");
|
|
16
22
|
const require_event_emitter = require("./event-emitter.cjs");
|
|
17
23
|
//#region src/engine/create-document-engine.ts
|
|
@@ -87,6 +93,7 @@ function createDocumentEngine(options) {
|
|
|
87
93
|
onSuccess: (document) => {
|
|
88
94
|
if (recovery && !saving.state().isDirty) recovery.remove(document.id);
|
|
89
95
|
events.emit("save:success", { document });
|
|
96
|
+
createAutomaticVersionAfterSave(document);
|
|
90
97
|
},
|
|
91
98
|
onError: (error) => events.emit("save:error", { error }),
|
|
92
99
|
onRetry: (event) => events.emit("save:retry", event)
|
|
@@ -151,7 +158,10 @@ function createDocumentEngine(options) {
|
|
|
151
158
|
if (unknownTypes.length > 0) throw new require_errors.DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
|
|
152
159
|
return document;
|
|
153
160
|
}
|
|
154
|
-
|
|
161
|
+
function loadDocument(input, loadOptions = {}) {
|
|
162
|
+
return loadMigratedDocument(input, loadOptions, {});
|
|
163
|
+
}
|
|
164
|
+
async function loadMigratedDocument(input, loadOptions, importDetails) {
|
|
155
165
|
ensureUsable();
|
|
156
166
|
activeLoad?.abort();
|
|
157
167
|
const controller = new AbortController();
|
|
@@ -161,7 +171,12 @@ function createDocumentEngine(options) {
|
|
|
161
171
|
try {
|
|
162
172
|
await recovery?.markLoadStarted(documentId);
|
|
163
173
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
164
|
-
const
|
|
174
|
+
const { document: migrated, migratedFrom } = require_migrate_document.migrateDocument(input, {
|
|
175
|
+
canvasWidth: canvas.getWidth(),
|
|
176
|
+
canvasHeight: canvas.getHeight(),
|
|
177
|
+
...importDetails
|
|
178
|
+
});
|
|
179
|
+
const checked = checkDocument(migrated);
|
|
165
180
|
const { document, warnings } = await require_asset_pipeline.prepareAssetsForLoad(checked, assetOptions, controller.signal);
|
|
166
181
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
167
182
|
await history.withoutRecording(() => require_fabric_adapter.loadIntoCanvas(canvas, {
|
|
@@ -188,7 +203,8 @@ function createDocumentEngine(options) {
|
|
|
188
203
|
if (warnings.length > 0) events.emit("assets:warning", { warnings });
|
|
189
204
|
events.emit("load:success", {
|
|
190
205
|
document,
|
|
191
|
-
warnings
|
|
206
|
+
warnings,
|
|
207
|
+
migratedFrom
|
|
192
208
|
});
|
|
193
209
|
return document;
|
|
194
210
|
} catch (error) {
|
|
@@ -202,6 +218,90 @@ function createDocumentEngine(options) {
|
|
|
202
218
|
}
|
|
203
219
|
}
|
|
204
220
|
}
|
|
221
|
+
function importFabricJson(json, importOptions = {}) {
|
|
222
|
+
let parsed = json;
|
|
223
|
+
if (typeof json === "string") try {
|
|
224
|
+
parsed = JSON.parse(json);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
227
|
+
return Promise.reject(new require_errors.DocumentEngineError("INVALID_DOCUMENT", `The text is not valid JSON: ${reason}`, { cause: error }));
|
|
228
|
+
}
|
|
229
|
+
const { id, metadata, ...loadOptions } = importOptions;
|
|
230
|
+
return loadMigratedDocument(parsed, loadOptions, {
|
|
231
|
+
id,
|
|
232
|
+
metadata
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
let savesSinceAutomaticVersion = 0;
|
|
236
|
+
let lastVersionTime = 0;
|
|
237
|
+
function nextVersionTimestamp() {
|
|
238
|
+
lastVersionTime = Math.max(Date.now(), lastVersionTime + 1);
|
|
239
|
+
return new Date(lastVersionTime).toISOString();
|
|
240
|
+
}
|
|
241
|
+
function requireVersions() {
|
|
242
|
+
ensureUsable();
|
|
243
|
+
if (!require_document_version.supportsVersions(storage)) throw new require_errors.DocumentEngineError("VERSIONS_UNSUPPORTED", "The storage adapter needs saveVersion, listVersions, loadVersion and deleteVersion to keep versions");
|
|
244
|
+
return storage;
|
|
245
|
+
}
|
|
246
|
+
async function pruneAutomaticVersions(versionStorage, documentId) {
|
|
247
|
+
const keepAuto = options.versions?.keepAuto ?? 20;
|
|
248
|
+
const existing = await versionStorage.listVersions(documentId);
|
|
249
|
+
await Promise.all(require_document_version.versionsToPrune(existing, keepAuto).map((version) => versionStorage.deleteVersion(documentId, version.id)));
|
|
250
|
+
}
|
|
251
|
+
async function storeVersion(name, kind, document) {
|
|
252
|
+
const versionStorage = requireVersions();
|
|
253
|
+
const content = document ?? (await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls)).document;
|
|
254
|
+
const version = {
|
|
255
|
+
id: require_ids.createId(),
|
|
256
|
+
documentId: content.id,
|
|
257
|
+
name,
|
|
258
|
+
kind,
|
|
259
|
+
createdAt: nextVersionTimestamp(),
|
|
260
|
+
revision: content.revision ?? saving.state().revision,
|
|
261
|
+
document: content
|
|
262
|
+
};
|
|
263
|
+
await versionStorage.saveVersion(version);
|
|
264
|
+
await pruneAutomaticVersions(versionStorage, content.id);
|
|
265
|
+
const summary = require_document_version.summarize(version);
|
|
266
|
+
events.emit("version:created", summary);
|
|
267
|
+
return summary;
|
|
268
|
+
}
|
|
269
|
+
function reportVersionError(error) {
|
|
270
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
271
|
+
const engineError = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("VERSION_FAILED", `Could not keep an automatic version: ${reason}`, { cause: error });
|
|
272
|
+
events.emit("version:error", { error: engineError });
|
|
273
|
+
}
|
|
274
|
+
function createAutomaticVersionAfterSave(document) {
|
|
275
|
+
const every = options.versions?.autoEvery ?? 0;
|
|
276
|
+
if (every <= 0 || !require_document_version.supportsVersions(storage)) return;
|
|
277
|
+
savesSinceAutomaticVersion += 1;
|
|
278
|
+
if (savesSinceAutomaticVersion < every) return;
|
|
279
|
+
savesSinceAutomaticVersion = 0;
|
|
280
|
+
storeVersion(`Autosave ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "auto", document).catch(reportVersionError);
|
|
281
|
+
}
|
|
282
|
+
async function restoreVersion(versionId) {
|
|
283
|
+
const versionStorage = requireVersions();
|
|
284
|
+
const current = documentInfo;
|
|
285
|
+
const version = await versionStorage.loadVersion(current.id, versionId);
|
|
286
|
+
await storeVersion(`Before restoring "${version.name}"`, "auto");
|
|
287
|
+
const { document: migrated } = require_migrate_document.migrateDocument(version.document, {
|
|
288
|
+
canvasWidth: canvas.getWidth(),
|
|
289
|
+
canvasHeight: canvas.getHeight()
|
|
290
|
+
});
|
|
291
|
+
const baseRevision = saving.state().revision;
|
|
292
|
+
const loaded = await loadDocument({
|
|
293
|
+
...migrated,
|
|
294
|
+
id: current.id,
|
|
295
|
+
createdAt: current.createdAt,
|
|
296
|
+
revision: baseRevision
|
|
297
|
+
});
|
|
298
|
+
noteContentChange();
|
|
299
|
+
events.emit("version:restored", {
|
|
300
|
+
version: require_document_version.summarize(version),
|
|
301
|
+
document: loaded
|
|
302
|
+
});
|
|
303
|
+
return loaded;
|
|
304
|
+
}
|
|
205
305
|
function requireStorage() {
|
|
206
306
|
if (!storage) throw new require_errors.DocumentEngineError("STORAGE_MISSING", "Pass a storage adapter to createDocumentEngine to use load and save");
|
|
207
307
|
return storage;
|
|
@@ -218,7 +318,7 @@ function createDocumentEngine(options) {
|
|
|
218
318
|
events.emit("load:error", { error: engineError });
|
|
219
319
|
throw engineError;
|
|
220
320
|
}
|
|
221
|
-
return
|
|
321
|
+
return loadMigratedDocument(stored, loadOptions ?? {}, { id: documentId });
|
|
222
322
|
}
|
|
223
323
|
function save(saveOptions) {
|
|
224
324
|
try {
|
|
@@ -270,6 +370,92 @@ function createDocumentEngine(options) {
|
|
|
270
370
|
canvas.requestRenderAll();
|
|
271
371
|
return images.length;
|
|
272
372
|
}
|
|
373
|
+
function fontsOnCanvas() {
|
|
374
|
+
rebuildIndex();
|
|
375
|
+
return require_asset_manifest.buildAssetManifest(require_fabric_adapter.serializeCanvas(canvas, registry.propertiesToInclude()).objects).fonts;
|
|
376
|
+
}
|
|
377
|
+
async function checkBeforeExport(exportOptions) {
|
|
378
|
+
ensureUsable();
|
|
379
|
+
const { format } = require_export_options.normalizeExportOptions(exportOptions);
|
|
380
|
+
return require_preflight_export.preflightExport(canvas, format, format === "json" ? [] : fontsOnCanvas(), assetOptions);
|
|
381
|
+
}
|
|
382
|
+
function describeProblems(check) {
|
|
383
|
+
return check.problems.map((problem) => problem.message).join("; ");
|
|
384
|
+
}
|
|
385
|
+
async function exportContent(exportOptions) {
|
|
386
|
+
const settings = require_export_options.normalizeExportOptions(exportOptions);
|
|
387
|
+
const stopIfCancelled = () => {
|
|
388
|
+
if (settings.signal?.aborted) throw new require_errors.DocumentEngineError("EXPORT_ABORTED", "The export was cancelled");
|
|
389
|
+
};
|
|
390
|
+
stopIfCancelled();
|
|
391
|
+
if (settings.format === "json") {
|
|
392
|
+
const prepared = await require_asset_pipeline.prepareAssetsForSave(toDocument(), assetOptions, uploadedUrls);
|
|
393
|
+
stopIfCancelled();
|
|
394
|
+
return {
|
|
395
|
+
format: "json",
|
|
396
|
+
mimeType: require_render_export.mimeTypes.json,
|
|
397
|
+
blob: new Blob([JSON.stringify(prepared.document)], { type: require_render_export.mimeTypes.json }),
|
|
398
|
+
width: prepared.document.canvas.width,
|
|
399
|
+
height: prepared.document.canvas.height,
|
|
400
|
+
warnings: prepared.warnings,
|
|
401
|
+
document: prepared.document
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const check = await checkBeforeExport(settings);
|
|
405
|
+
stopIfCancelled();
|
|
406
|
+
if (!check.ok) throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", `The export cannot run: ${describeProblems(check)}`, { problems: check.problems });
|
|
407
|
+
const format = settings.format;
|
|
408
|
+
try {
|
|
409
|
+
const rendered = require_render_export.withExportView(canvas, settings.background, format, () => {
|
|
410
|
+
const area = require_export_area.resolveExportArea(canvas, settings.area, settings.padding);
|
|
411
|
+
return {
|
|
412
|
+
area,
|
|
413
|
+
content: format === "svg" ? require_render_export.renderSvg(canvas, area, settings.scale) : require_render_export.renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
414
|
+
};
|
|
415
|
+
});
|
|
416
|
+
const blob = format === "svg" ? new Blob([rendered.content], { type: require_render_export.mimeTypes.svg }) : require_render_export.dataUrlToBlob(rendered.content);
|
|
417
|
+
return {
|
|
418
|
+
format,
|
|
419
|
+
mimeType: blob.type || require_render_export.mimeTypes[format],
|
|
420
|
+
blob,
|
|
421
|
+
width: Math.round(rendered.area.width * settings.scale),
|
|
422
|
+
height: Math.round(rendered.area.height * settings.scale),
|
|
423
|
+
warnings: check.warnings
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (require_errors.isDocumentEngineError(error)) throw error;
|
|
427
|
+
if (error?.name === "SecurityError") {
|
|
428
|
+
const problem = {
|
|
429
|
+
code: "CROSS_ORIGIN_IMAGE",
|
|
430
|
+
message: "The browser blocked the export because the canvas shows an image from another site without CORS",
|
|
431
|
+
objectIds: []
|
|
432
|
+
};
|
|
433
|
+
throw new require_errors.DocumentEngineError("EXPORT_BLOCKED", problem.message, {
|
|
434
|
+
problems: [problem],
|
|
435
|
+
cause: error
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
439
|
+
throw new require_errors.DocumentEngineError("EXPORT_FAILED", `Fabric could not export the canvas: ${reason}`, { cause: error });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function exportDocument(exportOptions) {
|
|
443
|
+
ensureUsable();
|
|
444
|
+
try {
|
|
445
|
+
const result = await exportContent(exportOptions);
|
|
446
|
+
events.emit("export:success", {
|
|
447
|
+
format: result.format,
|
|
448
|
+
width: result.width,
|
|
449
|
+
height: result.height,
|
|
450
|
+
warnings: result.warnings
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
const engineError = require_errors.isDocumentEngineError(error) ? error : new require_errors.DocumentEngineError("EXPORT_FAILED", String(error), { cause: error });
|
|
455
|
+
events.emit("export:error", { error: engineError });
|
|
456
|
+
throw engineError;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
273
459
|
function requireRecovery() {
|
|
274
460
|
ensureUsable();
|
|
275
461
|
if (!recovery) throw new require_errors.DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
@@ -329,6 +515,11 @@ function createDocumentEngine(options) {
|
|
|
329
515
|
toDocument,
|
|
330
516
|
loadDocument,
|
|
331
517
|
load,
|
|
518
|
+
importFabricJson,
|
|
519
|
+
createVersion: (name) => storeVersion(name ?? `Version ${(/* @__PURE__ */ new Date()).toLocaleString()}`, "named"),
|
|
520
|
+
listVersions: async (documentId) => requireVersions().listVersions(documentId ?? documentInfo.id),
|
|
521
|
+
restoreVersion,
|
|
522
|
+
deleteVersion: async (versionId) => requireVersions().deleteVersion(documentInfo.id, versionId),
|
|
332
523
|
save,
|
|
333
524
|
isDirty: () => saving.state().isDirty,
|
|
334
525
|
getSaveState: () => saving.state(),
|
|
@@ -343,6 +534,8 @@ function createDocumentEngine(options) {
|
|
|
343
534
|
},
|
|
344
535
|
checkAssets,
|
|
345
536
|
replaceImage,
|
|
537
|
+
export: exportDocument,
|
|
538
|
+
preflightExport: checkBeforeExport,
|
|
346
539
|
flushRecovery: async () => requireRecovery().flush(),
|
|
347
540
|
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
348
541
|
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
@@ -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 { VersionOptions, VersionSummary } from "../versions/document-version.cjs";
|
|
7
10
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.cjs";
|
|
8
11
|
import { AutosaveOptions } from "../save/autosave-scheduler.cjs";
|
|
9
12
|
import { RetryOptions } from "../save/retry.cjs";
|
|
@@ -23,10 +26,24 @@ export interface DocumentEngineOptions {
|
|
|
23
26
|
saveRetry?: RetryOptions;
|
|
24
27
|
assets?: AssetOptions;
|
|
25
28
|
recovery?: RecoveryOptions;
|
|
29
|
+
versions?: VersionOptions;
|
|
26
30
|
}
|
|
27
31
|
export interface LoadOptions {
|
|
28
32
|
restoreCanvasSize?: boolean;
|
|
29
33
|
}
|
|
34
|
+
export interface ImportOptions extends LoadOptions {
|
|
35
|
+
id?: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
export interface ExportResult {
|
|
39
|
+
format: ExportFormat;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
blob: Blob;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
warnings: AssetWarning[];
|
|
45
|
+
document?: FabricDocument;
|
|
46
|
+
}
|
|
30
47
|
export interface DocumentEngineEvents {
|
|
31
48
|
"load:start": {
|
|
32
49
|
documentId: string | undefined;
|
|
@@ -34,6 +51,7 @@ export interface DocumentEngineEvents {
|
|
|
34
51
|
"load:success": {
|
|
35
52
|
document: FabricDocument;
|
|
36
53
|
warnings: AssetWarning[];
|
|
54
|
+
migratedFrom: number | undefined;
|
|
37
55
|
};
|
|
38
56
|
"assets:warning": {
|
|
39
57
|
warnings: AssetWarning[];
|
|
@@ -66,6 +84,23 @@ export interface DocumentEngineEvents {
|
|
|
66
84
|
"recovery:error": {
|
|
67
85
|
error: DocumentEngineError;
|
|
68
86
|
};
|
|
87
|
+
"export:success": {
|
|
88
|
+
format: ExportFormat;
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
warnings: AssetWarning[];
|
|
92
|
+
};
|
|
93
|
+
"export:error": {
|
|
94
|
+
error: DocumentEngineError;
|
|
95
|
+
};
|
|
96
|
+
"version:created": VersionSummary;
|
|
97
|
+
"version:restored": {
|
|
98
|
+
version: VersionSummary;
|
|
99
|
+
document: FabricDocument;
|
|
100
|
+
};
|
|
101
|
+
"version:error": {
|
|
102
|
+
error: DocumentEngineError;
|
|
103
|
+
};
|
|
69
104
|
}
|
|
70
105
|
export interface DocumentEngine {
|
|
71
106
|
readonly canvas: StaticCanvas;
|
|
@@ -75,6 +110,11 @@ export interface DocumentEngine {
|
|
|
75
110
|
toDocument(): FabricDocument;
|
|
76
111
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
77
112
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
113
|
+
importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
|
|
114
|
+
createVersion(name?: string): Promise<VersionSummary>;
|
|
115
|
+
listVersions(documentId?: string): Promise<VersionSummary[]>;
|
|
116
|
+
restoreVersion(versionId: string): Promise<FabricDocument>;
|
|
117
|
+
deleteVersion(versionId: string): Promise<void>;
|
|
78
118
|
save(options?: SaveOptions): Promise<FabricDocument>;
|
|
79
119
|
isDirty(): boolean;
|
|
80
120
|
getSaveState(): SaveState;
|
|
@@ -94,6 +134,8 @@ export interface DocumentEngine {
|
|
|
94
134
|
getAssetManifest(): AssetManifest;
|
|
95
135
|
checkAssets(): Promise<AssetReport>;
|
|
96
136
|
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
137
|
+
export(options: ExportOptions): Promise<ExportResult>;
|
|
138
|
+
preflightExport(options: ExportOptions): Promise<ExportPreflight>;
|
|
97
139
|
flushRecovery(): Promise<void>;
|
|
98
140
|
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
99
141
|
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|
|
@@ -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 { VersionOptions, VersionSummary } from "../versions/document-version.js";
|
|
7
10
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "../recovery/recovery-controller.js";
|
|
8
11
|
import { AutosaveOptions } from "../save/autosave-scheduler.js";
|
|
9
12
|
import { RetryOptions } from "../save/retry.js";
|
|
@@ -23,10 +26,24 @@ export interface DocumentEngineOptions {
|
|
|
23
26
|
saveRetry?: RetryOptions;
|
|
24
27
|
assets?: AssetOptions;
|
|
25
28
|
recovery?: RecoveryOptions;
|
|
29
|
+
versions?: VersionOptions;
|
|
26
30
|
}
|
|
27
31
|
export interface LoadOptions {
|
|
28
32
|
restoreCanvasSize?: boolean;
|
|
29
33
|
}
|
|
34
|
+
export interface ImportOptions extends LoadOptions {
|
|
35
|
+
id?: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
export interface ExportResult {
|
|
39
|
+
format: ExportFormat;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
blob: Blob;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
warnings: AssetWarning[];
|
|
45
|
+
document?: FabricDocument;
|
|
46
|
+
}
|
|
30
47
|
export interface DocumentEngineEvents {
|
|
31
48
|
"load:start": {
|
|
32
49
|
documentId: string | undefined;
|
|
@@ -34,6 +51,7 @@ export interface DocumentEngineEvents {
|
|
|
34
51
|
"load:success": {
|
|
35
52
|
document: FabricDocument;
|
|
36
53
|
warnings: AssetWarning[];
|
|
54
|
+
migratedFrom: number | undefined;
|
|
37
55
|
};
|
|
38
56
|
"assets:warning": {
|
|
39
57
|
warnings: AssetWarning[];
|
|
@@ -66,6 +84,23 @@ export interface DocumentEngineEvents {
|
|
|
66
84
|
"recovery:error": {
|
|
67
85
|
error: DocumentEngineError;
|
|
68
86
|
};
|
|
87
|
+
"export:success": {
|
|
88
|
+
format: ExportFormat;
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
warnings: AssetWarning[];
|
|
92
|
+
};
|
|
93
|
+
"export:error": {
|
|
94
|
+
error: DocumentEngineError;
|
|
95
|
+
};
|
|
96
|
+
"version:created": VersionSummary;
|
|
97
|
+
"version:restored": {
|
|
98
|
+
version: VersionSummary;
|
|
99
|
+
document: FabricDocument;
|
|
100
|
+
};
|
|
101
|
+
"version:error": {
|
|
102
|
+
error: DocumentEngineError;
|
|
103
|
+
};
|
|
69
104
|
}
|
|
70
105
|
export interface DocumentEngine {
|
|
71
106
|
readonly canvas: StaticCanvas;
|
|
@@ -75,6 +110,11 @@ export interface DocumentEngine {
|
|
|
75
110
|
toDocument(): FabricDocument;
|
|
76
111
|
loadDocument(document: unknown, options?: LoadOptions): Promise<FabricDocument>;
|
|
77
112
|
load(documentId: string, options?: LoadOptions): Promise<FabricDocument>;
|
|
113
|
+
importFabricJson(json: string | Record<string, unknown>, options?: ImportOptions): Promise<FabricDocument>;
|
|
114
|
+
createVersion(name?: string): Promise<VersionSummary>;
|
|
115
|
+
listVersions(documentId?: string): Promise<VersionSummary[]>;
|
|
116
|
+
restoreVersion(versionId: string): Promise<FabricDocument>;
|
|
117
|
+
deleteVersion(versionId: string): Promise<void>;
|
|
78
118
|
save(options?: SaveOptions): Promise<FabricDocument>;
|
|
79
119
|
isDirty(): boolean;
|
|
80
120
|
getSaveState(): SaveState;
|
|
@@ -94,6 +134,8 @@ export interface DocumentEngine {
|
|
|
94
134
|
getAssetManifest(): AssetManifest;
|
|
95
135
|
checkAssets(): Promise<AssetReport>;
|
|
96
136
|
replaceImage(oldUrl: string, newUrl: string): Promise<number>;
|
|
137
|
+
export(options: ExportOptions): Promise<ExportResult>;
|
|
138
|
+
preflightExport(options: ExportOptions): Promise<ExportPreflight>;
|
|
97
139
|
flushRecovery(): Promise<void>;
|
|
98
140
|
getRecoverableDocuments(): Promise<RecoveryRecord[]>;
|
|
99
141
|
getRecovery(documentId?: string): Promise<RecoveryRecord | undefined>;
|