fabricjs-document-engine 1.0.2 → 1.2.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 (118) hide show
  1. package/README.md +220 -11
  2. package/README.zh-CN.md +220 -11
  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 +31 -17
  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 +31 -17
  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 +89 -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 +89 -7
  15. package/dist/commands/clipboard.cjs +141 -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 +140 -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/document/document-format.d.cts +15 -0
  24. package/dist/document/document-format.d.ts +15 -0
  25. package/dist/document/validate-document.cjs +8 -0
  26. package/dist/document/validate-document.js +8 -0
  27. package/dist/engine/create-document-engine.cjs +196 -31
  28. package/dist/engine/create-document-engine.d.cts +44 -7
  29. package/dist/engine/create-document-engine.d.ts +44 -7
  30. package/dist/engine/create-document-engine.js +197 -32
  31. package/dist/engine/errors.d.cts +1 -1
  32. package/dist/engine/errors.d.ts +1 -1
  33. package/dist/export/batch-render.cjs +172 -0
  34. package/dist/export/batch-render.d.cts +43 -0
  35. package/dist/export/batch-render.d.ts +43 -0
  36. package/dist/export/batch-render.js +171 -0
  37. package/dist/export/export-options.cjs +21 -1
  38. package/dist/export/export-options.d.cts +29 -0
  39. package/dist/export/export-options.d.ts +29 -0
  40. package/dist/export/export-options.js +21 -2
  41. package/dist/export/preflight-export.cjs +11 -2
  42. package/dist/export/preflight-export.d.cts +1 -1
  43. package/dist/export/preflight-export.d.ts +1 -1
  44. package/dist/export/preflight-export.js +11 -2
  45. package/dist/export/render-export.cjs +57 -4
  46. package/dist/export/render-export.js +57 -5
  47. package/dist/export/svg/clip-masks.cjs +39 -0
  48. package/dist/export/svg/clip-masks.js +37 -0
  49. package/dist/export/svg/embed-assets.cjs +250 -0
  50. package/dist/export/svg/embed-assets.js +250 -0
  51. package/dist/export/svg/overrides.cjs +32 -0
  52. package/dist/export/svg/overrides.js +32 -0
  53. package/dist/export/svg/raster-object.cjs +82 -0
  54. package/dist/export/svg/raster-object.js +81 -0
  55. package/dist/export/svg/text-decorations.cjs +108 -0
  56. package/dist/export/svg/text-decorations.js +107 -0
  57. package/dist/export/svg/text-on-path.cjs +242 -0
  58. package/dist/export/svg/text-on-path.js +240 -0
  59. package/dist/fabric/fabric-adapter.cjs +49 -6
  60. package/dist/fabric/fabric-adapter.js +49 -6
  61. package/dist/fabric/object-registry.cjs +1 -1
  62. package/dist/fabric/object-registry.js +1 -1
  63. package/dist/fabric/page-state.cjs +69 -0
  64. package/dist/fabric/page-state.js +64 -0
  65. package/dist/history/apply-state.cjs +19 -2
  66. package/dist/history/apply-state.js +19 -2
  67. package/dist/history/create-history.cjs +53 -13
  68. package/dist/history/create-history.d.cts +8 -0
  69. package/dist/history/create-history.d.ts +8 -0
  70. package/dist/history/create-history.js +53 -13
  71. package/dist/history/snapshot.cjs +10 -1
  72. package/dist/history/snapshot.js +10 -2
  73. package/dist/import/svg-import.cjs +256 -0
  74. package/dist/import/svg-import.d.cts +50 -0
  75. package/dist/import/svg-import.d.ts +50 -0
  76. package/dist/import/svg-import.js +256 -0
  77. package/dist/index.cjs +12 -0
  78. package/dist/index.d.cts +10 -5
  79. package/dist/index.d.ts +10 -5
  80. package/dist/index.js +4 -1
  81. package/dist/migrations/migrate-document.cjs +2 -1
  82. package/dist/migrations/migrate-document.js +2 -1
  83. package/dist/pdf/export-pdf.cjs +381 -0
  84. package/dist/pdf/export-pdf.d.cts +73 -0
  85. package/dist/pdf/export-pdf.d.ts +73 -0
  86. package/dist/pdf/export-pdf.js +381 -0
  87. package/dist/pdf/fonts.cjs +115 -0
  88. package/dist/pdf/fonts.d.cts +12 -0
  89. package/dist/pdf/fonts.d.ts +12 -0
  90. package/dist/pdf/fonts.js +112 -0
  91. package/dist/pdf/page-layout.cjs +79 -0
  92. package/dist/pdf/page-layout.d.cts +51 -0
  93. package/dist/pdf/page-layout.d.ts +51 -0
  94. package/dist/pdf/page-layout.js +76 -0
  95. package/dist/pdf.cjs +7 -0
  96. package/dist/pdf.d.cts +4 -0
  97. package/dist/pdf.d.ts +4 -0
  98. package/dist/pdf.js +3 -0
  99. package/dist/react/use-layers.cjs +53 -0
  100. package/dist/react/use-layers.d.cts +10 -0
  101. package/dist/react/use-layers.d.ts +10 -0
  102. package/dist/react/use-layers.js +53 -0
  103. package/dist/react.cjs +2 -0
  104. package/dist/react.d.cts +3 -1
  105. package/dist/react.d.ts +3 -1
  106. package/dist/react.js +2 -1
  107. package/dist/recovery/recovery-controller.cjs +148 -29
  108. package/dist/recovery/recovery-controller.d.cts +8 -0
  109. package/dist/recovery/recovery-controller.d.ts +8 -0
  110. package/dist/recovery/recovery-controller.js +148 -29
  111. package/dist/security/content-limits.cjs +43 -3
  112. package/dist/security/content-limits.d.cts +8 -0
  113. package/dist/security/content-limits.d.ts +8 -0
  114. package/dist/security/content-limits.js +39 -4
  115. package/dist/util/concurrency.cjs +41 -0
  116. package/dist/util/concurrency.js +40 -0
  117. package/package.json +30 -3
  118. package/schema/document-v1.schema.json +5 -1
package/README.zh-CN.md CHANGED
@@ -11,6 +11,8 @@
11
11
 
12
12
  画布、工具栏和界面都由你自己掌控。这个包在旁边工作,把画布上的内容变成一份可以保存、重新打开、继续编辑的文档。它支持 Fabric 6 和 7,可以用在 React、Next.js、Vue、Svelte 或原生 JavaScript 中。
13
13
 
14
+ 它也负责文档周边的工作:复制和粘贴、图层顺序、导入 SVG 文件,以及导出和画布看起来一样的图片、SVG 和 PDF。
15
+
14
16
  ## 为什么需要它
15
17
 
16
18
  做过 fabricjs 编辑器的人,基本都踩过同样的坑。Fabric.js 的序列化和绘制都做得很好,但一份文档需要的不止这些:
@@ -19,8 +21,9 @@
19
21
  - **自定义属性会丢失。** `toJSON` 和 `loadFromJSON` 会丢掉 Fabric 不认识的字段,除非你每次调用都把它们列出来([fabric.js#10887](https://github.com/fabricjs/fabric.js/issues/10887))。
20
22
  - **加载后找不到对象。** Fabric 不给对象分配稳定的 id,所以加载之后没法按 id 获取对象。编组里的子对象更是完全没有 id。
21
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))。
22
25
 
23
- 这个包解决这四个问题,以及它们背后的问题:图片缺失、字体加载失败、标签页崩溃和旧的文件格式。
26
+ 这个包解决这些问题,以及它们背后的问题:图片缺失、字体加载失败、标签页崩溃、旧的文件格式、大文档卡住页面,以及导入的 SVG 文件位置错乱。
24
27
 
25
28
  ## 安装
26
29
 
@@ -30,7 +33,7 @@
30
33
  npm install fabricjs-document-engine fabric
31
34
  ```
32
35
 
33
- 这个包用 TypeScript 编写,自带类型定义。它没有运行时依赖。`fabric` 是 peer dependency,只有使用 hooks 时才需要 React。
36
+ 这个包用 TypeScript 编写,自带类型定义。它没有运行时依赖。`fabric` 是 peer dependency,只有使用 hooks 时才需要 React,只有导出 PDF 时才需要 `jspdf` 和 `svg2pdf.js`。
34
37
 
35
38
  ## 快速开始
36
39
 
@@ -61,13 +64,19 @@ await engine.loadDocument(JSON.parse(localStorage.getItem(document.id)!));
61
64
  - **自定义对象。** 注册你自己的 Fabric 类,以及它们需要保留的额外属性。
62
65
  - **安全保存。** 它会跟踪未保存的改动,也可以自动保存。同一时间只运行一次保存,所以慢的旧保存永远不会覆盖新的改动。修订号检查能发现另一个标签页或设备保存了同一份文档,失败的保存会按退避策略重试。
63
66
  - **你自己的存储。** 用两个函数接入任意后端,或者使用内置的内存和 localStorage 适配器。不需要任何托管服务。
64
- - **图片和字体。** 文档会记录它需要的图片和字体。打开文档时,会先检查每张图片和每种字体。你会拿到缺失内容的准确列表,可以提供替换,只存在于当前标签页的图片会在保存时上传。
67
+ - **图片和字体。** 文档会记录它需要的图片和字体。打开文档时,会先检查每张图片和每种字体。你会拿到缺失内容的准确列表和原因(找不到、服务器错误、CORS、超时或文件损坏),可以提供替换,只存在于当前标签页的图片会在保存时上传。
65
68
  - **恢复。** 用户编辑时,未保存的内容会被复制到 IndexedDB;关闭或刷新标签页的那一刻还会再复制一次。崩溃或刷新之后,你可以提示用户恢复,包括只存在于旧标签页中的图片。
66
- - **导出。** 支持 PNG、JPEG、WebP、SVG 和可编辑的 JSON。区域、缩放和背景由你选择。导出前的预检意味着导出要么成功,要么准确告诉你是哪张图片或哪种字体导致失败。
69
+ - **导出。** 支持 PNG、JPEG、WebP、SVG、PDF 和可编辑的 JSON。区域、缩放和背景由你选择。导出前的预检意味着导出要么成功,要么准确告诉你是哪张图片或哪种字体导致失败。
70
+ - **和画布一致的 SVG。** 沿路径排列的文字在 SVG 中保持原来的位置、背景和下划线。图片和字体可以嵌入文件,所以在 Illustrator 或另一台电脑上也能打开。
71
+ - **带真实文字的 PDF。** 支持 A4、Letter 或画布大小的页面,可以设置页边距,一个文件可以有多页,文字仍然可以选中。只有阴影、混合模式等效果会变成图片。
72
+ - **导入 SVG。** SVG 文件按照它的 viewBox 放置,即使有元素在外面也不会错位;导入前会先移除脚本和外部链接。
73
+ - **批量渲染。** 在一个标签页里为几百份已保存的文档生成缩略图或导出文件,使用几个复用的画布,每份文档完成后都会释放。
67
74
  - **版本和迁移。** 保存命名版本,把任意版本恢复为新的修订,还能打开纯 Fabric JSON 或旧版本这个包保存的文档。
75
+ - **大文档。** 对象分批创建,页面保持响应。加载会报告进度,并能用 `AbortSignal` 取消;取消后画布保持原样。
76
+ - **复制、粘贴和图层。** 剪贴板会保留编组的变换和自定义属性,并给每个粘贴出的对象一个新 id;置顶、置底等图层命令可以让固定的背景保持不动。每个操作都是一步撤销。
68
77
  - **撤销和重做。** 用户的一次操作就是一步撤销。事务可以把代码里的多处改动合成一个带标签的步骤,撤销和重做后 id 保持不变。
69
78
  - **支持 React,不绑定框架。** 为 React 提供 hooks,为其他框架提供一个小的状态 store。
70
- - **经过加固。** 导入的文档会被清理并限制大小,撤销历史有内存上限,每个功能都在 Chromium、Firefox 和 WebKit 中测试过。
79
+ - **经过加固。** 导入的文档和 SVG 文件会被清理并限制大小,撤销历史有内存上限,每个功能都在 Fabric 6 和 7、Chromium、Firefox 和 WebKit 中测试过。导出结果会和画布逐像素比对。
71
80
 
72
81
  ## React、Next.js、Vue 和 Svelte
73
82
 
@@ -164,6 +173,26 @@ const storage: DocumentStorage = {
164
173
  - 对于重试也无法解决的失败,抛出带 `retryable: false` 的错误。其他错误都会重试。
165
174
  - 把 `signal` 传给 `fetch`。打开另一份文档时,引擎会中止它。
166
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
+
167
196
  ## 自动保存和保存冲突
168
197
 
169
198
  Fabric.js 自动保存只是一个选项。本节主要讲保存出错时会发生什么,因为编辑器正是在这里丢失内容的。
@@ -228,8 +257,25 @@ engine.on('assets:warning', ({ warnings }) => warnings.forEach((warning) => cons
228
257
 
229
258
  1. `resolveUrl` 可以改写每个存储的 URL,例如给它签名,或者把资源 id 映射到 CDN。
230
259
  2. `loadFont` 对每个字体变体运行一次。之后引擎会检查字体是否真的能渲染,而不是悄悄回退到默认字体。
231
- 3. 所有图片并行加载。如果有缺失,`replaceMissingImage` 可以为每一张提供替换 URL。返回 `null` 则保持缺失。
232
- 4. 如果仍有图片缺失,加载会以 `MISSING_ASSETS` 失败,`error.missingAssets` 列出每个 `{ url, objectIds }`。画布不会被改动。
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`。
233
279
 
234
280
  不可用的 Fabric.js 字体会产生 `FONT_UNAVAILABLE` 警告,文字使用后备字体。设置 `requireFonts: true` 则改为以 `MISSING_FONTS` 失败。警告也会随 `load:success` 以 `{ document, warnings }` 的形式传递。
235
281
 
@@ -276,12 +322,35 @@ downloadExport(result, 'poster.png');
276
322
  | `padding` | `content` 或 `selection` 周围的额外空白 | `0` |
277
323
  | `background` | `'keep'`、`'transparent'` 或任意 CSS 颜色 | `'keep'` |
278
324
  | `signal` | 用于取消的 `AbortSignal` | |
325
+ | `svg` | SVG 导出的选项:`{ textOnPath?, embedImages?, maxEmbeddedImageBytes?, embedFonts? }` | |
279
326
 
280
327
  - 当前的缩放和平移不影响结果。导出始终使用文档坐标,之后会恢复视图。
281
328
  - JPEG 没有透明通道,所以空的或透明的背景会变成白色,而不是黑色。
282
329
  - 导出不会改变画布、历史记录或未保存状态。
283
330
  - JSON 导出就是保存时生成的那份可移植文档;设置了 `assets.upload` 时,也包括上传后的图片。
284
- - 不支持导出 PDF。如果需要,把 PNG 或 SVG 结果交给 PDF 库处理。
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 自己的输出。
285
354
 
286
355
  ### 预检和错误
287
356
 
@@ -298,6 +367,68 @@ const check = await engine.preflightExport({ format: 'png' });
298
367
  if (!check.ok) showProblems(check.problems);
299
368
  ```
300
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
+
301
432
  ## 版本历史
302
433
 
303
434
  ```ts
@@ -425,6 +556,73 @@ engine.on('history:change', ({ canUndo, canRedo, undoLabel, redoLabel }) => {
425
556
 
426
557
  [撤销和重做指南](https://fabricjs-document-engine.jscrate.dev/zh/docs/guides/undo-redo)有在线示例,并更详细地介绍了文字编辑。
427
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
+ 无论用户的操作和你的代码怎样交错,下面这些保证都成立:
599
+
600
+ ```ts
601
+ // Page settings are one undo step and count as unsaved work
602
+ engine.setPage({ width: 1080, height: 1080, background: '#fff8e7' }, 'Square post');
603
+
604
+ // All or nothing: a failure leaves the canvas as it was
605
+ await engine.transaction('Apply template', async () => {
606
+ await addTemplateObjects(engine.canvas);
607
+ }, { rollback: true });
608
+
609
+ // A load refuses to overwrite edits made while it ran
610
+ try {
611
+ await engine.load('poster-42');
612
+ } catch (error) {
613
+ if (isDocumentEngineError(error) && error.code === 'LOAD_CONFLICT') askBeforeReplacing();
614
+ }
615
+ ```
616
+
617
+ - **整个页面都会保存。** 画布上的背景图片、叠加层和蒙版(`canvas.backgroundImage`、`overlayImage`、`clipPath`)会被保存、重新打开、检查缺失图片,也会进入版本和恢复副本。
618
+ - **页面修改可以撤销。** `setPage` 修改尺寸、背景、叠加层或蒙版,只算一步撤销;在 `transaction` 里直接修改画布也会被记录。
619
+ - **输入立即算作修改。** 每次按键都会把文档标记为未保存,并触发自动保存和恢复副本,而整次编辑仍然只算一步撤销。
620
+ - **加载不会覆盖修改。** 如果文档加载期间画布被修改,加载会以 `LOAD_CONFLICT` 停止并保留这些修改,除非传入 `discardUnsavedChanges: true`。
621
+ - **异步操作留在原来的文档里。** 在打开另一份文档之后才完成的 SVG 导入、图片替换或粘贴会以 `DOCUMENT_CHANGED` 被丢弃,而不会落到错误的文档里。
622
+ - **每个标签页有自己的恢复副本。** 两个标签页编辑同一份文档时不再互相覆盖副本,保存时只删除它覆盖的那一份。
623
+ - **绘制前先检查尺寸。** 超过浏览器画布限制的页面、导出和图片,会在创建任何画布之前被拒绝,大文件不会让标签页崩溃。限制用 `limits` 设置。
624
+ - **需要时可以回滚。** `transaction(label, work, { rollback: true })` 在 `work` 失败时撤回它做的所有修改。
625
+
428
626
  ## 文档格式
429
627
 
430
628
  ```ts
@@ -435,7 +633,15 @@ interface FabricDocument {
435
633
  updatedAt: string;
436
634
  revision?: number;
437
635
  fabricVersion?: string;
438
- canvas: { width: number; height: number; background?: unknown };
636
+ canvas: {
637
+ width: number;
638
+ height: number;
639
+ background?: unknown; // color, gradient or pattern
640
+ backgroundImage?: object; // Fabric image behind every object
641
+ overlay?: unknown; // color drawn over every object
642
+ overlayImage?: object; // Fabric image over every object
643
+ clipPath?: object; // mask for the whole canvas
644
+ };
439
645
  objects: SerializedFabricObject[];
440
646
  assets?: {
441
647
  images: Array<{ url: string; objectIds: string[] }>;
@@ -455,7 +661,7 @@ import schema from 'fabricjs-document-engine/schema/document-v1.json';
455
661
 
456
662
  ## 适用场景
457
663
 
458
- 当你在做 Fabric.js 画布编辑器时使用它:设计编辑器、图片编辑器、户型图工具、标签或证书生成器。绘制、选择和序列化仍然由 Fabric 完成。这个包在上面加了一层文档能力:id、历史记录、保存、加载、资源、导出和恢复。
664
+ 当你在做 Fabric.js 画布编辑器时使用它:设计编辑器、图片编辑器、户型图工具、标签或证书生成器。绘制、选择和序列化仍然由 Fabric 完成。这个包在上面加了一层文档能力:id、历史记录、保存、加载、资源、恢复、复制和粘贴、图层顺序、SVG 导入,以及图片、SVG 和 PDF 导出。
459
665
 
460
666
  如果你在比较画布编辑器 JS 库或撤销重做 JavaScript 库,注意它的范围。它不画工具栏,也不做实时协作。[对比页面](https://fabricjs-document-engine.jscrate.dev/zh/docs/overview/comparison)把它和 `fabric-history`、`fabricjs-react` 以及手写的 `toJSON` 放在一起比较。
461
667
 
@@ -466,7 +672,8 @@ import schema from 'fabricjs-document-engine/schema/document-v1.json';
466
672
  | Fabric | Fabric.js 6 和 Fabric.js 7(peer `^6.0.0 \|\| ^7.0.0`);Fabric 5 的纯 JSON 通过迁移打开 |
467
673
  | 浏览器 | 完整测试在 Chromium、Firefox 和 WebKit 中通过 |
468
674
  | React | 18 和 19,可选 |
469
- | Node | 18 或更高,用于服务端导入、校验和迁移 |
675
+ | Node | 18 或更高,用于服务端导入、校验和迁移。PDF 导出和 `renderDocuments` 需要浏览器 |
676
+ | PDF | 可选的 peer `jspdf` 4 和 `svg2pdf.js` 2.7 或更高,只用于 `fabricjs-document-engine/pdf` |
470
677
  | 模块 | ESM 和 CommonJS,带 TypeScript 类型 |
471
678
 
472
679
  测试过的版本和性能数据(5,000 个对象,除加载外每一步都在 50 ms 以内)见 [docs/compatibility.md](https://github.com/re-sohail/fabricjs-document-engine/blob/main/docs/compatibility.md)。
@@ -492,6 +699,8 @@ import schema from 'fabricjs-document-engine/schema/document-v1.json';
492
699
  - 名为 `__proto__`、`constructor` 或 `prototype` 的键会在 Fabric 看到之前被删除。Fabric 会把每个键复制到它创建的对象上,否则这些键可能改变对象的原型。
493
700
  - 图片地址在 `assets.resolveUrl` 之后、任何请求之前检查。允许 `http:`、`https:`、`blob:`、相对地址和 `data:image/...`。`javascript:`、`file:` 和非图片的 `data:` 地址会以 `UNSAFE_DOCUMENT` 拒绝。
494
701
  - 超过 50,000 个对象、或嵌套超过 100 层的文档会在加载前被拒绝,恶意文件无法卡死标签页。
702
+ - 传给 `importSvg` 的 SVG 文件会在 Fabric 解析之前去掉脚本、事件处理器、`foreignObject` 和指向其他文件的链接,同样的大小限制也适用。
703
+ - 用 `clipboard.write` 写入剪贴板的 JSON 会像文档一样经过检查,所以粘贴的内容不能带入不安全的图片地址。
495
704
 
496
705
  ```ts
497
706
  createDocumentEngine({
@@ -1,7 +1,10 @@
1
+ import { ImageLoadFailure } from "./image-check.cjs";
1
2
  //#region src/assets/asset-manifest.d.ts
2
3
  export interface ImageAsset {
3
4
  url: string;
4
5
  objectIds: string[];
6
+ /** Why the image could not be loaded. Set on images in `missingImages` and `missingAssets`. */
7
+ failure?: ImageLoadFailure;
5
8
  }
6
9
  export interface FontAsset {
7
10
  family: string;
@@ -1,7 +1,10 @@
1
+ import { ImageLoadFailure } from "./image-check.js";
1
2
  //#region src/assets/asset-manifest.d.ts
2
3
  export interface ImageAsset {
3
4
  url: string;
4
5
  objectIds: string[];
6
+ /** Why the image could not be loaded. Set on images in `missingImages` and `missingAssets`. */
7
+ failure?: ImageLoadFailure;
5
8
  }
6
9
  export interface FontAsset {
7
10
  family: string;
@@ -1,10 +1,18 @@
1
1
  const require_asset_references = require("./asset-references.cjs");
2
2
  const require_asset_manifest = require("./asset-manifest.cjs");
3
+ const require_page_state = require("../fabric/page-state.cjs");
3
4
  const require_errors = require("../engine/errors.cjs");
4
5
  const require_font_check = require("./font-check.cjs");
5
6
  const require_content_limits = require("../security/content-limits.cjs");
6
7
  const require_image_check = require("./image-check.cjs");
7
8
  //#region src/assets/asset-pipeline.ts
9
+ function imageCheckOptions(options) {
10
+ return {
11
+ timeoutMs: options.imageTimeout,
12
+ concurrency: options.maxConcurrentImages,
13
+ maxPixels: options.maxImagePixels
14
+ };
15
+ }
8
16
  function cloneDocument(document) {
9
17
  return JSON.parse(JSON.stringify(document));
10
18
  }
@@ -27,7 +35,7 @@ function pointTo(references, url) {
27
35
  }
28
36
  }
29
37
  async function rewriteUrls(document, rewrite) {
30
- const groups = groupByUrl(require_asset_references.findImageReferences(document.objects));
38
+ const groups = groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)));
31
39
  await Promise.all([...groups].map(async ([url, references]) => {
32
40
  const next = await rewrite(url, references);
33
41
  if (next !== url) pointTo(references, next);
@@ -35,7 +43,7 @@ async function rewriteUrls(document, rewrite) {
35
43
  }
36
44
  function crossOriginWarnings(document) {
37
45
  const warnings = [];
38
- for (const [url, references] of groupByUrl(require_asset_references.findImageReferences(document.objects))) {
46
+ for (const [url, references] of groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)))) {
39
47
  if (!require_image_check.isCrossOriginUrl(url) || references.every((reference) => reference.crossOrigin)) continue;
40
48
  warnings.push({
41
49
  code: "IMAGE_CROSS_ORIGIN",
@@ -54,17 +62,23 @@ function fontWarnings(fonts) {
54
62
  objectIds: font.objectIds
55
63
  }));
56
64
  }
57
- async function inspectAssets(document, options, signal) {
58
- const manifest = require_asset_manifest.buildAssetManifest(document.objects);
59
- const uniqueImages = [...groupByUrl(require_asset_references.findImageReferences(document.objects).filter((reference) => !require_asset_manifest.isEmbeddedUrl(reference.url)))].map(([url, references]) => ({
65
+ async function inspectAssets(document, options, signal, onImageProgress) {
66
+ const manifest = require_asset_manifest.buildAssetManifest(require_page_state.documentObjects(document));
67
+ const uniqueImages = [...groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)).filter((reference) => !require_asset_manifest.isEmbeddedUrl(reference.url)))].map(([url, references]) => ({
60
68
  url,
61
69
  crossOrigin: references[0].crossOrigin
62
70
  }));
63
- const [missingUrls, unavailableFonts] = await Promise.all([options.checkImages === false ? Promise.resolve([]) : require_image_check.findMissingImages(uniqueImages, signal), require_font_check.findUnavailableFonts(manifest.fonts, options.loadFont)]);
64
- const missing = new Set(missingUrls);
71
+ const [failures, unavailableFonts] = await Promise.all([options.checkImages === false ? Promise.resolve([]) : require_image_check.findMissingImages(uniqueImages, signal, {
72
+ ...imageCheckOptions(options),
73
+ onProgress: onImageProgress
74
+ }), require_font_check.findUnavailableFonts(manifest.fonts, options.loadFont)]);
75
+ const failuresByUrl = new Map(failures.map((failure) => [failure.url, failure]));
65
76
  return {
66
77
  manifest,
67
- missingImages: manifest.images.filter((image) => missing.has(image.url)),
78
+ missingImages: manifest.images.filter((image) => failuresByUrl.has(image.url)).map((image) => ({
79
+ ...image,
80
+ failure: failuresByUrl.get(image.url)
81
+ })),
68
82
  unavailableFonts,
69
83
  warnings: [...crossOriginWarnings(document), ...fontWarnings(unavailableFonts)]
70
84
  };
@@ -80,13 +94,13 @@ async function replaceMissingImages(document, missingImages, options, signal) {
80
94
  const replacement = await replace(image);
81
95
  if (typeof replacement === "string" && replacement.length > 0) replacements.set(image.url, replacement);
82
96
  }
83
- const brokenReplacements = new Set(await require_image_check.findMissingImages([...replacements.values()].map((url) => ({
97
+ const brokenReplacements = new Set((await require_image_check.findMissingImages([...replacements.values()].map((url) => ({
84
98
  url,
85
99
  crossOrigin: null
86
- })), signal));
100
+ })), signal, imageCheckOptions(options))).map((failure) => failure.url));
87
101
  const stillMissing = [];
88
102
  const warnings = [];
89
- const groups = groupByUrl(require_asset_references.findImageReferences(document.objects));
103
+ const groups = groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)));
90
104
  for (const image of missingImages) {
91
105
  const replacement = replacements.get(image.url);
92
106
  if (replacement === void 0 || brokenReplacements.has(replacement)) {
@@ -106,20 +120,20 @@ async function replaceMissingImages(document, missingImages, options, signal) {
106
120
  warnings
107
121
  };
108
122
  }
109
- async function prepareAssetsForLoad(input, options, signal, isAllowedUrl) {
123
+ async function prepareAssetsForLoad(input, options, signal, isAllowedUrl, onImageProgress) {
110
124
  const document = cloneDocument(input);
111
125
  const { resolveUrl } = options;
112
126
  if (resolveUrl) await rewriteUrls(document, async (url) => resolveUrl(url));
113
- require_content_limits.refuseUnsafeImageUrls(document.objects, isAllowedUrl);
114
- const report = await inspectAssets(document, options, signal);
127
+ require_content_limits.refuseUnsafeImageUrls(require_page_state.documentObjects(document), isAllowedUrl);
128
+ const report = await inspectAssets(document, options, signal, onImageProgress);
115
129
  if (options.requireFonts && report.unavailableFonts.length > 0) {
116
130
  const families = report.unavailableFonts.map((font) => font.family).join(", ");
117
131
  throw new require_errors.DocumentEngineError("MISSING_FONTS", `These fonts are not available: ${families}`, { missingFonts: report.unavailableFonts });
118
132
  }
119
133
  const { stillMissing, warnings } = await replaceMissingImages(document, report.missingImages, options, signal);
120
134
  if (stillMissing.length > 0) {
121
- const urls = stillMissing.map((image) => image.url).join(", ");
122
- throw new require_errors.DocumentEngineError("MISSING_ASSETS", `These images could not be loaded: ${urls}`, { missingAssets: stillMissing });
135
+ const reasons = stillMissing.map((image) => image.failure ? `${image.url} (${image.failure.reason})` : image.url).join(", ");
136
+ throw new require_errors.DocumentEngineError("MISSING_ASSETS", `These images could not be loaded: ${reasons}`, { missingAssets: stillMissing });
123
137
  }
124
138
  return {
125
139
  document,
@@ -164,7 +178,7 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
164
178
  }
165
179
  return uploading;
166
180
  });
167
- document.assets = require_asset_manifest.buildAssetManifest(document.objects);
181
+ document.assets = require_asset_manifest.buildAssetManifest(require_page_state.documentObjects(document));
168
182
  return {
169
183
  document,
170
184
  warnings
@@ -1,7 +1,7 @@
1
1
  import { AssetManifest, FontAsset, ImageAsset } from "./asset-manifest.cjs";
2
2
  import { FontLoader } from "./font-check.cjs";
3
3
  //#region src/assets/asset-pipeline.d.ts
4
- export type AssetWarningCode = "IMAGE_CROSS_ORIGIN" | "FONT_UNAVAILABLE" | "ASSET_NOT_PORTABLE" | "IMAGE_REPLACED";
4
+ export type AssetWarningCode = "IMAGE_CROSS_ORIGIN" | "FONT_UNAVAILABLE" | "ASSET_NOT_PORTABLE" | "IMAGE_REPLACED" | "IMAGE_NOT_EMBEDDED" | "FONT_NOT_EMBEDDED" | "TEXT_ON_PATH_APPROXIMATED" | "CLIP_PATH_RASTERIZED";
5
5
  export interface AssetWarning {
6
6
  code: AssetWarningCode;
7
7
  message: string;
@@ -21,6 +21,10 @@ export interface AssetOptions {
21
21
  loadFont?: FontLoader;
22
22
  checkImages?: boolean;
23
23
  requireFonts?: boolean;
24
+ /** Milliseconds to wait for each image before it counts as missing. Default 30000. `0` waits forever. */
25
+ imageTimeout?: number;
26
+ /** How many images load at the same time while checking. Default 6. */
27
+ maxConcurrentImages?: number;
24
28
  }
25
29
  export interface AssetReport {
26
30
  manifest: AssetManifest;
@@ -1,7 +1,7 @@
1
1
  import { AssetManifest, FontAsset, ImageAsset } from "./asset-manifest.js";
2
2
  import { FontLoader } from "./font-check.js";
3
3
  //#region src/assets/asset-pipeline.d.ts
4
- export type AssetWarningCode = "IMAGE_CROSS_ORIGIN" | "FONT_UNAVAILABLE" | "ASSET_NOT_PORTABLE" | "IMAGE_REPLACED";
4
+ export type AssetWarningCode = "IMAGE_CROSS_ORIGIN" | "FONT_UNAVAILABLE" | "ASSET_NOT_PORTABLE" | "IMAGE_REPLACED" | "IMAGE_NOT_EMBEDDED" | "FONT_NOT_EMBEDDED" | "TEXT_ON_PATH_APPROXIMATED" | "CLIP_PATH_RASTERIZED";
5
5
  export interface AssetWarning {
6
6
  code: AssetWarningCode;
7
7
  message: string;
@@ -21,6 +21,10 @@ export interface AssetOptions {
21
21
  loadFont?: FontLoader;
22
22
  checkImages?: boolean;
23
23
  requireFonts?: boolean;
24
+ /** Milliseconds to wait for each image before it counts as missing. Default 30000. `0` waits forever. */
25
+ imageTimeout?: number;
26
+ /** How many images load at the same time while checking. Default 6. */
27
+ maxConcurrentImages?: number;
24
28
  }
25
29
  export interface AssetReport {
26
30
  manifest: AssetManifest;
@@ -1,10 +1,18 @@
1
1
  import { findImageReferences } from "./asset-references.js";
2
2
  import { buildAssetManifest, isEmbeddedUrl } from "./asset-manifest.js";
3
+ import { documentObjects } from "../fabric/page-state.js";
3
4
  import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
4
5
  import { findUnavailableFonts } from "./font-check.js";
5
6
  import { refuseUnsafeImageUrls } from "../security/content-limits.js";
6
7
  import { findMissingImages, isCrossOriginUrl, isPortableUrl } from "./image-check.js";
7
8
  //#region src/assets/asset-pipeline.ts
9
+ function imageCheckOptions(options) {
10
+ return {
11
+ timeoutMs: options.imageTimeout,
12
+ concurrency: options.maxConcurrentImages,
13
+ maxPixels: options.maxImagePixels
14
+ };
15
+ }
8
16
  function cloneDocument(document) {
9
17
  return JSON.parse(JSON.stringify(document));
10
18
  }
@@ -27,7 +35,7 @@ function pointTo(references, url) {
27
35
  }
28
36
  }
29
37
  async function rewriteUrls(document, rewrite) {
30
- const groups = groupByUrl(findImageReferences(document.objects));
38
+ const groups = groupByUrl(findImageReferences(documentObjects(document)));
31
39
  await Promise.all([...groups].map(async ([url, references]) => {
32
40
  const next = await rewrite(url, references);
33
41
  if (next !== url) pointTo(references, next);
@@ -35,7 +43,7 @@ async function rewriteUrls(document, rewrite) {
35
43
  }
36
44
  function crossOriginWarnings(document) {
37
45
  const warnings = [];
38
- for (const [url, references] of groupByUrl(findImageReferences(document.objects))) {
46
+ for (const [url, references] of groupByUrl(findImageReferences(documentObjects(document)))) {
39
47
  if (!isCrossOriginUrl(url) || references.every((reference) => reference.crossOrigin)) continue;
40
48
  warnings.push({
41
49
  code: "IMAGE_CROSS_ORIGIN",
@@ -54,17 +62,23 @@ function fontWarnings(fonts) {
54
62
  objectIds: font.objectIds
55
63
  }));
56
64
  }
57
- async function inspectAssets(document, options, signal) {
58
- const manifest = buildAssetManifest(document.objects);
59
- const uniqueImages = [...groupByUrl(findImageReferences(document.objects).filter((reference) => !isEmbeddedUrl(reference.url)))].map(([url, references]) => ({
65
+ async function inspectAssets(document, options, signal, onImageProgress) {
66
+ const manifest = buildAssetManifest(documentObjects(document));
67
+ const uniqueImages = [...groupByUrl(findImageReferences(documentObjects(document)).filter((reference) => !isEmbeddedUrl(reference.url)))].map(([url, references]) => ({
60
68
  url,
61
69
  crossOrigin: references[0].crossOrigin
62
70
  }));
63
- const [missingUrls, unavailableFonts] = await Promise.all([options.checkImages === false ? Promise.resolve([]) : findMissingImages(uniqueImages, signal), findUnavailableFonts(manifest.fonts, options.loadFont)]);
64
- const missing = new Set(missingUrls);
71
+ const [failures, unavailableFonts] = await Promise.all([options.checkImages === false ? Promise.resolve([]) : findMissingImages(uniqueImages, signal, {
72
+ ...imageCheckOptions(options),
73
+ onProgress: onImageProgress
74
+ }), findUnavailableFonts(manifest.fonts, options.loadFont)]);
75
+ const failuresByUrl = new Map(failures.map((failure) => [failure.url, failure]));
65
76
  return {
66
77
  manifest,
67
- missingImages: manifest.images.filter((image) => missing.has(image.url)),
78
+ missingImages: manifest.images.filter((image) => failuresByUrl.has(image.url)).map((image) => ({
79
+ ...image,
80
+ failure: failuresByUrl.get(image.url)
81
+ })),
68
82
  unavailableFonts,
69
83
  warnings: [...crossOriginWarnings(document), ...fontWarnings(unavailableFonts)]
70
84
  };
@@ -80,13 +94,13 @@ async function replaceMissingImages(document, missingImages, options, signal) {
80
94
  const replacement = await replace(image);
81
95
  if (typeof replacement === "string" && replacement.length > 0) replacements.set(image.url, replacement);
82
96
  }
83
- const brokenReplacements = new Set(await findMissingImages([...replacements.values()].map((url) => ({
97
+ const brokenReplacements = new Set((await findMissingImages([...replacements.values()].map((url) => ({
84
98
  url,
85
99
  crossOrigin: null
86
- })), signal));
100
+ })), signal, imageCheckOptions(options))).map((failure) => failure.url));
87
101
  const stillMissing = [];
88
102
  const warnings = [];
89
- const groups = groupByUrl(findImageReferences(document.objects));
103
+ const groups = groupByUrl(findImageReferences(documentObjects(document)));
90
104
  for (const image of missingImages) {
91
105
  const replacement = replacements.get(image.url);
92
106
  if (replacement === void 0 || brokenReplacements.has(replacement)) {
@@ -106,20 +120,20 @@ async function replaceMissingImages(document, missingImages, options, signal) {
106
120
  warnings
107
121
  };
108
122
  }
109
- async function prepareAssetsForLoad(input, options, signal, isAllowedUrl) {
123
+ async function prepareAssetsForLoad(input, options, signal, isAllowedUrl, onImageProgress) {
110
124
  const document = cloneDocument(input);
111
125
  const { resolveUrl } = options;
112
126
  if (resolveUrl) await rewriteUrls(document, async (url) => resolveUrl(url));
113
- refuseUnsafeImageUrls(document.objects, isAllowedUrl);
114
- const report = await inspectAssets(document, options, signal);
127
+ refuseUnsafeImageUrls(documentObjects(document), isAllowedUrl);
128
+ const report = await inspectAssets(document, options, signal, onImageProgress);
115
129
  if (options.requireFonts && report.unavailableFonts.length > 0) {
116
130
  const families = report.unavailableFonts.map((font) => font.family).join(", ");
117
131
  throw new DocumentEngineError("MISSING_FONTS", `These fonts are not available: ${families}`, { missingFonts: report.unavailableFonts });
118
132
  }
119
133
  const { stillMissing, warnings } = await replaceMissingImages(document, report.missingImages, options, signal);
120
134
  if (stillMissing.length > 0) {
121
- const urls = stillMissing.map((image) => image.url).join(", ");
122
- throw new DocumentEngineError("MISSING_ASSETS", `These images could not be loaded: ${urls}`, { missingAssets: stillMissing });
135
+ const reasons = stillMissing.map((image) => image.failure ? `${image.url} (${image.failure.reason})` : image.url).join(", ");
136
+ throw new DocumentEngineError("MISSING_ASSETS", `These images could not be loaded: ${reasons}`, { missingAssets: stillMissing });
123
137
  }
124
138
  return {
125
139
  document,
@@ -164,7 +178,7 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
164
178
  }
165
179
  return uploading;
166
180
  });
167
- document.assets = buildAssetManifest(document.objects);
181
+ document.assets = buildAssetManifest(documentObjects(document));
168
182
  return {
169
183
  document,
170
184
  warnings