fabricjs-document-engine 1.0.1 → 1.1.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 +277 -36
- package/README.zh-CN.md +697 -0
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +22 -10
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +22 -10
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +88 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +88 -7
- package/dist/commands/clipboard.cjs +136 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +135 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -0
- package/dist/engine/create-document-engine.cjs +85 -14
- package/dist/engine/create-document-engine.d.cts +20 -1
- package/dist/engine/create-document-engine.d.ts +20 -1
- package/dist/engine/create-document-engine.js +86 -15
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +18 -1
- package/dist/export/export-options.d.cts +23 -0
- package/dist/export/export-options.d.ts +23 -0
- package/dist/export/export-options.js +18 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +18 -3
- package/dist/export/render-export.js +18 -4
- package/dist/export/svg/embed-assets.cjs +249 -0
- package/dist/export/svg/embed-assets.js +249 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/text-on-path.cjs +252 -0
- package/dist/export/svg/text-on-path.js +249 -0
- package/dist/fabric/fabric-adapter.cjs +46 -5
- package/dist/fabric/fabric-adapter.js +46 -5
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -1
- package/dist/pdf/export-pdf.cjs +440 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +440 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf/text-decorations.cjs +108 -0
- package/dist/pdf/text-decorations.js +107 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/util/concurrency.cjs +28 -0
- package/dist/util/concurrency.js +27 -0
- package/package.json +55 -14
package/README.md
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
# fabricjs-document-engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Save, load, undo and redo for an existing [Fabric.js](https://fabricjs.com) canvas. Your objects keep their ids, and a slow save never overwrites newer work.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/fabricjs-document-engine)
|
|
6
|
+
[](https://bundlephobia.com/package/fabricjs-document-engine)
|
|
7
|
+
[](https://www.npmjs.com/package/fabricjs-document-engine)
|
|
8
|
+
[](https://github.com/re-sohail/fabricjs-document-engine/blob/main/LICENSE)
|
|
6
9
|
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
10
|
+
[Documentation](https://fabricjs-document-engine.jscrate.dev) · [Live demos](https://fabricjs-document-engine.jscrate.dev/#examples-heading) · [Editor tutorial](https://fabricjs-document-engine.jscrate.dev/docs/overview/tutorial) · [API](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/api.md) · [中文](https://github.com/re-sohail/fabricjs-document-engine/blob/main/README.zh-CN.md)
|
|
11
|
+
|
|
12
|
+
You keep your own canvas, toolbar and UI. This package sits beside them and turns what is on the canvas into a document you can save, reopen and keep editing. It works with Fabric 6 and 7, in React, Next.js, Vue, Svelte or plain JavaScript.
|
|
13
|
+
|
|
14
|
+
It also covers the jobs around the document: copy and paste, layer order, importing SVG files, and exporting images, SVG and PDF that look like the canvas.
|
|
15
|
+
|
|
16
|
+
## Why this exists
|
|
17
|
+
|
|
18
|
+
Anyone who has shipped a fabricjs editor has hit the same walls. Fabric.js serialization and drawing work well, but a document needs more than that:
|
|
19
|
+
|
|
20
|
+
- **There is no undo.** Fabric has no built-in history, so every team writes its own and loses object references on the way ([fabric.js#10011](https://github.com/fabricjs/fabric.js/issues/10011)).
|
|
21
|
+
- **Custom properties vanish.** `toJSON` and `loadFromJSON` drop fields Fabric does not know about, unless you list them on every call ([fabric.js#10887](https://github.com/fabricjs/fabric.js/issues/10887)).
|
|
22
|
+
- **Objects cannot be found again.** After loading, you cannot get an object by id, because Fabric gives objects no stable id. Group children have none at all.
|
|
23
|
+
- **Saves race each other.** An older request can finish last and overwrite newer edits, or a second tab can save over the first.
|
|
24
|
+
- **Export stops at the canvas.** There is no PDF export ([fabric.js#5906](https://github.com/fabricjs/fabric.js/issues/5906)), curved text exports to SVG in the wrong place ([fabric.js#6958](https://github.com/fabricjs/fabric.js/issues/6958)), and an exported SVG shows empty boxes once its image links stop working ([fabric.js#1980](https://github.com/fabricjs/fabric.js/issues/1980)).
|
|
25
|
+
|
|
26
|
+
This package handles those problems and the ones behind them: missing images, fonts that fail to load, crashed tabs, old file formats, large documents that freeze the page, and SVG files that land in the wrong place when imported.
|
|
21
27
|
|
|
22
28
|
## Install
|
|
23
29
|
|
|
30
|
+
Install from npm, together with Fabric:
|
|
31
|
+
|
|
24
32
|
```bash
|
|
25
33
|
npm install fabricjs-document-engine fabric
|
|
26
34
|
```
|
|
27
35
|
|
|
36
|
+
The package is written in TypeScript and ships its own types. It has no runtime dependencies. `fabric` is a peer dependency, React is needed only for the hooks, and `jspdf` and `svg2pdf.js` only for PDF export.
|
|
37
|
+
|
|
28
38
|
## Quick start
|
|
29
39
|
|
|
40
|
+
Save a Fabric.js canvas as JSON, then load it back:
|
|
41
|
+
|
|
30
42
|
```ts
|
|
31
43
|
import { Canvas, Rect } from 'fabric';
|
|
32
44
|
import { createDocumentEngine } from 'fabricjs-document-engine';
|
|
@@ -42,7 +54,33 @@ localStorage.setItem(document.id, JSON.stringify(document));
|
|
|
42
54
|
await engine.loadDocument(JSON.parse(localStorage.getItem(document.id)!));
|
|
43
55
|
```
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
That is the whole setup for a first test. localStorage is fine here; in a real app you pass a storage adapter and let autosave do the work, as shown below. The [quick start guide](https://fabricjs-document-engine.jscrate.dev/docs/overview/quick-start) walks through it step by step.
|
|
58
|
+
|
|
59
|
+
## What it handles
|
|
60
|
+
|
|
61
|
+
- **Stable object ids.** Every object, including children of groups, gets an id that survives moving, styling, grouping, saving and reopening. `engine.getObjectById(id)` finds it again.
|
|
62
|
+
- **A versioned document format.** It records the schema version, canvas size, background, object order and your own metadata.
|
|
63
|
+
- **Safe loading.** Documents are validated first. Unknown object types are refused before the canvas is touched. A missing image fails the load instead of silently disappearing. When loads overlap, the newest one wins.
|
|
64
|
+
- **Custom objects.** Register your own Fabric classes and the extra properties they need to keep.
|
|
65
|
+
- **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.
|
|
66
|
+
- **Your storage.** Plug in any backend with two functions, or use the built-in memory and localStorage adapters. No hosted service is needed.
|
|
67
|
+
- **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 and why (not found, server error, CORS, timeout or a broken file), can offer replacements, and tab-only images are uploaded when you save.
|
|
68
|
+
- **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.
|
|
69
|
+
- **Export.** PNG, JPEG, WebP, SVG, PDF 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.
|
|
70
|
+
- **SVG that matches the canvas.** Text on a path keeps its place, its background and its underline in the SVG. Images and fonts can be embedded, so the file opens in Illustrator or on another computer.
|
|
71
|
+
- **PDF with real text.** Pages in A4, Letter or the canvas size, with margins, several pages per file, and text that stays selectable. Only shadows, blend modes and similar effects become pictures.
|
|
72
|
+
- **SVG import.** SVG files open where their viewBox puts them, even with elements outside it, and scripts and outside links are removed first.
|
|
73
|
+
- **Rendering in bulk.** Thumbnails or exports for hundreds of saved documents in one tab, on a few reused canvases that are freed after each document.
|
|
74
|
+
- **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.
|
|
75
|
+
- **Large documents.** Objects are created in chunks so the page stays responsive. Loads report progress and can be cancelled with an `AbortSignal`, and a cancelled load leaves the canvas as it was.
|
|
76
|
+
- **Copy, paste and layers.** A clipboard that keeps group transforms and custom properties and gives every pasted object a new id, plus bring-to-front and send-to-back commands that keep a pinned background in place. Each is one undo step.
|
|
77
|
+
- **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.
|
|
78
|
+
- **React ready, framework free.** Hooks for React, and a small state store for any other framework.
|
|
79
|
+
- **Hardened.** Imported documents and SVG files are cleaned and size-limited, undo history has a memory budget, and every feature is tested on Fabric 6 and 7 in Chromium, Firefox and WebKit. Exports are checked pixel by pixel against the canvas.
|
|
80
|
+
|
|
81
|
+
## React, Next.js, Vue and Svelte
|
|
82
|
+
|
|
83
|
+
A Fabric.js React example with a toolbar that shows undo and save state:
|
|
46
84
|
|
|
47
85
|
```tsx
|
|
48
86
|
import { useDocumentEngine, useDocumentState, DocumentEngineProvider, useEngine } from 'fabricjs-document-engine/react';
|
|
@@ -74,11 +112,13 @@ function YourToolbar() {
|
|
|
74
112
|
- `useDocumentState(engine)` returns `{ documentId, isLoading, loadError, saveStatus, isDirty, isSaving, revision, lastSavedAt, saveError, canUndo, canRedo, undoLabel, redoLabel, assetWarnings }` and re-renders when any of them change.
|
|
75
113
|
- `useDocumentEvent(engine, 'save:error', handler)` subscribes to any event with the latest handler.
|
|
76
114
|
- `DocumentEngineProvider` and `useEngine()` pass the engine to deeply nested toolbars.
|
|
77
|
-
- The React entry is marked `'use client'
|
|
115
|
+
- The React entry is marked `'use client'`. For Fabric.js in Next.js, render the editor in a client component loaded with a dynamic import that skips the server, because Fabric needs `window`. React is an optional peer dependency, and the core never imports it.
|
|
116
|
+
|
|
117
|
+
For other frameworks, `createDocumentStateStore(engine)` gives the same state as `{ getSnapshot, subscribe }`. It fits Svelte stores, Vue's `shallowRef` and similar tools.
|
|
78
118
|
|
|
79
|
-
|
|
119
|
+
Framework guides: [React](https://fabricjs-document-engine.jscrate.dev/docs/frameworks/react) · [Next.js](https://fabricjs-document-engine.jscrate.dev/docs/frameworks/next-js) · [Fabric.js with Vue 3](https://fabricjs-document-engine.jscrate.dev/docs/frameworks/vue) (keep the canvas out of deep reactivity with `toRaw`) · [Fabric.js with Svelte](https://fabricjs-document-engine.jscrate.dev/docs/frameworks/svelte) · [Plain JavaScript](https://fabricjs-document-engine.jscrate.dev/docs/frameworks/vanilla-js)
|
|
80
120
|
|
|
81
|
-
##
|
|
121
|
+
## Save and load from a database or API
|
|
82
122
|
|
|
83
123
|
The built-in adapters are the quickest way to start:
|
|
84
124
|
|
|
@@ -133,7 +173,29 @@ const storage: DocumentStorage = {
|
|
|
133
173
|
- Throw an error with `retryable: false` for failures that retrying cannot fix. Every other error is retried.
|
|
134
174
|
- Pass `signal` to `fetch`. The engine aborts it when another document is opened.
|
|
135
175
|
|
|
136
|
-
##
|
|
176
|
+
## Load progress and cancelling
|
|
177
|
+
|
|
178
|
+
Large Fabric.js documents with thousands of objects can take a while to open. The engine creates objects 100 at a time and gives the page a turn between chunks, so the page stays responsive. Show progress, and let the user cancel:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
cancelButton.onclick = () => controller.abort();
|
|
183
|
+
|
|
184
|
+
await engine.load('big-floor-plan', {
|
|
185
|
+
signal: controller.signal,
|
|
186
|
+
onProgress: ({ stage, done, total }) => {
|
|
187
|
+
// stage is 'prepare', 'images', 'objects' or 'done'
|
|
188
|
+
progressBar.value = total > 0 ? done / total : 0;
|
|
189
|
+
progressLabel.textContent = stage;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Cancelling rejects with `LOAD_ABORTED` and leaves the canvas showing what it showed before. A failed load does the same: every object is created before the canvas is cleared. The `load:progress` event carries the same progress for code that is not the caller.
|
|
195
|
+
|
|
196
|
+
## Autosave and save conflicts
|
|
197
|
+
|
|
198
|
+
Fabric.js autosave is one option. The rest of this section is about what happens when saves go wrong, because that is where editors lose work.
|
|
137
199
|
|
|
138
200
|
```ts
|
|
139
201
|
const engine = createDocumentEngine({
|
|
@@ -166,7 +228,9 @@ import { bindUnsavedChangesWarning } from 'fabricjs-document-engine';
|
|
|
166
228
|
const unbind = bindUnsavedChangesWarning(engine);
|
|
167
229
|
```
|
|
168
230
|
|
|
169
|
-
|
|
231
|
+
Full guides: [autosave](https://fabricjs-document-engine.jscrate.dev/docs/guides/autosave) and [save conflicts](https://fabricjs-document-engine.jscrate.dev/docs/guides/save-conflicts).
|
|
232
|
+
|
|
233
|
+
## Images, fonts and CORS
|
|
170
234
|
|
|
171
235
|
Every saved document carries an `assets` manifest that lists each image URL and font variant, together with the ids of the objects that use them. Images embedded as `data:` URLs are left out of the manifest because they need no fetching.
|
|
172
236
|
|
|
@@ -193,10 +257,27 @@ engine.on('assets:warning', ({ warnings }) => warnings.forEach((warning) => cons
|
|
|
193
257
|
|
|
194
258
|
1. `resolveUrl` can rewrite each stored URL, for example to sign it or to map asset ids to a CDN.
|
|
195
259
|
2. `loadFont` runs for each font variant. Then the engine checks that the font really renders, rather than silently falling back to a default.
|
|
196
|
-
3.
|
|
197
|
-
4. If images are still missing, loading fails with `MISSING_ASSETS`, and `error.missingAssets` lists each `{ url, objectIds }`. The canvas is not touched.
|
|
260
|
+
3. Images load six at a time (`maxConcurrentImages`), and each one has 30 seconds (`imageTimeout`). If any are missing, `replaceMissingImage` can supply a replacement URL for each one. Return `null` to leave it missing.
|
|
261
|
+
4. If images are still missing, loading fails with `MISSING_ASSETS`, and `error.missingAssets` lists each `{ url, objectIds, failure }`. The canvas is not touched.
|
|
262
|
+
|
|
263
|
+
`failure.reason` tells you why an image failed, so you can show the right message:
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
try {
|
|
267
|
+
await engine.load('poster-42');
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (isDocumentEngineError(error) && error.code === 'MISSING_ASSETS') {
|
|
270
|
+
for (const { url, objectIds, failure } of error.missingAssets) {
|
|
271
|
+
// NOT_FOUND, HTTP_ERROR, CORS, NETWORK, TIMEOUT, DECODE or ABORTED
|
|
272
|
+
console.warn(failure?.reason, failure?.status, url, objectIds);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Browsers hide some details on purpose. When an image from another site fails without CORS headers, the reason is `CORS` if the image asked for `crossOrigin`, and `NETWORK` otherwise.
|
|
198
279
|
|
|
199
|
-
|
|
280
|
+
Fabric.js fonts that are not available produce a `FONT_UNAVAILABLE` warning, and the text uses a fallback font. Set `requireFonts: true` to fail with `MISSING_FONTS` instead. Warnings are also delivered with `load:success` as `{ document, warnings }`.
|
|
200
281
|
|
|
201
282
|
### When a document is saved
|
|
202
283
|
|
|
@@ -204,7 +285,7 @@ Images that exist only in this tab (`blob:` URLs) and embedded `data:` images ar
|
|
|
204
285
|
|
|
205
286
|
### Cross-origin images
|
|
206
287
|
|
|
207
|
-
An image from another site without `crossOrigin: 'anonymous'` taints the canvas, and exporting it will fail. The engine warns with `IMAGE_CROSS_ORIGIN` so you can fix it before the user tries to export.
|
|
288
|
+
A Fabric.js CORS image problem is the most common reason an export fails. An image from another site without `crossOrigin: 'anonymous'` taints the canvas, and exporting it will fail. The engine warns with `IMAGE_CROSS_ORIGIN` so you can fix it before the user tries to export.
|
|
208
289
|
|
|
209
290
|
### Checking and replacing at any time
|
|
210
291
|
|
|
@@ -219,7 +300,9 @@ await engine.replaceImage('/old-logo.png', '/new-logo.png');
|
|
|
219
300
|
|
|
220
301
|
`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.
|
|
221
302
|
|
|
222
|
-
## Export
|
|
303
|
+
## Export an image, SVG or JSON
|
|
304
|
+
|
|
305
|
+
Fabric.js export to PNG, JPEG, WebP or SVG goes through one call. The result is a `Blob` you can download or upload.
|
|
223
306
|
|
|
224
307
|
```ts
|
|
225
308
|
import { downloadExport } from 'fabricjs-document-engine';
|
|
@@ -239,11 +322,35 @@ downloadExport(result, 'poster.png');
|
|
|
239
322
|
| `padding` | Extra space around `content` or `selection` | `0` |
|
|
240
323
|
| `background` | `'keep'`, `'transparent'` or any CSS color | `'keep'` |
|
|
241
324
|
| `signal` | An `AbortSignal` to cancel | |
|
|
325
|
+
| `svg` | `{ textOnPath?, embedImages?, maxEmbeddedImageBytes?, embedFonts? }` for SVG exports | |
|
|
242
326
|
|
|
243
327
|
- The current zoom and pan do not matter. Exports always use document coordinates, and the view is restored afterwards.
|
|
244
328
|
- A JPEG has no transparency, so an empty or transparent background becomes white instead of black.
|
|
245
329
|
- The export never changes the canvas, the history or the unsaved state.
|
|
246
330
|
- A JSON export is the same portable document a save produces, including uploaded images when `assets.upload` is set.
|
|
331
|
+
- For PDF, see [Export a PDF](#export-a-pdf) below.
|
|
332
|
+
|
|
333
|
+
### SVG that opens anywhere
|
|
334
|
+
|
|
335
|
+
A Fabric.js SVG export links to images by URL. Open the file in Illustrator, on another computer or after a signed URL expires, and the images are empty boxes. Embed them, and the fonts too:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const result = await engine.export({
|
|
339
|
+
format: 'svg',
|
|
340
|
+
svg: {
|
|
341
|
+
embedImages: true, // or 'require' to block the export when one cannot be embedded
|
|
342
|
+
embedFonts: { 'Brand Sans': '/fonts/brand-sans.woff2' },
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
An image from another site that does not allow CORS cannot be read by the page. With `embedImages: true` it stays a link and you get an `IMAGE_NOT_EMBEDDED` warning that names the objects. Fonts can be a URL or the file's bytes, and fonts set on single letters are embedded too.
|
|
348
|
+
|
|
349
|
+
### Curved text in SVG
|
|
350
|
+
|
|
351
|
+
Fabric.js text on a path (`text.path`) does not export to SVG the way it looks on the canvas: `pathAlign` is ignored, raised letters move the wrong way, and text backgrounds and underlines are drawn straight. When the text contains a space, Fabric 6 and 7 even write invalid XML that browsers and Illustrator refuse to open.
|
|
352
|
+
|
|
353
|
+
SVG exports write each letter where the canvas draws it, with its background and underline in the same place, so curved text looks the same in the SVG. Every SVG reader understands the output, and it stays editable text. Pass `svg: { textOnPath: 'fabric' }` to keep Fabric's own output instead.
|
|
247
354
|
|
|
248
355
|
### Preflight and errors
|
|
249
356
|
|
|
@@ -260,7 +367,69 @@ const check = await engine.preflightExport({ format: 'png' });
|
|
|
260
367
|
if (!check.ok) showProblems(check.problems);
|
|
261
368
|
```
|
|
262
369
|
|
|
263
|
-
##
|
|
370
|
+
## Export a PDF
|
|
371
|
+
|
|
372
|
+
Fabric.js has no PDF export, and the usual recipe, a screenshot pasted into jsPDF, gives blurry pages with no selectable text. `exportPdf` draws the canvas as real PDF vectors and text instead. Install the two optional libraries first:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npm install jspdf svg2pdf.js
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
import { downloadExport } from 'fabricjs-document-engine';
|
|
380
|
+
import { exportPdf } from 'fabricjs-document-engine/pdf';
|
|
381
|
+
|
|
382
|
+
const { blob, warnings } = await exportPdf(engine, {
|
|
383
|
+
page: 'A4', // 'A3', 'A5', 'Letter', 'Legal', 'Tabloid', 'canvas' or [width, height] in points
|
|
384
|
+
margin: 36, // half an inch
|
|
385
|
+
fonts: [
|
|
386
|
+
{ family: 'Inter', source: '/fonts/Inter-Regular.ttf' },
|
|
387
|
+
{ family: 'Inter', source: '/fonts/Inter-Bold.ttf', weight: 'bold' },
|
|
388
|
+
],
|
|
389
|
+
metadata: { title: 'Spring poster' },
|
|
390
|
+
});
|
|
391
|
+
downloadExport({ blob, format: 'pdf' }, 'poster.pdf');
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
- **Text stays text.** Text in the fonts you pass, and in Arial, Helvetica, Times and Courier, can be selected and searched in the PDF. Fonts must be TrueType (.ttf) files, which most font sites offer next to the web formats.
|
|
395
|
+
- **Hybrid by default.** Everything is drawn as vectors, except what PDF vectors cannot show: shadows, blend modes, gradient outlines, outlines that keep their width while scaled, and text in a font with no file. Each of those is drawn as a 300 dpi picture of just that object, in its place, and `warnings` names it. Use `mode: 'vector'` for vectors only, or `mode: 'raster'` for one picture per page.
|
|
396
|
+
- **Curved text and underlines** come out as on the canvas, using the same fixes as the SVG export.
|
|
397
|
+
- **Several pages.** Pass an array of engines, Fabric canvases or saved documents to get one page each. Saved documents are drawn on an off-screen canvas that is freed after its page.
|
|
398
|
+
|
|
399
|
+
## Render many documents
|
|
400
|
+
|
|
401
|
+
Making thumbnails or PDFs for hundreds of saved designs in one browser tab runs out of memory with a canvas per design, because browsers free canvas memory late. `renderDocuments` reuses a few off-screen canvases and frees every object and cache canvas after each document:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { renderDocuments } from 'fabricjs-document-engine';
|
|
405
|
+
|
|
406
|
+
for await (const { documentId, result, error } of renderDocuments(savedDocuments, { format: 'png', scale: 0.5, concurrency: 2 })) {
|
|
407
|
+
if (result) await uploadThumbnail(documentId, result.blob);
|
|
408
|
+
else console.warn(documentId, error?.message);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Results arrive as each document finishes, so they can be uploaded one by one. A broken document reports its own `error` and the rest still render. `documents` can be an async iterable, such as pages of a database query, and a `signal` stops the batch.
|
|
413
|
+
|
|
414
|
+
## Import an SVG file
|
|
415
|
+
|
|
416
|
+
Fabric's usual SVG import, `loadSVGFromString` with `util.groupSVGElements`, sizes the group to what is drawn. An element outside the SVG's viewBox, or a hidden one, then moves and resizes the whole artwork. `importSvg` keeps the SVG's own frame:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
const { objects, viewport, warnings } = await engine.importSvg(svgText, {
|
|
420
|
+
left: 40,
|
|
421
|
+
top: 40,
|
|
422
|
+
fit: { width: 300, height: 200 }, // optional: scale into a box
|
|
423
|
+
offscreen: 'clip', // or 'keep' (default) or 'drop'
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
- Elements land where the SVG puts them, after `viewBox` and `preserveAspectRatio`.
|
|
428
|
+
- The result is one group with a fixed layout the size of the viewport, or separate objects with `as: 'objects'`. Either way it is one undo step, and every object gets an id.
|
|
429
|
+
- Scripts, event handlers, `foreignObject`, links to other files and image addresses that `limits.isAllowedUrl` refuses are removed, and `warnings` says what was removed.
|
|
430
|
+
- Size limits apply as for documents, so a huge or deeply nested SVG is refused with `UNSAFE_DOCUMENT`.
|
|
431
|
+
|
|
432
|
+
## Version history
|
|
264
433
|
|
|
265
434
|
```ts
|
|
266
435
|
const version = await engine.createVersion('Sent to client');
|
|
@@ -275,7 +444,7 @@ await engine.deleteVersion(version.id);
|
|
|
275
444
|
- **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.
|
|
276
445
|
- Undo and redo cover recent edits in this session. Versions preserve chosen states for later.
|
|
277
446
|
|
|
278
|
-
##
|
|
447
|
+
## Load from JSON and migrate from Fabric 5
|
|
279
448
|
|
|
280
449
|
Plain Fabric JSON, such as the output of `canvas.toJSON()` from Fabric 5, 6 or 7, opens directly:
|
|
281
450
|
|
|
@@ -285,9 +454,11 @@ await engine.importFabricJson(savedJsonText, { id: 'plan-42', metadata: { source
|
|
|
285
454
|
|
|
286
455
|
`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.
|
|
287
456
|
|
|
288
|
-
Every document records its `schemaVersion
|
|
457
|
+
Every document records its `schemaVersion`, which makes Fabric.js migration a one-way, step-by-step upgrade. When the package format changes, older documents are upgraded 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.
|
|
458
|
+
|
|
459
|
+
## Recover unsaved work
|
|
289
460
|
|
|
290
|
-
|
|
461
|
+
Fabric.js IndexedDB recovery runs in the background while the user edits:
|
|
291
462
|
|
|
292
463
|
```ts
|
|
293
464
|
import { createIndexedDbRecovery } from 'fabricjs-document-engine/recovery';
|
|
@@ -315,7 +486,9 @@ if (latest && confirm(`Restore unsaved work from ${new Date(latest.savedAt).toLo
|
|
|
315
486
|
- `engine.flushRecovery()` writes a copy right now. `engine.getRecovery(id?)` reads one.
|
|
316
487
|
- `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.
|
|
317
488
|
|
|
318
|
-
## Custom objects
|
|
489
|
+
## Custom objects and properties
|
|
490
|
+
|
|
491
|
+
A Fabric.js custom object keeps its extra fields only if something lists them at save time. Register the class once and its properties survive every save, load, undo and redo:
|
|
319
492
|
|
|
320
493
|
```ts
|
|
321
494
|
import { Rect } from 'fabric';
|
|
@@ -333,7 +506,7 @@ const engine = createDocumentEngine({
|
|
|
333
506
|
|
|
334
507
|
If a document contains a type that has not been registered, loading fails with `UNKNOWN_OBJECT_TYPE` and lists the missing types. Your object is never turned into something else.
|
|
335
508
|
|
|
336
|
-
##
|
|
509
|
+
## Fabric.js undo and redo
|
|
337
510
|
|
|
338
511
|
History is on by default. The engine records these automatically:
|
|
339
512
|
|
|
@@ -358,7 +531,7 @@ await engine.redo();
|
|
|
358
531
|
```
|
|
359
532
|
|
|
360
533
|
- Transactions can be nested, and the outermost label is used. They can also be async: `await engine.transaction('Import', async () => { ... })`.
|
|
361
|
-
-
|
|
534
|
+
- To group objects or ungroup them, do the remove and the add inside one transaction, and they take one undo step. Grouping is an ordinary change to the object list.
|
|
362
535
|
- Undo and redo rebuild the changed objects from their saved state, so they come back as new instances with the same ids. Look them up again with `engine.getObjectById(id)` rather than keeping old references.
|
|
363
536
|
- Keep the last 50 steps with `createDocumentEngine({ canvas, history: { limit: 50 } })`. The default is 100.
|
|
364
537
|
|
|
@@ -381,6 +554,45 @@ engine.on('history:change', ({ canUndo, canRedo, undoLabel, redoLabel }) => {
|
|
|
381
554
|
});
|
|
382
555
|
```
|
|
383
556
|
|
|
557
|
+
The [undo and redo guide](https://fabricjs-document-engine.jscrate.dev/docs/guides/undo-redo) has a live demo and covers text editing in more detail.
|
|
558
|
+
|
|
559
|
+
## Copy, paste and layer order
|
|
560
|
+
|
|
561
|
+
Copy and paste in Fabric.js usually means `object.clone()`, which copies the id and can place objects from a moved selection or a group in the wrong spot. The clipboard copies objects where they really are on the canvas, keeps custom properties, and gives every pasted object, group child and clip path a new id:
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
import { createClipboard } from 'fabricjs-document-engine';
|
|
565
|
+
|
|
566
|
+
const clipboard = createClipboard(engine);
|
|
567
|
+
|
|
568
|
+
clipboard.copy(); // the selection, or pass objects
|
|
569
|
+
await clipboard.paste(); // one undo step, 10 units further each time
|
|
570
|
+
clipboard.cut(); // one undo step; the next paste lands in place
|
|
571
|
+
await clipboard.paste({ target: otherEngine });
|
|
572
|
+
|
|
573
|
+
// Share between tabs through the system clipboard
|
|
574
|
+
await navigator.clipboard.writeText(JSON.stringify(clipboard.read()));
|
|
575
|
+
clipboard.write(JSON.parse(await navigator.clipboard.readText()));
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Content passed to `write` is checked like a loaded document, so pasted JSON cannot carry unsafe image addresses.
|
|
579
|
+
|
|
580
|
+
Layer commands move objects, or the selection, and record one undo step. Several selected objects keep their order. A pinned object, such as a background, never moves:
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
import { bringForward, bringToFront, getLayers, sendBackward, sendToBack } from 'fabricjs-document-engine';
|
|
584
|
+
|
|
585
|
+
const keepBackground = { pinned: (object) => object.name === 'background' };
|
|
586
|
+
|
|
587
|
+
bringToFront(engine);
|
|
588
|
+
sendToBack(engine, undefined, keepBackground); // stops just above the background
|
|
589
|
+
bringForward(engine, [logo]);
|
|
590
|
+
|
|
591
|
+
getLayers(engine); // [{ id, type, name, index, visible, locked }], top first
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
The engine saves each object's `name`, so a layers panel keeps its labels. In React, `useLayers(engine)` from `fabricjs-document-engine/react` returns the same list and updates on every change.
|
|
595
|
+
|
|
384
596
|
## Document format
|
|
385
597
|
|
|
386
598
|
```ts
|
|
@@ -409,6 +621,25 @@ The format is described by a JSON Schema that ships with the package:
|
|
|
409
621
|
import schema from 'fabricjs-document-engine/schema/document-v1.json';
|
|
410
622
|
```
|
|
411
623
|
|
|
624
|
+
## Where it fits
|
|
625
|
+
|
|
626
|
+
Use it when you are building a Fabric.js canvas editor: a design editor, an image editor, a floor planner, a label or certificate builder. Fabric still does the drawing, selection and serialization. This package adds the document layer on top: ids, history, saving, loading, assets, recovery, copy and paste, layer order, SVG import, and image, SVG and PDF export.
|
|
627
|
+
|
|
628
|
+
If you are comparing a canvas editor JS library or an undo redo JavaScript library, note the scope. It does not draw a toolbar, and it does not do real-time collaboration. The [comparison page](https://fabricjs-document-engine.jscrate.dev/docs/overview/comparison) sets it next to `fabric-history`, `fabricjs-react` and hand-written `toJSON`.
|
|
629
|
+
|
|
630
|
+
## Compatibility
|
|
631
|
+
|
|
632
|
+
| | Supported |
|
|
633
|
+
| --- | --- |
|
|
634
|
+
| Fabric | Fabric.js 6 and Fabric.js 7 (peer `^6.0.0 \|\| ^7.0.0`); plain JSON from Fabric 5 opens through migration |
|
|
635
|
+
| Browsers | Full suite passes in Chromium, Firefox and WebKit |
|
|
636
|
+
| React | 18 and 19, optional |
|
|
637
|
+
| Node | 18 or later, for server-side import, validation and migration. PDF export and `renderDocuments` need a browser |
|
|
638
|
+
| PDF | Optional peers `jspdf` 4 and `svg2pdf.js` 2.7 or later, only for `fabricjs-document-engine/pdf` |
|
|
639
|
+
| Modules | ESM and CommonJS, with TypeScript types |
|
|
640
|
+
|
|
641
|
+
Tested versions and performance numbers (5,000 objects, every step under 50 ms except load) are in [docs/compatibility.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility.md).
|
|
642
|
+
|
|
412
643
|
## API reference
|
|
413
644
|
|
|
414
645
|
Every function, option, event and error code is listed in [docs/api.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/api.md). Every failure is a `DocumentEngineError` with a stable `code` you can switch on, such as `SAVE_CONFLICT`, `MISSING_ASSETS` or `UNSAVED_CHANGES`. A failed load never clears or half-fills your canvas.
|
|
@@ -421,7 +652,7 @@ Version 1.0 freezes the document format and the adapter contracts:
|
|
|
421
652
|
- Public API names, options, events and error codes do not change within 1.x. New ones may be added.
|
|
422
653
|
- Storage, version and recovery adapters written for 1.0 keep working. `verifyStorageAdapter(storage)` from `fabricjs-document-engine/storage` checks that your adapter follows the save rules.
|
|
423
654
|
|
|
424
|
-
The full promise is in the [compatibility policy](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility-policy.md).
|
|
655
|
+
The full promise is in the [compatibility policy](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility-policy.md). A complete setup is in the [production guide](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/production.md).
|
|
425
656
|
|
|
426
657
|
## Imported content and limits
|
|
427
658
|
|
|
@@ -430,6 +661,8 @@ Documents often come from users, so the engine treats them as untrusted:
|
|
|
430
661
|
- Keys named `__proto__`, `constructor` or `prototype` are removed before Fabric sees them. Fabric copies every key onto the object it creates, so these keys could otherwise change an object's prototype.
|
|
431
662
|
- Image addresses are checked after `assets.resolveUrl`, before anything is fetched. `http:`, `https:`, `blob:`, relative addresses and `data:image/...` are allowed. `javascript:`, `file:` and non-image `data:` addresses are refused with `UNSAFE_DOCUMENT`.
|
|
432
663
|
- A document with more than 50,000 objects, or nested more than 100 levels deep, is refused before loading, so a hostile file cannot freeze the tab.
|
|
664
|
+
- SVG files passed to `importSvg` lose scripts, event handlers, `foreignObject` and links to other files before Fabric parses them, and the same size limits apply.
|
|
665
|
+
- JSON written into the clipboard with `clipboard.write` is checked like a document, so pasted content cannot bring in unsafe image addresses.
|
|
433
666
|
|
|
434
667
|
```ts
|
|
435
668
|
createDocumentEngine({
|
|
@@ -449,7 +682,15 @@ Accessibility guidance for your toolbar, status text and dialogs is in [docs/acc
|
|
|
449
682
|
|
|
450
683
|
## Troubleshooting
|
|
451
684
|
|
|
452
|
-
Common problems and fixes are in [docs/troubleshooting.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/troubleshooting.md).
|
|
685
|
+
Common problems and fixes are in [docs/troubleshooting.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/troubleshooting.md). Questions people ask most, such as why `loadFromJSON` loses custom properties, are answered in the [FAQ](https://fabricjs-document-engine.jscrate.dev/docs/overview/faq).
|
|
686
|
+
|
|
687
|
+
## Help and contributing
|
|
688
|
+
|
|
689
|
+
- Documentation and live Fabric.js examples: [fabricjs-document-engine.jscrate.dev](https://fabricjs-document-engine.jscrate.dev)
|
|
690
|
+
- Bugs and feature requests: [GitHub issues](https://github.com/re-sohail/fabricjs-document-engine/issues)
|
|
691
|
+
- Release notes: [CHANGELOG.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/CHANGELOG.md)
|
|
692
|
+
|
|
693
|
+
Maintained by [Sohail Khan](https://me.jscrate.dev). Pull requests are welcome. Every user-facing change needs a changeset (`npx changeset`).
|
|
453
694
|
|
|
454
695
|
## License
|
|
455
696
|
|