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
@@ -0,0 +1,697 @@
1
+ # fabricjs-document-engine
2
+
3
+ 为现有的 [Fabric.js](https://fabricjs.com) 画布提供保存、加载、撤销和重做。对象的 id 始终不变,慢的旧保存也不会覆盖新的改动。
4
+
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)
9
+
10
+ [中文文档](https://fabricjs-document-engine.jscrate.dev/zh) · [在线示例](https://fabricjs-document-engine.jscrate.dev/zh#examples-heading) · [fabric.js 使用教程](https://fabricjs-document-engine.jscrate.dev/zh/docs/overview/tutorial) · [API](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/api.md) · [English](https://github.com/re-sohail/fabricjs-document-engine/blob/main/README.md)
11
+
12
+ 画布、工具栏和界面都由你自己掌控。这个包在旁边工作,把画布上的内容变成一份可以保存、重新打开、继续编辑的文档。它支持 Fabric 6 和 7,可以用在 React、Next.js、Vue、Svelte 或原生 JavaScript 中。
13
+
14
+ 它也负责文档周边的工作:复制和粘贴、图层顺序、导入 SVG 文件,以及导出和画布看起来一样的图片、SVG 和 PDF。
15
+
16
+ ## 为什么需要它
17
+
18
+ 做过 fabricjs 编辑器的人,基本都踩过同样的坑。Fabric.js 的序列化和绘制都做得很好,但一份文档需要的不止这些:
19
+
20
+ - **没有撤销功能。** Fabric 没有内置历史记录,每个团队都得自己写撤销和重做(上一步/下一步),而且往往在这个过程中丢掉对象引用([fabric.js#10011](https://github.com/fabricjs/fabric.js/issues/10011))。
21
+ - **自定义属性会丢失。** `toJSON` 和 `loadFromJSON` 会丢掉 Fabric 不认识的字段,除非你每次调用都把它们列出来([fabric.js#10887](https://github.com/fabricjs/fabric.js/issues/10887))。
22
+ - **加载后找不到对象。** Fabric 不给对象分配稳定的 id,所以加载之后没法按 id 获取对象。编组里的子对象更是完全没有 id。
23
+ - **保存互相冲突。** 旧的请求可能最后才返回,覆盖掉新的改动;另一个标签页也可能覆盖这一个的保存。
24
+ - **导出能力有限。** 没有 PDF 导出([fabric.js#5906](https://github.com/fabricjs/fabric.js/issues/5906)),曲线文字导出成 SVG 后位置不对([fabric.js#6958](https://github.com/fabricjs/fabric.js/issues/6958)),导出的 SVG 一旦图片链接失效就只剩空框([fabric.js#1980](https://github.com/fabricjs/fabric.js/issues/1980))。
25
+
26
+ 这个包解决这些问题,以及它们背后的问题:图片缺失、字体加载失败、标签页崩溃、旧的文件格式、大文档卡住页面,以及导入的 SVG 文件位置错乱。
27
+
28
+ ## 安装
29
+
30
+ 从 npm 安装,同时安装 Fabric:
31
+
32
+ ```bash
33
+ npm install fabricjs-document-engine fabric
34
+ ```
35
+
36
+ 这个包用 TypeScript 编写,自带类型定义。它没有运行时依赖。`fabric` 是 peer dependency,只有使用 hooks 时才需要 React,只有导出 PDF 时才需要 `jspdf` 和 `svg2pdf.js`。
37
+
38
+ ## 快速开始
39
+
40
+ 把 Fabric.js 画布保存为 JSON,再加载回来(回显):
41
+
42
+ ```ts
43
+ import { Canvas, Rect } from 'fabric';
44
+ import { createDocumentEngine } from 'fabricjs-document-engine';
45
+
46
+ const canvas = new Canvas('editor', { width: 800, height: 600 });
47
+ const engine = createDocumentEngine({ canvas });
48
+
49
+ canvas.add(new Rect({ width: 100, height: 80, fill: 'tomato' }));
50
+
51
+ const document = engine.toDocument();
52
+ localStorage.setItem(document.id, JSON.stringify(document));
53
+
54
+ await engine.loadDocument(JSON.parse(localStorage.getItem(document.id)!));
55
+ ```
56
+
57
+ 第一次试用,这些就够了。这里用 localStorage 没问题;在真实应用中,你会传入一个存储适配器,让自动保存来完成工作,下文会讲到。[快速开始指南](https://fabricjs-document-engine.jscrate.dev/zh/docs/overview/quick-start)会一步步带你完成。
58
+
59
+ ## 它能做什么
60
+
61
+ - **稳定的对象 id。** 每个对象,包括编组里的子对象,都有一个 id,移动、改样式、编组、保存和重新打开后都不会变。`engine.getObjectById(id)` 可以再次找到它。
62
+ - **带版本号的文档格式。** 它记录 schema 版本、画布尺寸、背景、对象顺序和你自己的元数据。
63
+ - **安全加载。** 文档会先经过校验。未知的对象类型会在动画布之前被拒绝。图片缺失会让加载失败,而不是悄悄消失。多次加载重叠时,以最新的一次为准。
64
+ - **自定义对象。** 注册你自己的 Fabric 类,以及它们需要保留的额外属性。
65
+ - **安全保存。** 它会跟踪未保存的改动,也可以自动保存。同一时间只运行一次保存,所以慢的旧保存永远不会覆盖新的改动。修订号检查能发现另一个标签页或设备保存了同一份文档,失败的保存会按退避策略重试。
66
+ - **你自己的存储。** 用两个函数接入任意后端,或者使用内置的内存和 localStorage 适配器。不需要任何托管服务。
67
+ - **图片和字体。** 文档会记录它需要的图片和字体。打开文档时,会先检查每张图片和每种字体。你会拿到缺失内容的准确列表和原因(找不到、服务器错误、CORS、超时或文件损坏),可以提供替换,只存在于当前标签页的图片会在保存时上传。
68
+ - **恢复。** 用户编辑时,未保存的内容会被复制到 IndexedDB;关闭或刷新标签页的那一刻还会再复制一次。崩溃或刷新之后,你可以提示用户恢复,包括只存在于旧标签页中的图片。
69
+ - **导出。** 支持 PNG、JPEG、WebP、SVG、PDF 和可编辑的 JSON。区域、缩放和背景由你选择。导出前的预检意味着导出要么成功,要么准确告诉你是哪张图片或哪种字体导致失败。
70
+ - **和画布一致的 SVG。** 沿路径排列的文字在 SVG 中保持原来的位置、背景和下划线。图片和字体可以嵌入文件,所以在 Illustrator 或另一台电脑上也能打开。
71
+ - **带真实文字的 PDF。** 支持 A4、Letter 或画布大小的页面,可以设置页边距,一个文件可以有多页,文字仍然可以选中。只有阴影、混合模式等效果会变成图片。
72
+ - **导入 SVG。** SVG 文件按照它的 viewBox 放置,即使有元素在外面也不会错位;导入前会先移除脚本和外部链接。
73
+ - **批量渲染。** 在一个标签页里为几百份已保存的文档生成缩略图或导出文件,使用几个复用的画布,每份文档完成后都会释放。
74
+ - **版本和迁移。** 保存命名版本,把任意版本恢复为新的修订,还能打开纯 Fabric JSON 或旧版本这个包保存的文档。
75
+ - **大文档。** 对象分批创建,页面保持响应。加载会报告进度,并能用 `AbortSignal` 取消;取消后画布保持原样。
76
+ - **复制、粘贴和图层。** 剪贴板会保留编组的变换和自定义属性,并给每个粘贴出的对象一个新 id;置顶、置底等图层命令可以让固定的背景保持不动。每个操作都是一步撤销。
77
+ - **撤销和重做。** 用户的一次操作就是一步撤销。事务可以把代码里的多处改动合成一个带标签的步骤,撤销和重做后 id 保持不变。
78
+ - **支持 React,不绑定框架。** 为 React 提供 hooks,为其他框架提供一个小的状态 store。
79
+ - **经过加固。** 导入的文档和 SVG 文件会被清理并限制大小,撤销历史有内存上限,每个功能都在 Fabric 6 和 7、Chromium、Firefox 和 WebKit 中测试过。导出结果会和画布逐像素比对。
80
+
81
+ ## React、Next.js、Vue 和 Svelte
82
+
83
+ 一个 Fabric.js React 示例,工具栏显示撤销和保存状态:
84
+
85
+ ```tsx
86
+ import { useDocumentEngine, useDocumentState, DocumentEngineProvider, useEngine } from 'fabricjs-document-engine/react';
87
+
88
+ function Editor({ canvas }: { canvas: Canvas | null }) {
89
+ const engine = useDocumentEngine(canvas, { storage, autosave: true });
90
+ return (
91
+ <DocumentEngineProvider engine={engine}>
92
+ <YourToolbar />
93
+ </DocumentEngineProvider>
94
+ );
95
+ }
96
+
97
+ function YourToolbar() {
98
+ const engine = useEngine();
99
+ const state = useDocumentState(engine);
100
+ if (!engine || !state) return null;
101
+ return (
102
+ <>
103
+ <button disabled={!state.canUndo} onClick={() => engine.undo()}>Undo {state.undoLabel}</button>
104
+ <button disabled={!state.isDirty} onClick={() => engine.save()}>Save</button>
105
+ <span>{state.saveStatus}</span>
106
+ </>
107
+ );
108
+ }
109
+ ```
110
+
111
+ - `useDocumentEngine(canvas, options)` 在 Fabric 画布创建后创建引擎,并在组件卸载时销毁它。在那之前返回 `null`。选项只在创建引擎时读取。
112
+ - `useDocumentState(engine)` 返回 `{ documentId, isLoading, loadError, saveStatus, isDirty, isSaving, revision, lastSavedAt, saveError, canUndo, canRedo, undoLabel, redoLabel, assetWarnings }`,其中任意一项变化都会重新渲染。
113
+ - `useDocumentEvent(engine, 'save:error', handler)` 用最新的 handler 订阅任意事件。
114
+ - `DocumentEngineProvider` 和 `useEngine()` 把引擎传给嵌套很深的工具栏。
115
+ - React 入口标记了 `'use client'`。在 Next.js 中使用 Fabric.js 时,要在客户端组件中渲染编辑器,并用跳过服务端的动态导入加载它,因为 Fabric 需要 `window`。React 是可选的 peer dependency,核心代码从不导入它。
116
+
117
+ 对于其他框架,`createDocumentStateStore(engine)` 以 `{ getSnapshot, subscribe }` 的形式提供同样的状态,适用于 Svelte store、Vue 的 `shallowRef` 等工具。
118
+
119
+ 框架指南:[React](https://fabricjs-document-engine.jscrate.dev/zh/docs/frameworks/react) · [Next.js](https://fabricjs-document-engine.jscrate.dev/zh/docs/frameworks/next-js) · [Fabric.js 与 Vue 3](https://fabricjs-document-engine.jscrate.dev/zh/docs/frameworks/vue)(用 `toRaw` 让画布避开深层响应式) · [Fabric.js 与 Svelte](https://fabricjs-document-engine.jscrate.dev/zh/docs/frameworks/svelte) · [原生 JavaScript](https://fabricjs-document-engine.jscrate.dev/zh/docs/frameworks/vanilla-js)
120
+
121
+ ## 保存到数据库或 API,并从中加载
122
+
123
+ 内置适配器是最快的起步方式:
124
+
125
+ ```ts
126
+ import { createLocalStorage, createMemoryStorage } from 'fabricjs-document-engine/storage';
127
+
128
+ const engine = createDocumentEngine({
129
+ canvas,
130
+ storage: createLocalStorage({ prefix: 'my-app:' }),
131
+ autosave: true,
132
+ });
133
+
134
+ await engine.load('project-42');
135
+ await engine.save();
136
+ ```
137
+
138
+ 两个适配器都提供 `listDocuments()` 和 `deleteDocument(id)`。要基于其他键值存储构建,使用 `createKeyValueStorage({ read, write, remove, keys }, prefix)`。
139
+
140
+ 更多适配器,包括带修订号检查和图片上传的 REST API,见 [docs/storage-examples.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/storage-examples.md)。
141
+
142
+ ### 你自己的后端
143
+
144
+ 一个存储适配器就是两个函数:
145
+
146
+ ```ts
147
+ import { createDocumentEngine, createConflictError } from 'fabricjs-document-engine';
148
+ import type { DocumentStorage } from 'fabricjs-document-engine';
149
+
150
+ const storage: DocumentStorage = {
151
+ async loadDocument(id) {
152
+ const response = await fetch(`/api/documents/${id}`);
153
+ return response.json();
154
+ },
155
+ async saveDocument(document, { expectedRevision, signal }) {
156
+ const response = await fetch(`/api/documents/${document.id}`, {
157
+ method: 'PUT',
158
+ headers: { 'If-Match': String(expectedRevision ?? '*') },
159
+ body: JSON.stringify(document),
160
+ signal,
161
+ });
162
+ if (response.status === 409) throw Object.assign(new Error('Saved elsewhere'), { code: 'SAVE_CONFLICT' });
163
+ if (response.status === 403) throw Object.assign(new Error('Not allowed'), { retryable: false });
164
+ if (!response.ok) throw new Error(`Save failed with ${response.status}`);
165
+ return { revision: document.revision };
166
+ },
167
+ };
168
+ ```
169
+
170
+ - `expectedRevision` 是这个编辑器上次保存或加载时的修订号。当存储中的文档修订号不同时,拒绝这次保存。用户选择覆盖时,它是 `null`。
171
+ - `document.revision` 是下一个修订号。如果你的后端自己分配编号,返回 `{ revision }`。
172
+ - 抛出带 `code: 'SAVE_CONFLICT'` 的错误,或使用 `createConflictError(id, expected, actual)`,来报告冲突。冲突永远不会重试。
173
+ - 对于重试也无法解决的失败,抛出带 `retryable: false` 的错误。其他错误都会重试。
174
+ - 把 `signal` 传给 `fetch`。打开另一份文档时,引擎会中止它。
175
+
176
+ ## 加载进度和取消
177
+
178
+ 包含几千个对象的大型 Fabric.js 文档打开时可能需要一些时间。引擎每次创建 100 个对象,并在每批之间把控制权交还给页面,所以页面保持响应。你可以显示进度,并让用户取消:
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
+ 取消会以 `LOAD_ABORTED` 拒绝,画布继续显示原来的内容。加载失败时也一样:所有对象都创建完成后才会清空画布。`load:progress` 事件也带有同样的进度,方便调用方以外的代码使用。
195
+
196
+ ## 自动保存和保存冲突
197
+
198
+ Fabric.js 自动保存只是一个选项。本节主要讲保存出错时会发生什么,因为编辑器正是在这里丢失内容的。
199
+
200
+ ```ts
201
+ const engine = createDocumentEngine({
202
+ canvas,
203
+ storage,
204
+ autosave: { delay: 1000, maxWait: 10000 },
205
+ saveRetry: { attempts: 3, baseDelay: 500, maxDelay: 8000 },
206
+ });
207
+
208
+ engine.on('save:status', ({ status, isDirty, revision, lastSavedAt, error }) => {
209
+ statusLabel.textContent = status;
210
+ });
211
+ ```
212
+
213
+ `status` 是 `saved`、`unsaved`、`saving`、`error` 或 `conflict` 之一。
214
+
215
+ - **未保存的改动。** 每一步记录下来的历史、撤销、重做和元数据修改,都会把文档标记为已修改。`engine.isDirty()` 告诉你是否有未保存的内容。保存进行时做的修改,会一直保持未保存状态,直到下一次保存。
216
+ - **自动保存。** 停止编辑 `delay` 毫秒后保存;即使用户一直在编辑,最迟也会在第一次未保存修改后的 `maxWait` 毫秒保存。`autosave: true` 使用上面示例中的默认值。
217
+ - **同一时间只有一次保存。** 保存进行中再调用 `save()`,只会排队一次后续保存,保存的是最新内容。响应永远不会乱序到达。
218
+ - **过期的响应。** 保存进行中如果加载了另一份文档,这次保存的响应会被忽略,排队中的保存会以 `SAVE_CANCELLED` 取消。
219
+ - **冲突。** 当另一个标签页或设备先保存时,保存会以 `SAVE_CONFLICT` 失败,状态变为 `conflict`。你可以用 `engine.load(id)` 重新加载文档,或者用 `engine.save({ overwrite: true })` 保留你的版本。
220
+ - **重试。** 临时失败会按指数退避加随机抖动重试。每次重试都会触发 `save:retry`,带有 `{ attempt, delay, error }`。
221
+ - **未保存的内容不会被悄悄替换。** 使用存储适配器时,只要有未保存的改动,`load`、`loadDocument`、`importFabricJson` 和 `newDocument` 都会以 `UNSAVED_CHANGES` 拒绝。先保存,或者在用户选择丢弃改动时传入 `{ discardUnsavedChanges: true }`。
222
+
223
+ ### 离开前提醒
224
+
225
+ ```ts
226
+ import { bindUnsavedChangesWarning } from 'fabricjs-document-engine';
227
+
228
+ const unbind = bindUnsavedChangesWarning(engine);
229
+ ```
230
+
231
+ 完整指南:[自动保存](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/autosave)和[保存冲突](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/save-conflicts)。
232
+
233
+ ## 图片、字体和 CORS
234
+
235
+ 每份保存的文档都带有一个 `assets` 清单,列出每个图片 URL 和字体变体,以及使用它们的对象 id。以 `data:` URL 内嵌的图片不需要请求,所以不在清单中。
236
+
237
+ ```ts
238
+ const engine = createDocumentEngine({
239
+ canvas,
240
+ storage,
241
+ assets: {
242
+ resolveUrl: (url) => url.replace('asset://', 'https://cdn.example.com/'),
243
+ replaceMissingImage: (image) => '/placeholder.png',
244
+ upload: async ({ blob }) => uploadToYourBucket(blob),
245
+ loadFont: async ({ family, weight, style }) => {
246
+ const face = new FontFace(family, `url(/fonts/${family}-${weight}.woff2)`, { weight, style });
247
+ document.fonts.add(await face.load());
248
+ },
249
+ requireFonts: false,
250
+ },
251
+ });
252
+
253
+ engine.on('assets:warning', ({ warnings }) => warnings.forEach((warning) => console.warn(warning.message)));
254
+ ```
255
+
256
+ ### 打开文档时
257
+
258
+ 1. `resolveUrl` 可以改写每个存储的 URL,例如给它签名,或者把资源 id 映射到 CDN。
259
+ 2. `loadFont` 对每个字体变体运行一次。之后引擎会检查字体是否真的能渲染,而不是悄悄回退到默认字体。
260
+ 3. 图片每次加载六张(`maxConcurrentImages`),每张最多等 30 秒(`imageTimeout`)。如果有缺失,`replaceMissingImage` 可以为每一张提供替换 URL。返回 `null` 则保持缺失。
261
+ 4. 如果仍有图片缺失,加载会以 `MISSING_ASSETS` 失败,`error.missingAssets` 列出每个 `{ url, objectIds, failure }`。画布不会被改动。
262
+
263
+ `failure.reason` 说明图片失败的原因,方便你显示合适的提示:
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
+ 浏览器会有意隐藏一些细节。来自其他网站、没有 CORS 头的图片失败时,如果图片设置了 `crossOrigin`,原因是 `CORS`,否则是 `NETWORK`。
279
+
280
+ 不可用的 Fabric.js 字体会产生 `FONT_UNAVAILABLE` 警告,文字使用后备字体。设置 `requireFonts: true` 则改为以 `MISSING_FONTS` 失败。警告也会随 `load:success` 以 `{ document, warnings }` 的形式传递。
281
+
282
+ ### 保存文档时
283
+
284
+ 只存在于当前标签页的图片(`blob:` URL)和内嵌的 `data:` 图片,会各传给 `upload` 一次,文档中保存返回的 URL。没有 `upload` 处理函数时,`blob:` 图片会产生 `ASSET_NOT_PORTABLE` 警告,因为其他设备无法打开它们。
285
+
286
+ ### 跨域图片
287
+
288
+ Fabric.js 的 CORS 图片问题是导出失败最常见的原因。来自其他站点、没有设置 `crossOrigin: 'anonymous'` 的图片会污染画布,导出就会失败。引擎会以 `IMAGE_CROSS_ORIGIN` 发出警告,让你在用户导出之前修复它。
289
+
290
+ ### 随时检查和替换
291
+
292
+ ```ts
293
+ const report = await engine.checkAssets();
294
+ report.missingImages;
295
+ report.unavailableFonts;
296
+ report.warnings;
297
+
298
+ await engine.replaceImage('/old-logo.png', '/new-logo.png');
299
+ ```
300
+
301
+ `replaceImage` 会替换所有使用某个 URL 的图片。每张图片在页面上的尺寸保持不变,这次修改算一步撤销。`engine.getAssetManifest()` 返回当前画布的清单。
302
+
303
+ ## 导出图片、SVG 或 JSON
304
+
305
+ Fabric.js 导出 PNG、JPEG、WebP 或 SVG(fabricjs 导出 svg、导出图片),一次调用就能完成。结果是一个 `Blob`,可以下载或上传。
306
+
307
+ ```ts
308
+ import { downloadExport } from 'fabricjs-document-engine';
309
+
310
+ const result = await engine.export({ format: 'png', scale: 2 });
311
+ downloadExport(result, 'poster.png');
312
+ ```
313
+
314
+ `result` 是 `{ format, mimeType, blob, width, height, warnings }`。JSON 导出还包含 `document`。
315
+
316
+ | 选项 | 取值 | 默认值 |
317
+ | --- | --- | --- |
318
+ | `format` | `'png'`、`'jpeg'`、`'webp'`、`'svg'` 或 `'json'` | 必填 |
319
+ | `scale` | 输出尺寸倍数,例如 `2` 用于高分屏 | `1` |
320
+ | `quality` | 0 到 1,用于 JPEG 和 WebP | `0.92` |
321
+ | `area` | `'canvas'`、`'content'`(所有对象)、`'selection'`,或 `{ left, top, width, height }` | `'canvas'` |
322
+ | `padding` | `content` 或 `selection` 周围的额外空白 | `0` |
323
+ | `background` | `'keep'`、`'transparent'` 或任意 CSS 颜色 | `'keep'` |
324
+ | `signal` | 用于取消的 `AbortSignal` | |
325
+ | `svg` | SVG 导出的选项:`{ textOnPath?, embedImages?, maxEmbeddedImageBytes?, embedFonts? }` | |
326
+
327
+ - 当前的缩放和平移不影响结果。导出始终使用文档坐标,之后会恢复视图。
328
+ - JPEG 没有透明通道,所以空的或透明的背景会变成白色,而不是黑色。
329
+ - 导出不会改变画布、历史记录或未保存状态。
330
+ - JSON 导出就是保存时生成的那份可移植文档;设置了 `assets.upload` 时,也包括上传后的图片。
331
+ - PDF 导出见下文的[导出 PDF](#导出-pdf)。
332
+
333
+ ### 在任何地方都能打开的 SVG
334
+
335
+ Fabric.js 导出的 SVG 通过 URL 链接图片。在 Illustrator 里、在另一台电脑上,或者签名 URL 过期之后打开,图片就成了空框。可以把图片和字体一起嵌入文件:
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
+ 来自不允许 CORS 的其他网站的图片,页面无法读取。使用 `embedImages: true` 时它会保留为链接,并给出带有对象 id 的 `IMAGE_NOT_EMBEDDED` 警告。字体可以是 URL,也可以是文件字节;设置在单个字母上的字体也会被嵌入。
348
+
349
+ ### SVG 中的曲线文字
350
+
351
+ Fabric.js 沿路径排列的文字(`text.path`)导出成 SVG 后,和画布上看起来不一样:`pathAlign` 被忽略,抬高的字母移向错误的方向,文字背景和下划线被画成直的。文字里有空格时,Fabric 6 和 7 甚至会写出无效的 XML,浏览器和 Illustrator 都打不开。
352
+
353
+ SVG 导出会把每个字母写在画布上绘制它的位置,背景和下划线也在同一个位置,所以曲线文字在 SVG 里看起来一样。所有 SVG 阅读器都能理解这种输出,文字也仍然可以编辑。传入 `svg: { textOnPath: 'fabric' }` 可以保留 Fabric 自己的输出。
354
+
355
+ ### 预检和错误
356
+
357
+ 渲染之前,引擎会检查画布上的对象:
358
+
359
+ - **`MISSING_IMAGE`**:某张图片加载失败。
360
+ - **`CROSS_ORIGIN_IMAGE`**:来自其他站点、没有 CORS 的图片会让浏览器阻止 PNG、JPEG 或 WebP 导出。SVG 和 JSON 不受影响。
361
+ - **`MISSING_FONT`**:某种字体不可用,且开启了 `assets.requireFonts`。否则你会得到 `FONT_UNAVAILABLE` 警告。
362
+
363
+ 只要发现问题,`export` 就会以 `EXPORT_BLOCKED` 拒绝,`error.problems` 列出每个 `{ code, message, url?, family?, objectIds }`。你可以先运行同样的检查,把结果显示在界面上:
364
+
365
+ ```ts
366
+ const check = await engine.preflightExport({ format: 'png' });
367
+ if (!check.ok) showProblems(check.problems);
368
+ ```
369
+
370
+ ## 导出 PDF
371
+
372
+ Fabric.js 本身没有 PDF 导出,常见的做法是把截图塞进 jsPDF,得到的页面模糊,文字也无法选中。`exportPdf` 把画布画成真正的 PDF 矢量和文字。先安装两个可选的库:
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
+ - **文字仍然是文字。** 使用你传入的字体,以及 Arial、Helvetica、Times 和 Courier 的文字,在 PDF 中可以选中和搜索。字体必须是 TrueType(.ttf)文件,大多数字体网站都会在网页格式之外提供它。
395
+ - **默认是混合模式。** 所有内容都画成矢量,只有 PDF 矢量无法表现的部分除外:阴影、混合模式、渐变描边、缩放时保持宽度的描边,以及没有字体文件的文字。这些对象会在原位置被画成 300 dpi 的图片,`warnings` 会指出是哪些对象。使用 `mode: 'vector'` 只输出矢量,使用 `mode: 'raster'` 则每页一张图片。
396
+ - **曲线文字和下划线**的效果和画布上一样,使用的是与 SVG 导出相同的修正。
397
+ - **多页。** 传入引擎、Fabric 画布或已保存文档组成的数组,每个生成一页。已保存的文档会画在离屏画布上,每页完成后释放。
398
+
399
+ ## 批量渲染文档
400
+
401
+ 在一个浏览器标签页里为几百份已保存的设计生成缩略图或 PDF,如果每份设计用一个画布,内存会耗尽,因为浏览器释放画布内存很慢。`renderDocuments` 复用几个离屏画布,并在每份文档之后释放所有对象和缓存画布:
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
+ 每份文档完成后就会返回结果,所以可以逐个上传。损坏的文档会报告自己的 `error`,其余文档继续渲染。`documents` 可以是异步可迭代对象,例如数据库查询的分页结果;`signal` 可以停止整个批次。
413
+
414
+ ## 导入 SVG 文件
415
+
416
+ Fabric 常用的 SVG 导入方式,即 `loadSVGFromString` 加 `util.groupSVGElements`,会按绘制的内容确定编组大小。SVG 的 viewBox 之外的元素,或者隐藏的元素,会让整幅图移动并改变大小。`importSvg` 保留 SVG 自己的画框:
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
+ - 元素落在 SVG 放置它们的位置,已经应用了 `viewBox` 和 `preserveAspectRatio`。
428
+ - 结果是一个固定布局、大小等于视口的编组;使用 `as: 'objects'` 时则是分开的对象。两种方式都只算一步撤销,每个对象都有 id。
429
+ - 脚本、事件处理器、`foreignObject`、指向其他文件的链接,以及 `limits.isAllowedUrl` 拒绝的图片地址都会被移除,`warnings` 会说明移除了什么。
430
+ - 大小限制和文档相同,过大或嵌套过深的 SVG 会以 `UNSAFE_DOCUMENT` 被拒绝。
431
+
432
+ ## 版本历史
433
+
434
+ ```ts
435
+ const version = await engine.createVersion('Sent to client');
436
+ const versions = await engine.listVersions();
437
+ await engine.restoreVersion(version.id);
438
+ await engine.deleteVersion(version.id);
439
+ ```
440
+
441
+ - 版本是文档的完整副本,保存在你的存储适配器中。内置适配器都支持版本。自定义适配器需要增加四个方法:`saveVersion(version)`、`listVersions(documentId)`、`loadVersion(documentId, versionId)` 和 `deleteVersion(documentId, versionId)`。
442
+ - `listVersions` 按从新到旧返回摘要:`{ id, documentId, name, kind, createdAt, revision }`。`kind` 是 `named` 或 `auto`。
443
+ - **恢复永远不会丢失内容。** 引擎会先保留一个名为 `Before restoring "..."` 的自动版本,然后把旧内容作为同一份文档的一个新的、未保存的修订加载。下一次保存会把它存为最新修订,历史保持线性。要撤销一次恢复,恢复那个自动版本即可。
444
+ - **自动版本。** 使用 `versions: { autoEvery: 10, keepAuto: 20 }`,每成功保存 10 次保留一个版本。命名版本永远不会被清理。自动版本只保留最新的 `keepAuto` 个,默认 20 个。
445
+ - 撤销和重做覆盖本次会话中最近的编辑。版本则把选定的状态保留下来,供以后使用。
446
+
447
+ ## 加载 JSON 并从 Fabric 5 迁移
448
+
449
+ 纯 Fabric JSON,例如 Fabric 5、6 或 7 中 `canvas.toJSON()` 的输出,可以直接打开:
450
+
451
+ ```ts
452
+ await engine.importFabricJson(savedJsonText, { id: 'plan-42', metadata: { source: 'old editor' } });
453
+ ```
454
+
455
+ `loadDocument` 和 `load(id)` 也能识别纯 Fabric JSON,所以现有 Fabric 应用存储的项目不需要单独的导入步骤就能打开。用 `load(id)` 加载的文档会保留这个 id,下一次保存时以当前格式存储。
456
+
457
+ 每份文档都记录了自己的 `schemaVersion`,因此 Fabric.js 迁移是单向、逐步的升级。包的格式变化后,旧文档会在打开时升级。发生升级时,`load:success` 会报告 `migratedFrom`。某一步失败会以 `MIGRATION_FAILED` 拒绝,`error.migrationFrom` 指出它开始时的版本。来自更新版本的文档会以 `UNSUPPORTED_SCHEMA` 拒绝,而不会被误读。`migrateDocument(value, context)` 和 `detectSchemaVersion(value)` 也已导出,供服务端批量升级等工具使用。
458
+
459
+ ## 恢复未保存的内容
460
+
461
+ 用户编辑时,Fabric.js 的 IndexedDB 恢复在后台运行:
462
+
463
+ ```ts
464
+ import { createIndexedDbRecovery } from 'fabricjs-document-engine/recovery';
465
+
466
+ const engine = createDocumentEngine({
467
+ canvas,
468
+ storage,
469
+ recovery: { store: createIndexedDbRecovery(), interval: 2000 },
470
+ });
471
+
472
+ const [latest] = await engine.getRecoverableDocuments();
473
+ if (latest && confirm(`Restore unsaved work from ${new Date(latest.savedAt).toLocaleString()}?`)) {
474
+ await engine.restoreRecovery(latest.documentId);
475
+ } else if (latest) {
476
+ await engine.discardRecovery(latest.documentId);
477
+ }
478
+ ```
479
+
480
+ - **检查点。** 有未保存的改动时,最多每 `interval` 毫秒写一次副本(默认 2000)。文档已保存时不写入。
481
+ - **关闭或刷新。** 页面卸载时,浏览器不会让 IndexedDB 写完。所以标签页隐藏或关闭时,引擎还会立即往 localStorage 写一份副本。读取时以最新的副本为准。
482
+ - **只在当前标签页的图片。** `blob:` URL 的图片刷新后就没了。检查点会保留图片数据,恢复时为它们创建新的 URL。
483
+ - **保存之后。** 当一次保存覆盖了所有改动,副本会被删除。如果保存过程中标签页关闭了,副本会保留,所以从编辑到服务器之间的内容不会丢失。
484
+ - **恢复。** 恢复的文档会被标记为未保存,并保留它所基于的修订号。如果服务器在此期间有了更新,下一次保存会报告 `SAVE_CONFLICT`,而不是覆盖更新的内容。
485
+ - **中断的加载。** 文档加载时会保留一个标记。如果标签页在加载中崩溃,下次启动时 `engine.getInterruptedLoad()` 会返回 `{ documentId, startedAt }`,你可以跳过或丢弃那份文档,避免再次崩溃。
486
+ - `engine.flushRecovery()` 立即写一份副本。`engine.getRecovery(id?)` 读取一份。
487
+ - `createMemoryRecovery()` 把副本保存在内存中,适合测试。要使用你自己的存储,实现 `{ get, set, delete, keys }`,页面关闭时可以再加一个可选的同步方法 `setNow`。
488
+
489
+ ## 自定义对象和属性
490
+
491
+ Fabric.js 自定义对象的额外字段,只有在保存时被列出来才会保留,否则就会出现丢失自定义属性的问题。注册一次类,它的属性就能在每次保存、加载、撤销和重做中保留下来:
492
+
493
+ ```ts
494
+ import { Rect } from 'fabric';
495
+
496
+ class Sticker extends Rect {
497
+ static type = 'Sticker';
498
+ declare label: string;
499
+ }
500
+
501
+ const engine = createDocumentEngine({
502
+ canvas,
503
+ customObjects: [{ fabricClass: Sticker, properties: ['label'] }],
504
+ });
505
+ ```
506
+
507
+ 如果文档中包含未注册的类型,加载会以 `UNKNOWN_OBJECT_TYPE` 失败,并列出缺少的类型。你的对象永远不会被变成别的东西。
508
+
509
+ ## Fabric.js 撤销和重做
510
+
511
+ 历史记录默认开启。引擎会自动记录这些操作:
512
+
513
+ - 添加和删除对象,同一个 tick 内的多次改动合为一步
514
+ - 指针移动、缩放和旋转(Fabric 的 `object:modified`)
515
+ - 完成的文字编辑
516
+
517
+ 对于代码直接做的修改,例如 `object.set('fill', 'red')` 或 `canvas.bringObjectForward(object)`,Fabric 不会触发任何事件。把它们包在事务里,或者调用 `commit`:
518
+
519
+ ```ts
520
+ engine.transaction('Arrange furniture', () => {
521
+ chair.set({ left: 120, top: 80 });
522
+ table.set('fill', 'oak');
523
+ canvas.bringObjectToFront(table);
524
+ });
525
+
526
+ canvas.sendObjectBackwards(rug);
527
+ engine.commit('Send rug backwards');
528
+
529
+ await engine.undo();
530
+ await engine.redo();
531
+ ```
532
+
533
+ - 事务可以嵌套,使用最外层的标签。事务也可以是异步的:`await engine.transaction('Import', async () => { ... })`。
534
+ - 要编组或取消编组,把删除和添加放在同一个事务里,它们就只占一步撤销。编组只是对象列表的普通修改。
535
+ - 撤销和重做会根据保存的状态重建被修改的对象,所以它们会以新实例、相同 id 的形式回来。请用 `engine.getObjectById(id)` 重新获取,而不是保留旧的引用。
536
+ - 用 `createDocumentEngine({ canvas, history: { limit: 50 } })` 只保留最近 50 步。默认是 100。
537
+
538
+ ### 键盘快捷键
539
+
540
+ ```ts
541
+ import { bindKeyboardShortcuts } from 'fabricjs-document-engine';
542
+
543
+ const unbind = bindKeyboardShortcuts(engine);
544
+ ```
545
+
546
+ Ctrl/Cmd + Z 撤销。Ctrl/Cmd + Shift + Z 和 Ctrl + Y 重做。用户在 input、textarea、contenteditable 元素或 Fabric 文字中输入时,快捷键会被忽略,所以那里的原生文字撤销照常工作。传入 `{ target: element }` 可以监听 `window` 以外的元素。
547
+
548
+ ### 工具栏状态
549
+
550
+ ```ts
551
+ engine.on('history:change', ({ canUndo, canRedo, undoLabel, redoLabel }) => {
552
+ undoButton.disabled = !canUndo;
553
+ undoButton.title = undoLabel ? `Undo ${undoLabel}` : 'Undo';
554
+ });
555
+ ```
556
+
557
+ [撤销和重做指南](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/undo-redo)有在线示例,并更详细地介绍了文字编辑。
558
+
559
+ ## 复制、粘贴和图层顺序
560
+
561
+ 在 Fabric.js 里复制粘贴通常用 `object.clone()`,它会复制 id,还可能把移动过的选区或编组里的对象放错位置。这个剪贴板按对象在画布上的真实位置复制,保留自定义属性,并给每个粘贴出的对象、编组子对象和裁剪路径一个新 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
+ 传给 `write` 的内容会像加载的文档一样经过检查,所以粘贴的 JSON 不能带入不安全的图片地址。
579
+
580
+ 图层命令移动指定对象或当前选区,并记录一步撤销。多个选中的对象保持原有顺序。固定的对象(例如背景)永远不会移动:
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
+ 引擎会保存每个对象的 `name`,所以图层面板的名称不会丢。在 React 中,`fabricjs-document-engine/react` 的 `useLayers(engine)` 返回同样的列表,并在每次改动后更新。
595
+
596
+ ## 文档格式
597
+
598
+ ```ts
599
+ interface FabricDocument {
600
+ schemaVersion: number;
601
+ id: string;
602
+ createdAt: string;
603
+ updatedAt: string;
604
+ revision?: number;
605
+ fabricVersion?: string;
606
+ canvas: { width: number; height: number; background?: unknown };
607
+ objects: SerializedFabricObject[];
608
+ assets?: {
609
+ images: Array<{ url: string; objectIds: string[] }>;
610
+ fonts: Array<{ family: string; weight: string; style: string; objectIds: string[] }>;
611
+ };
612
+ metadata: Record<string, unknown>;
613
+ }
614
+ ```
615
+
616
+ 标题、所有者、标签等项目数据,请用 `engine.updateMetadata()` 放在 `metadata` 中,而不是放在 Fabric 对象上。
617
+
618
+ 这个格式由包内附带的 JSON Schema 描述:
619
+
620
+ ```ts
621
+ import schema from 'fabricjs-document-engine/schema/document-v1.json';
622
+ ```
623
+
624
+ ## 适用场景
625
+
626
+ 当你在做 Fabric.js 画布编辑器时使用它:设计编辑器、图片编辑器、户型图工具、标签或证书生成器。绘制、选择和序列化仍然由 Fabric 完成。这个包在上面加了一层文档能力:id、历史记录、保存、加载、资源、恢复、复制和粘贴、图层顺序、SVG 导入,以及图片、SVG 和 PDF 导出。
627
+
628
+ 如果你在比较画布编辑器 JS 库或撤销重做 JavaScript 库,注意它的范围。它不画工具栏,也不做实时协作。[对比页面](https://fabricjs-document-engine.jscrate.dev/zh/docs/overview/comparison)把它和 `fabric-history`、`fabricjs-react` 以及手写的 `toJSON` 放在一起比较。
629
+
630
+ ## 兼容性
631
+
632
+ | | 支持情况 |
633
+ | --- | --- |
634
+ | Fabric | Fabric.js 6 和 Fabric.js 7(peer `^6.0.0 \|\| ^7.0.0`);Fabric 5 的纯 JSON 通过迁移打开 |
635
+ | 浏览器 | 完整测试在 Chromium、Firefox 和 WebKit 中通过 |
636
+ | React | 18 和 19,可选 |
637
+ | Node | 18 或更高,用于服务端导入、校验和迁移。PDF 导出和 `renderDocuments` 需要浏览器 |
638
+ | PDF | 可选的 peer `jspdf` 4 和 `svg2pdf.js` 2.7 或更高,只用于 `fabricjs-document-engine/pdf` |
639
+ | 模块 | ESM 和 CommonJS,带 TypeScript 类型 |
640
+
641
+ 测试过的版本和性能数据(5,000 个对象,除加载外每一步都在 50 ms 以内)见 [docs/compatibility.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility.md)。
642
+
643
+ ## API 参考
644
+
645
+ 每个函数、选项、事件和错误码都列在 [docs/api.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/api.md) 中。每个失败都是一个 `DocumentEngineError`,带有稳定的 `code`,可以直接用来判断,例如 `SAVE_CONFLICT`、`MISSING_ASSETS` 或 `UNSAVED_CHANGES`。加载失败永远不会清空画布,也不会只填一半。
646
+
647
+ ## 稳定性
648
+
649
+ 1.0 版冻结了文档格式和适配器约定:
650
+
651
+ - 文档由发布的 JSON Schema `fabricjs-document-engine/schema/document-v1.json` 校验。每个 1.x 版本都能读取之前版本写入的所有文档,以及 Fabric 5、6 和 7 的纯 Fabric JSON。
652
+ - 公开 API 的名称、选项、事件和错误码在 1.x 内不会改变,只可能新增。
653
+ - 为 1.0 编写的存储、版本和恢复适配器会继续可用。`fabricjs-document-engine/storage` 中的 `verifyStorageAdapter(storage)` 可以检查你的适配器是否遵守保存规则。
654
+
655
+ 完整承诺见[兼容性策略](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility-policy.md)。完整配置见[生产环境指南](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/production.md)。
656
+
657
+ ## 导入内容和限制
658
+
659
+ 文档经常来自用户,所以引擎把它们当作不可信的内容:
660
+
661
+ - 名为 `__proto__`、`constructor` 或 `prototype` 的键会在 Fabric 看到之前被删除。Fabric 会把每个键复制到它创建的对象上,否则这些键可能改变对象的原型。
662
+ - 图片地址在 `assets.resolveUrl` 之后、任何请求之前检查。允许 `http:`、`https:`、`blob:`、相对地址和 `data:image/...`。`javascript:`、`file:` 和非图片的 `data:` 地址会以 `UNSAFE_DOCUMENT` 拒绝。
663
+ - 超过 50,000 个对象、或嵌套超过 100 层的文档会在加载前被拒绝,恶意文件无法卡死标签页。
664
+ - 传给 `importSvg` 的 SVG 文件会在 Fabric 解析之前去掉脚本、事件处理器、`foreignObject` 和指向其他文件的链接,同样的大小限制也适用。
665
+ - 用 `clipboard.write` 写入剪贴板的 JSON 会像文档一样经过检查,所以粘贴的内容不能带入不安全的图片地址。
666
+
667
+ ```ts
668
+ createDocumentEngine({
669
+ canvas,
670
+ limits: {
671
+ maxObjects: 10_000,
672
+ maxDepth: 40,
673
+ isAllowedUrl: (url) => url.startsWith('https://cdn.example.com/'),
674
+ },
675
+ history: { limit: 100, maxBytes: 32 * 1024 * 1024 },
676
+ });
677
+ ```
678
+
679
+ `history.maxBytes` 限制撤销历史占用的内存。默认 64 MB,超出时先丢弃最早的步骤。SVG 导出会转义文字,所以文本框里的 `<script>` 这类内容仍然只是文字。
680
+
681
+ 关于工具栏、状态文字和对话框的无障碍建议,见 [docs/accessibility.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/accessibility.md)。
682
+
683
+ ## 问题排查
684
+
685
+ 常见问题和解决方法见 [docs/troubleshooting.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/troubleshooting.md)。大家问得最多的问题,例如 `loadFromJSON` 为什么会丢失自定义属性,在[常见问题](https://fabricjs-document-engine.jscrate.dev/zh/docs/overview/faq)中有解答。
686
+
687
+ ## 帮助和贡献
688
+
689
+ - 文档和 Fabric.js 在线示例:[fabricjs-document-engine.jscrate.dev](https://fabricjs-document-engine.jscrate.dev/zh)
690
+ - Bug 和功能建议:[GitHub issues](https://github.com/re-sohail/fabricjs-document-engine/issues)
691
+ - 更新日志:[CHANGELOG.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/CHANGELOG.md)
692
+
693
+ 由 [Sohail Khan](https://me.jscrate.dev) 维护。欢迎提交 Pull Request。每个面向用户的改动都需要一个 changeset(`npx changeset`)。
694
+
695
+ ## 许可证
696
+
697
+ MIT