fabricjs-document-engine 1.1.0 → 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.
- package/README.md +39 -1
- package/README.zh-CN.md +39 -1
- package/dist/assets/asset-pipeline.cjs +10 -8
- package/dist/assets/asset-pipeline.d.cts +1 -1
- package/dist/assets/asset-pipeline.d.ts +1 -1
- package/dist/assets/asset-pipeline.js +10 -8
- package/dist/assets/image-check.cjs +4 -3
- package/dist/assets/image-check.d.cts +1 -1
- package/dist/assets/image-check.d.ts +1 -1
- package/dist/assets/image-check.js +4 -3
- package/dist/commands/clipboard.cjs +5 -0
- package/dist/commands/clipboard.js +5 -0
- package/dist/document/document-format.d.cts +15 -0
- package/dist/document/document-format.d.ts +15 -0
- package/dist/document/validate-document.cjs +8 -0
- package/dist/document/validate-document.js +8 -0
- package/dist/engine/create-document-engine.cjs +115 -21
- package/dist/engine/create-document-engine.d.cts +24 -6
- package/dist/engine/create-document-engine.d.ts +24 -6
- package/dist/engine/create-document-engine.js +116 -22
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/export-options.cjs +3 -0
- package/dist/export/export-options.d.cts +6 -0
- package/dist/export/export-options.d.ts +6 -0
- package/dist/export/export-options.js +3 -0
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/render-export.cjs +42 -4
- package/dist/export/render-export.js +43 -5
- package/dist/export/svg/clip-masks.cjs +39 -0
- package/dist/export/svg/clip-masks.js +37 -0
- package/dist/export/svg/embed-assets.cjs +1 -0
- package/dist/export/svg/embed-assets.js +1 -0
- package/dist/export/svg/raster-object.cjs +82 -0
- package/dist/export/svg/raster-object.js +81 -0
- package/dist/{pdf → export/svg}/text-decorations.cjs +3 -3
- package/dist/{pdf → export/svg}/text-decorations.js +3 -3
- package/dist/export/svg/text-on-path.cjs +0 -10
- package/dist/export/svg/text-on-path.js +1 -10
- package/dist/fabric/fabric-adapter.cjs +5 -3
- package/dist/fabric/fabric-adapter.js +5 -3
- package/dist/fabric/page-state.cjs +69 -0
- package/dist/fabric/page-state.js +64 -0
- package/dist/history/apply-state.cjs +19 -2
- package/dist/history/apply-state.js +19 -2
- package/dist/history/create-history.cjs +53 -13
- package/dist/history/create-history.d.cts +8 -0
- package/dist/history/create-history.d.ts +8 -0
- package/dist/history/create-history.js +53 -13
- package/dist/history/snapshot.cjs +10 -1
- package/dist/history/snapshot.js +10 -2
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/migrations/migrate-document.cjs +2 -1
- package/dist/migrations/migrate-document.js +2 -1
- package/dist/pdf/export-pdf.cjs +8 -67
- package/dist/pdf/export-pdf.js +8 -67
- package/dist/recovery/recovery-controller.cjs +148 -29
- package/dist/recovery/recovery-controller.d.cts +8 -0
- package/dist/recovery/recovery-controller.d.ts +8 -0
- package/dist/recovery/recovery-controller.js +148 -29
- package/dist/security/content-limits.cjs +43 -3
- package/dist/security/content-limits.d.cts +8 -0
- package/dist/security/content-limits.d.ts +8 -0
- package/dist/security/content-limits.js +39 -4
- package/dist/util/concurrency.cjs +16 -3
- package/dist/util/concurrency.js +16 -3
- package/package.json +1 -1
- package/schema/document-v1.schema.json +5 -1
package/README.md
CHANGED
|
@@ -593,6 +593,36 @@ getLayers(engine); // [{ id, type, name, index, visible, locked }], top first
|
|
|
593
593
|
|
|
594
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
595
|
|
|
596
|
+
## Edits that are never lost
|
|
597
|
+
|
|
598
|
+
These guarantees hold however the user and your code interleave work:
|
|
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
|
+
- **The whole page is saved.** Background images, overlays and a mask on the canvas (`canvas.backgroundImage`, `overlayImage`, `clipPath`) are saved, reopened, checked for missing images and kept in versions and recovery copies.
|
|
618
|
+
- **Page changes are undoable.** `setPage` changes the size, background, overlay or mask as one undo step, and changes made to the canvas inside a `transaction` count too.
|
|
619
|
+
- **Typing counts at once.** Each keystroke marks the document unsaved and feeds autosave and recovery, while the whole edit stays one undo step.
|
|
620
|
+
- **Loads keep edits.** If the canvas is edited while a document loads, the load stops with `LOAD_CONFLICT` and the edits stay, unless you pass `discardUnsavedChanges: true`.
|
|
621
|
+
- **Async work stays in its document.** An SVG import, image replacement or paste that finishes after another document was opened is dropped with `DOCUMENT_CHANGED`, instead of landing in the wrong document.
|
|
622
|
+
- **Each tab has its own recovery copy.** Two tabs editing the same document no longer overwrite each other's copy, and a save removes only the copy it covers.
|
|
623
|
+
- **Sizes are checked before drawing.** Pages, exports and images over the browser's canvas limits are refused before any canvas is made, so a huge file cannot crash the tab. Limits are set with `limits`.
|
|
624
|
+
- **Rollback when you want it.** `transaction(label, work, { rollback: true })` undoes everything `work` changed when it fails.
|
|
625
|
+
|
|
596
626
|
## Document format
|
|
597
627
|
|
|
598
628
|
```ts
|
|
@@ -603,7 +633,15 @@ interface FabricDocument {
|
|
|
603
633
|
updatedAt: string;
|
|
604
634
|
revision?: number;
|
|
605
635
|
fabricVersion?: string;
|
|
606
|
-
canvas: {
|
|
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
|
+
};
|
|
607
645
|
objects: SerializedFabricObject[];
|
|
608
646
|
assets?: {
|
|
609
647
|
images: Array<{ url: string; objectIds: string[] }>;
|
package/README.zh-CN.md
CHANGED
|
@@ -593,6 +593,36 @@ getLayers(engine); // [{ id, type, name, index, visible, locked }], top first
|
|
|
593
593
|
|
|
594
594
|
引擎会保存每个对象的 `name`,所以图层面板的名称不会丢。在 React 中,`fabricjs-document-engine/react` 的 `useLayers(engine)` 返回同样的列表,并在每次改动后更新。
|
|
595
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
|
+
|
|
596
626
|
## 文档格式
|
|
597
627
|
|
|
598
628
|
```ts
|
|
@@ -603,7 +633,15 @@ interface FabricDocument {
|
|
|
603
633
|
updatedAt: string;
|
|
604
634
|
revision?: number;
|
|
605
635
|
fabricVersion?: string;
|
|
606
|
-
canvas: {
|
|
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
|
+
};
|
|
607
645
|
objects: SerializedFabricObject[];
|
|
608
646
|
assets?: {
|
|
609
647
|
images: Array<{ url: string; objectIds: string[] }>;
|
|
@@ -1,5 +1,6 @@
|
|
|
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");
|
|
@@ -8,7 +9,8 @@ const require_image_check = require("./image-check.cjs");
|
|
|
8
9
|
function imageCheckOptions(options) {
|
|
9
10
|
return {
|
|
10
11
|
timeoutMs: options.imageTimeout,
|
|
11
|
-
concurrency: options.maxConcurrentImages
|
|
12
|
+
concurrency: options.maxConcurrentImages,
|
|
13
|
+
maxPixels: options.maxImagePixels
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
function cloneDocument(document) {
|
|
@@ -33,7 +35,7 @@ function pointTo(references, url) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
async function rewriteUrls(document, rewrite) {
|
|
36
|
-
const groups = groupByUrl(require_asset_references.findImageReferences(document
|
|
38
|
+
const groups = groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)));
|
|
37
39
|
await Promise.all([...groups].map(async ([url, references]) => {
|
|
38
40
|
const next = await rewrite(url, references);
|
|
39
41
|
if (next !== url) pointTo(references, next);
|
|
@@ -41,7 +43,7 @@ async function rewriteUrls(document, rewrite) {
|
|
|
41
43
|
}
|
|
42
44
|
function crossOriginWarnings(document) {
|
|
43
45
|
const warnings = [];
|
|
44
|
-
for (const [url, references] of groupByUrl(require_asset_references.findImageReferences(document
|
|
46
|
+
for (const [url, references] of groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)))) {
|
|
45
47
|
if (!require_image_check.isCrossOriginUrl(url) || references.every((reference) => reference.crossOrigin)) continue;
|
|
46
48
|
warnings.push({
|
|
47
49
|
code: "IMAGE_CROSS_ORIGIN",
|
|
@@ -61,8 +63,8 @@ function fontWarnings(fonts) {
|
|
|
61
63
|
}));
|
|
62
64
|
}
|
|
63
65
|
async function inspectAssets(document, options, signal, onImageProgress) {
|
|
64
|
-
const manifest = require_asset_manifest.buildAssetManifest(document
|
|
65
|
-
const uniqueImages = [...groupByUrl(require_asset_references.findImageReferences(document
|
|
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]) => ({
|
|
66
68
|
url,
|
|
67
69
|
crossOrigin: references[0].crossOrigin
|
|
68
70
|
}));
|
|
@@ -98,7 +100,7 @@ async function replaceMissingImages(document, missingImages, options, signal) {
|
|
|
98
100
|
})), signal, imageCheckOptions(options))).map((failure) => failure.url));
|
|
99
101
|
const stillMissing = [];
|
|
100
102
|
const warnings = [];
|
|
101
|
-
const groups = groupByUrl(require_asset_references.findImageReferences(document
|
|
103
|
+
const groups = groupByUrl(require_asset_references.findImageReferences(require_page_state.documentObjects(document)));
|
|
102
104
|
for (const image of missingImages) {
|
|
103
105
|
const replacement = replacements.get(image.url);
|
|
104
106
|
if (replacement === void 0 || brokenReplacements.has(replacement)) {
|
|
@@ -122,7 +124,7 @@ async function prepareAssetsForLoad(input, options, signal, isAllowedUrl, onImag
|
|
|
122
124
|
const document = cloneDocument(input);
|
|
123
125
|
const { resolveUrl } = options;
|
|
124
126
|
if (resolveUrl) await rewriteUrls(document, async (url) => resolveUrl(url));
|
|
125
|
-
require_content_limits.refuseUnsafeImageUrls(document
|
|
127
|
+
require_content_limits.refuseUnsafeImageUrls(require_page_state.documentObjects(document), isAllowedUrl);
|
|
126
128
|
const report = await inspectAssets(document, options, signal, onImageProgress);
|
|
127
129
|
if (options.requireFonts && report.unavailableFonts.length > 0) {
|
|
128
130
|
const families = report.unavailableFonts.map((font) => font.family).join(", ");
|
|
@@ -176,7 +178,7 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
|
|
|
176
178
|
}
|
|
177
179
|
return uploading;
|
|
178
180
|
});
|
|
179
|
-
document.assets = require_asset_manifest.buildAssetManifest(document
|
|
181
|
+
document.assets = require_asset_manifest.buildAssetManifest(require_page_state.documentObjects(document));
|
|
180
182
|
return {
|
|
181
183
|
document,
|
|
182
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" | "IMAGE_NOT_EMBEDDED" | "FONT_NOT_EMBEDDED" | "TEXT_ON_PATH_APPROXIMATED";
|
|
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;
|
|
@@ -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" | "IMAGE_NOT_EMBEDDED" | "FONT_NOT_EMBEDDED" | "TEXT_ON_PATH_APPROXIMATED";
|
|
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;
|
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
@@ -8,7 +9,8 @@ import { findMissingImages, isCrossOriginUrl, isPortableUrl } from "./image-chec
|
|
|
8
9
|
function imageCheckOptions(options) {
|
|
9
10
|
return {
|
|
10
11
|
timeoutMs: options.imageTimeout,
|
|
11
|
-
concurrency: options.maxConcurrentImages
|
|
12
|
+
concurrency: options.maxConcurrentImages,
|
|
13
|
+
maxPixels: options.maxImagePixels
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
function cloneDocument(document) {
|
|
@@ -33,7 +35,7 @@ function pointTo(references, url) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
async function rewriteUrls(document, rewrite) {
|
|
36
|
-
const groups = groupByUrl(findImageReferences(document
|
|
38
|
+
const groups = groupByUrl(findImageReferences(documentObjects(document)));
|
|
37
39
|
await Promise.all([...groups].map(async ([url, references]) => {
|
|
38
40
|
const next = await rewrite(url, references);
|
|
39
41
|
if (next !== url) pointTo(references, next);
|
|
@@ -41,7 +43,7 @@ async function rewriteUrls(document, rewrite) {
|
|
|
41
43
|
}
|
|
42
44
|
function crossOriginWarnings(document) {
|
|
43
45
|
const warnings = [];
|
|
44
|
-
for (const [url, references] of groupByUrl(findImageReferences(document
|
|
46
|
+
for (const [url, references] of groupByUrl(findImageReferences(documentObjects(document)))) {
|
|
45
47
|
if (!isCrossOriginUrl(url) || references.every((reference) => reference.crossOrigin)) continue;
|
|
46
48
|
warnings.push({
|
|
47
49
|
code: "IMAGE_CROSS_ORIGIN",
|
|
@@ -61,8 +63,8 @@ function fontWarnings(fonts) {
|
|
|
61
63
|
}));
|
|
62
64
|
}
|
|
63
65
|
async function inspectAssets(document, options, signal, onImageProgress) {
|
|
64
|
-
const manifest = buildAssetManifest(document
|
|
65
|
-
const uniqueImages = [...groupByUrl(findImageReferences(document
|
|
66
|
+
const manifest = buildAssetManifest(documentObjects(document));
|
|
67
|
+
const uniqueImages = [...groupByUrl(findImageReferences(documentObjects(document)).filter((reference) => !isEmbeddedUrl(reference.url)))].map(([url, references]) => ({
|
|
66
68
|
url,
|
|
67
69
|
crossOrigin: references[0].crossOrigin
|
|
68
70
|
}));
|
|
@@ -98,7 +100,7 @@ async function replaceMissingImages(document, missingImages, options, signal) {
|
|
|
98
100
|
})), signal, imageCheckOptions(options))).map((failure) => failure.url));
|
|
99
101
|
const stillMissing = [];
|
|
100
102
|
const warnings = [];
|
|
101
|
-
const groups = groupByUrl(findImageReferences(document
|
|
103
|
+
const groups = groupByUrl(findImageReferences(documentObjects(document)));
|
|
102
104
|
for (const image of missingImages) {
|
|
103
105
|
const replacement = replacements.get(image.url);
|
|
104
106
|
if (replacement === void 0 || brokenReplacements.has(replacement)) {
|
|
@@ -122,7 +124,7 @@ async function prepareAssetsForLoad(input, options, signal, isAllowedUrl, onImag
|
|
|
122
124
|
const document = cloneDocument(input);
|
|
123
125
|
const { resolveUrl } = options;
|
|
124
126
|
if (resolveUrl) await rewriteUrls(document, async (url) => resolveUrl(url));
|
|
125
|
-
refuseUnsafeImageUrls(document
|
|
127
|
+
refuseUnsafeImageUrls(documentObjects(document), isAllowedUrl);
|
|
126
128
|
const report = await inspectAssets(document, options, signal, onImageProgress);
|
|
127
129
|
if (options.requireFonts && report.unavailableFonts.length > 0) {
|
|
128
130
|
const families = report.unavailableFonts.map((font) => font.family).join(", ");
|
|
@@ -176,7 +178,7 @@ async function prepareAssetsForSave(document, options, uploadedUrls) {
|
|
|
176
178
|
}
|
|
177
179
|
return uploading;
|
|
178
180
|
});
|
|
179
|
-
document.assets = buildAssetManifest(document
|
|
181
|
+
document.assets = buildAssetManifest(documentObjects(document));
|
|
180
182
|
return {
|
|
181
183
|
document,
|
|
182
184
|
warnings
|
|
@@ -73,7 +73,7 @@ async function explainFailure(image, signal) {
|
|
|
73
73
|
}
|
|
74
74
|
return failure(url, "NETWORK", "could not be reached. Check the URL and the network connection.");
|
|
75
75
|
}
|
|
76
|
-
async function checkImage(image, signal, timeoutMs) {
|
|
76
|
+
async function checkImage(image, signal, timeoutMs, maxPixels) {
|
|
77
77
|
const attempt = new AbortController();
|
|
78
78
|
let timedOut = false;
|
|
79
79
|
const stop = () => attempt.abort();
|
|
@@ -84,10 +84,11 @@ async function checkImage(image, signal, timeoutMs) {
|
|
|
84
84
|
}, timeoutMs) : void 0;
|
|
85
85
|
try {
|
|
86
86
|
if (signal.aborted) attempt.abort();
|
|
87
|
-
await fabric.util.loadImage(image.url, {
|
|
87
|
+
const element = await fabric.util.loadImage(image.url, {
|
|
88
88
|
signal: attempt.signal,
|
|
89
89
|
crossOrigin: image.crossOrigin ?? null
|
|
90
90
|
});
|
|
91
|
+
if (element.naturalWidth * element.naturalHeight > maxPixels) return failure(image.url, "TOO_LARGE", `is ${element.naturalWidth} × ${element.naturalHeight} pixels, more than the limit of ${maxPixels}`);
|
|
91
92
|
return;
|
|
92
93
|
} catch {
|
|
93
94
|
if (timedOut) return failure(image.url, "TIMEOUT", `did not finish loading within ${timeoutMs} ms`, { timeoutMs });
|
|
@@ -104,7 +105,7 @@ async function findMissingImages(images, signal, options = {}) {
|
|
|
104
105
|
let done = 0;
|
|
105
106
|
options.onProgress?.(0, images.length);
|
|
106
107
|
return (await require_concurrency.mapWithConcurrency(images, options.concurrency ?? 6, async (image) => {
|
|
107
|
-
const result = await checkImage(image, signal, timeoutMs);
|
|
108
|
+
const result = await checkImage(image, signal, timeoutMs, options.maxPixels ?? Infinity);
|
|
108
109
|
done += 1;
|
|
109
110
|
options.onProgress?.(done, images.length);
|
|
110
111
|
return result;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* a server on another site that sends no CORS headers looks the same as a
|
|
5
5
|
* network failure unless the image asked for CORS.
|
|
6
6
|
*/
|
|
7
|
-
export type ImageFailureReason = "NOT_FOUND" | "HTTP_ERROR" | "CORS" | "NETWORK" | "TIMEOUT" | "DECODE" | "ABORTED";
|
|
7
|
+
export type ImageFailureReason = "NOT_FOUND" | "HTTP_ERROR" | "CORS" | "NETWORK" | "TIMEOUT" | "DECODE" | "ABORTED" | "TOO_LARGE";
|
|
8
8
|
export interface ImageLoadFailure {
|
|
9
9
|
url: string;
|
|
10
10
|
reason: ImageFailureReason;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* a server on another site that sends no CORS headers looks the same as a
|
|
5
5
|
* network failure unless the image asked for CORS.
|
|
6
6
|
*/
|
|
7
|
-
export type ImageFailureReason = "NOT_FOUND" | "HTTP_ERROR" | "CORS" | "NETWORK" | "TIMEOUT" | "DECODE" | "ABORTED";
|
|
7
|
+
export type ImageFailureReason = "NOT_FOUND" | "HTTP_ERROR" | "CORS" | "NETWORK" | "TIMEOUT" | "DECODE" | "ABORTED" | "TOO_LARGE";
|
|
8
8
|
export interface ImageLoadFailure {
|
|
9
9
|
url: string;
|
|
10
10
|
reason: ImageFailureReason;
|
|
@@ -73,7 +73,7 @@ async function explainFailure(image, signal) {
|
|
|
73
73
|
}
|
|
74
74
|
return failure(url, "NETWORK", "could not be reached. Check the URL and the network connection.");
|
|
75
75
|
}
|
|
76
|
-
async function checkImage(image, signal, timeoutMs) {
|
|
76
|
+
async function checkImage(image, signal, timeoutMs, maxPixels) {
|
|
77
77
|
const attempt = new AbortController();
|
|
78
78
|
let timedOut = false;
|
|
79
79
|
const stop = () => attempt.abort();
|
|
@@ -84,10 +84,11 @@ async function checkImage(image, signal, timeoutMs) {
|
|
|
84
84
|
}, timeoutMs) : void 0;
|
|
85
85
|
try {
|
|
86
86
|
if (signal.aborted) attempt.abort();
|
|
87
|
-
await util.loadImage(image.url, {
|
|
87
|
+
const element = await util.loadImage(image.url, {
|
|
88
88
|
signal: attempt.signal,
|
|
89
89
|
crossOrigin: image.crossOrigin ?? null
|
|
90
90
|
});
|
|
91
|
+
if (element.naturalWidth * element.naturalHeight > maxPixels) return failure(image.url, "TOO_LARGE", `is ${element.naturalWidth} × ${element.naturalHeight} pixels, more than the limit of ${maxPixels}`);
|
|
91
92
|
return;
|
|
92
93
|
} catch {
|
|
93
94
|
if (timedOut) return failure(image.url, "TIMEOUT", `did not finish loading within ${timeoutMs} ms`, { timeoutMs });
|
|
@@ -104,7 +105,7 @@ async function findMissingImages(images, signal, options = {}) {
|
|
|
104
105
|
let done = 0;
|
|
105
106
|
options.onProgress?.(0, images.length);
|
|
106
107
|
return (await mapWithConcurrency(images, options.concurrency ?? 6, async (image) => {
|
|
107
|
-
const result = await checkImage(image, signal, timeoutMs);
|
|
108
|
+
const result = await checkImage(image, signal, timeoutMs, options.maxPixels ?? Infinity);
|
|
108
109
|
done += 1;
|
|
109
110
|
options.onProgress?.(done, images.length);
|
|
110
111
|
return result;
|
|
@@ -111,7 +111,12 @@ function createClipboard(engine, options = {}) {
|
|
|
111
111
|
const distance = offset * pastes;
|
|
112
112
|
const serialized = content.objects.map((object) => moved(withoutIds(object), distance));
|
|
113
113
|
refuseUnknownTypes(serialized);
|
|
114
|
+
const startedIn = target.getDocumentInfo().session;
|
|
114
115
|
const created = await require_fabric_adapter.createObjects(clone(serialized));
|
|
116
|
+
if (target.getDocumentInfo().session !== startedIn) {
|
|
117
|
+
created.forEach((object) => object.dispose?.());
|
|
118
|
+
throw new require_errors.DocumentEngineError("DOCUMENT_CHANGED", "The paste was dropped because another document was opened while it ran");
|
|
119
|
+
}
|
|
115
120
|
target.transaction(created.length === 1 ? "Paste" : `Paste ${created.length} objects`, () => {
|
|
116
121
|
target.canvas.add(...created);
|
|
117
122
|
});
|
|
@@ -111,7 +111,12 @@ function createClipboard(engine, options = {}) {
|
|
|
111
111
|
const distance = offset * pastes;
|
|
112
112
|
const serialized = content.objects.map((object) => moved(withoutIds(object), distance));
|
|
113
113
|
refuseUnknownTypes(serialized);
|
|
114
|
+
const startedIn = target.getDocumentInfo().session;
|
|
114
115
|
const created = await createObjects(clone(serialized));
|
|
116
|
+
if (target.getDocumentInfo().session !== startedIn) {
|
|
117
|
+
created.forEach((object) => object.dispose?.());
|
|
118
|
+
throw new DocumentEngineError("DOCUMENT_CHANGED", "The paste was dropped because another document was opened while it ran");
|
|
119
|
+
}
|
|
115
120
|
target.transaction(created.length === 1 ? "Paste" : `Paste ${created.length} objects`, () => {
|
|
116
121
|
target.canvas.add(...created);
|
|
117
122
|
});
|
|
@@ -11,7 +11,16 @@ export interface SerializedFabricObject {
|
|
|
11
11
|
export interface DocumentCanvas {
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
|
+
/** A color, gradient or pattern behind everything. */
|
|
14
15
|
background?: unknown;
|
|
16
|
+
/** A Fabric image drawn behind every object. */
|
|
17
|
+
backgroundImage?: SerializedFabricObject;
|
|
18
|
+
/** A color, gradient or pattern drawn over every object. */
|
|
19
|
+
overlay?: unknown;
|
|
20
|
+
/** A Fabric image drawn over every object. */
|
|
21
|
+
overlayImage?: SerializedFabricObject;
|
|
22
|
+
/** A Fabric object that clips the whole canvas. */
|
|
23
|
+
clipPath?: SerializedFabricObject;
|
|
15
24
|
}
|
|
16
25
|
export interface FabricDocument {
|
|
17
26
|
schemaVersion: number;
|
|
@@ -30,5 +39,11 @@ export interface DocumentInfo {
|
|
|
30
39
|
createdAt: string;
|
|
31
40
|
updatedAt: string;
|
|
32
41
|
metadata: Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Goes up each time the canvas shows a different document: a load, a new
|
|
44
|
+
* document or a restore. Async work started under one session must not
|
|
45
|
+
* change the canvas in another. Set by `getDocumentInfo()`.
|
|
46
|
+
*/
|
|
47
|
+
session?: number;
|
|
33
48
|
}
|
|
34
49
|
//#endregion
|
|
@@ -11,7 +11,16 @@ export interface SerializedFabricObject {
|
|
|
11
11
|
export interface DocumentCanvas {
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
|
+
/** A color, gradient or pattern behind everything. */
|
|
14
15
|
background?: unknown;
|
|
16
|
+
/** A Fabric image drawn behind every object. */
|
|
17
|
+
backgroundImage?: SerializedFabricObject;
|
|
18
|
+
/** A color, gradient or pattern drawn over every object. */
|
|
19
|
+
overlay?: unknown;
|
|
20
|
+
/** A Fabric image drawn over every object. */
|
|
21
|
+
overlayImage?: SerializedFabricObject;
|
|
22
|
+
/** A Fabric object that clips the whole canvas. */
|
|
23
|
+
clipPath?: SerializedFabricObject;
|
|
15
24
|
}
|
|
16
25
|
export interface FabricDocument {
|
|
17
26
|
schemaVersion: number;
|
|
@@ -30,5 +39,11 @@ export interface DocumentInfo {
|
|
|
30
39
|
createdAt: string;
|
|
31
40
|
updatedAt: string;
|
|
32
41
|
metadata: Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Goes up each time the canvas shows a different document: a load, a new
|
|
44
|
+
* document or a restore. Async work started under one session must not
|
|
45
|
+
* change the canvas in another. Set by `getDocumentInfo()`.
|
|
46
|
+
*/
|
|
47
|
+
session?: number;
|
|
33
48
|
}
|
|
34
49
|
//#endregion
|
|
@@ -67,6 +67,14 @@ function validateDocument(value) {
|
|
|
67
67
|
else {
|
|
68
68
|
if (!isPositiveNumber(value.canvas.width)) issues.push(invalid("canvas.width", "must be a positive number"));
|
|
69
69
|
if (!isPositiveNumber(value.canvas.height)) issues.push(invalid("canvas.height", "must be a positive number"));
|
|
70
|
+
for (const field of [
|
|
71
|
+
"backgroundImage",
|
|
72
|
+
"overlayImage",
|
|
73
|
+
"clipPath"
|
|
74
|
+
]) {
|
|
75
|
+
const pageObject = value.canvas[field];
|
|
76
|
+
if (pageObject !== void 0 && pageObject !== null) validateObjectList([pageObject], `canvas.${field}`, issues);
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
validateObjectList(value.objects, "objects", issues);
|
|
72
80
|
return issues;
|
|
@@ -67,6 +67,14 @@ function validateDocument(value) {
|
|
|
67
67
|
else {
|
|
68
68
|
if (!isPositiveNumber(value.canvas.width)) issues.push(invalid("canvas.width", "must be a positive number"));
|
|
69
69
|
if (!isPositiveNumber(value.canvas.height)) issues.push(invalid("canvas.height", "must be a positive number"));
|
|
70
|
+
for (const field of [
|
|
71
|
+
"backgroundImage",
|
|
72
|
+
"overlayImage",
|
|
73
|
+
"clipPath"
|
|
74
|
+
]) {
|
|
75
|
+
const pageObject = value.canvas[field];
|
|
76
|
+
if (pageObject !== void 0 && pageObject !== null) validateObjectList([pageObject], `canvas.${field}`, issues);
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
validateObjectList(value.objects, "objects", issues);
|
|
72
80
|
return issues;
|