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.
Files changed (89) hide show
  1. package/README.md +277 -36
  2. package/README.zh-CN.md +697 -0
  3. package/dist/assets/asset-manifest.d.cts +3 -0
  4. package/dist/assets/asset-manifest.d.ts +3 -0
  5. package/dist/assets/asset-pipeline.cjs +22 -10
  6. package/dist/assets/asset-pipeline.d.cts +5 -1
  7. package/dist/assets/asset-pipeline.d.ts +5 -1
  8. package/dist/assets/asset-pipeline.js +22 -10
  9. package/dist/assets/font-check.cjs +9 -1
  10. package/dist/assets/font-check.js +9 -1
  11. package/dist/assets/image-check.cjs +88 -6
  12. package/dist/assets/image-check.d.cts +17 -0
  13. package/dist/assets/image-check.d.ts +17 -0
  14. package/dist/assets/image-check.js +88 -7
  15. package/dist/commands/clipboard.cjs +136 -0
  16. package/dist/commands/clipboard.d.cts +47 -0
  17. package/dist/commands/clipboard.d.ts +47 -0
  18. package/dist/commands/clipboard.js +135 -0
  19. package/dist/commands/layers.cjs +97 -0
  20. package/dist/commands/layers.d.cts +39 -0
  21. package/dist/commands/layers.d.ts +39 -0
  22. package/dist/commands/layers.js +92 -0
  23. package/dist/engine/create-document-engine.cjs +85 -14
  24. package/dist/engine/create-document-engine.d.cts +20 -1
  25. package/dist/engine/create-document-engine.d.ts +20 -1
  26. package/dist/engine/create-document-engine.js +86 -15
  27. package/dist/engine/errors.d.cts +1 -1
  28. package/dist/engine/errors.d.ts +1 -1
  29. package/dist/export/batch-render.cjs +172 -0
  30. package/dist/export/batch-render.d.cts +43 -0
  31. package/dist/export/batch-render.d.ts +43 -0
  32. package/dist/export/batch-render.js +171 -0
  33. package/dist/export/export-options.cjs +18 -1
  34. package/dist/export/export-options.d.cts +23 -0
  35. package/dist/export/export-options.d.ts +23 -0
  36. package/dist/export/export-options.js +18 -2
  37. package/dist/export/preflight-export.cjs +11 -2
  38. package/dist/export/preflight-export.d.cts +1 -1
  39. package/dist/export/preflight-export.d.ts +1 -1
  40. package/dist/export/preflight-export.js +11 -2
  41. package/dist/export/render-export.cjs +18 -3
  42. package/dist/export/render-export.js +18 -4
  43. package/dist/export/svg/embed-assets.cjs +249 -0
  44. package/dist/export/svg/embed-assets.js +249 -0
  45. package/dist/export/svg/overrides.cjs +32 -0
  46. package/dist/export/svg/overrides.js +32 -0
  47. package/dist/export/svg/text-on-path.cjs +252 -0
  48. package/dist/export/svg/text-on-path.js +249 -0
  49. package/dist/fabric/fabric-adapter.cjs +46 -5
  50. package/dist/fabric/fabric-adapter.js +46 -5
  51. package/dist/fabric/object-registry.cjs +1 -1
  52. package/dist/fabric/object-registry.js +1 -1
  53. package/dist/import/svg-import.cjs +256 -0
  54. package/dist/import/svg-import.d.cts +50 -0
  55. package/dist/import/svg-import.d.ts +50 -0
  56. package/dist/import/svg-import.js +256 -0
  57. package/dist/index.cjs +12 -0
  58. package/dist/index.d.cts +9 -4
  59. package/dist/index.d.ts +9 -4
  60. package/dist/index.js +4 -1
  61. package/dist/pdf/export-pdf.cjs +440 -0
  62. package/dist/pdf/export-pdf.d.cts +73 -0
  63. package/dist/pdf/export-pdf.d.ts +73 -0
  64. package/dist/pdf/export-pdf.js +440 -0
  65. package/dist/pdf/fonts.cjs +115 -0
  66. package/dist/pdf/fonts.d.cts +12 -0
  67. package/dist/pdf/fonts.d.ts +12 -0
  68. package/dist/pdf/fonts.js +112 -0
  69. package/dist/pdf/page-layout.cjs +79 -0
  70. package/dist/pdf/page-layout.d.cts +51 -0
  71. package/dist/pdf/page-layout.d.ts +51 -0
  72. package/dist/pdf/page-layout.js +76 -0
  73. package/dist/pdf/text-decorations.cjs +108 -0
  74. package/dist/pdf/text-decorations.js +107 -0
  75. package/dist/pdf.cjs +7 -0
  76. package/dist/pdf.d.cts +4 -0
  77. package/dist/pdf.d.ts +4 -0
  78. package/dist/pdf.js +3 -0
  79. package/dist/react/use-layers.cjs +53 -0
  80. package/dist/react/use-layers.d.cts +10 -0
  81. package/dist/react/use-layers.d.ts +10 -0
  82. package/dist/react/use-layers.js +53 -0
  83. package/dist/react.cjs +2 -0
  84. package/dist/react.d.cts +3 -1
  85. package/dist/react.d.ts +3 -1
  86. package/dist/react.js +2 -1
  87. package/dist/util/concurrency.cjs +28 -0
  88. package/dist/util/concurrency.js +27 -0
  89. package/package.json +55 -14
package/README.md CHANGED
@@ -1,32 +1,44 @@
1
1
  # fabricjs-document-engine
2
2
 
3
- Turn an existing [Fabric.js](https://fabricjs.com) canvas into a dependable editable document.
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
- Fabric already draws objects, handles interaction and serializes to JSON. This package coordinates those pieces into a document workflow you can trust. You keep your own canvas, toolbar and UI.
5
+ [![npm version](https://img.shields.io/npm/v/fabricjs-document-engine.svg)](https://www.npmjs.com/package/fabricjs-document-engine)
6
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/fabricjs-document-engine)](https://bundlephobia.com/package/fabricjs-document-engine)
7
+ [![types](https://img.shields.io/npm/types/fabricjs-document-engine.svg)](https://www.npmjs.com/package/fabricjs-document-engine)
8
+ [![license](https://img.shields.io/npm/l/fabricjs-document-engine.svg)](https://github.com/re-sohail/fabricjs-document-engine/blob/main/LICENSE)
6
9
 
7
- - **Stable object ids.** Every object, including children of groups, gets an id that survives moving, styling, grouping, saving and reopening.
8
- - **A versioned document format.** It records the schema version, canvas size, background, object order and your own metadata.
9
- - **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.
10
- - **Custom objects.** Register your own Fabric classes and the extra properties they need to keep.
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
- - **Your storage.** Plug in any backend with two functions, or use the built-in memory and localStorage adapters. No hosted service is needed.
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.
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.
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.
18
- - **React ready, framework free.** Hooks for React, and a small state store for any other framework. Your toolbar and UI stay yours.
19
- - **Hardened.** Imported documents are cleaned and size-limited, undo history has a memory budget, and every feature is tested in Chromium, Firefox and WebKit. See the [compatibility and performance results](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility.md).
20
- - **Fabric 6 and 7.** Every release is tested against both.
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
- ## React
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'` for Next.js. React is an optional peer dependency, so the core never imports it.
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
- For other frameworks, `createDocumentStateStore(engine)` gives the same state as `{ getSnapshot, subscribe }`, which fits Svelte stores, Vue's `shallowRef` and similar tools.
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
- ## Saving and loading through storage
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
- ## Safe saving
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
- ## Assets and fonts
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. Every image loads in parallel. If any are missing, `replaceMissingImage` can supply a replacement URL for each one. Return `null` to leave it missing.
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
- 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 }`.
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
- ## Versions
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
- ## Migration and importing Fabric JSON
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`. 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.
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
- ## Recovery
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
- ## Undo and redo
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
- - Grouping and ungrouping are ordinary changes to the object list. Do the remove and the add inside one transaction and they take one undo step.
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). Tested browsers, Fabric versions and performance results are in [docs/compatibility.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility.md), and a complete setup is in the [production guide](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/production.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