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
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
const require_asset_references = require("../assets/asset-references.cjs");
|
|
2
2
|
const require_errors = require("../engine/errors.cjs");
|
|
3
3
|
//#region src/security/content-limits.ts
|
|
4
|
+
const DEFAULT_MAX_CANVAS_SIDE = 16384;
|
|
5
|
+
const DEFAULT_MAX_CANVAS_PIXELS = 67108864;
|
|
6
|
+
const DEFAULT_MAX_IMAGE_PIXELS = 67108864;
|
|
7
|
+
const DEFAULT_MAX_DOCUMENT_LENGTH = 1e8;
|
|
8
|
+
/**
|
|
9
|
+
* Why a drawing surface of this size is refused, or undefined when it fits.
|
|
10
|
+
* Pure arithmetic, run before any canvas is created: a canvas over the
|
|
11
|
+
* browser's limit fails silently or crashes the tab.
|
|
12
|
+
*/
|
|
13
|
+
function canvasSizeProblem(width, height, limits = {}) {
|
|
14
|
+
const maxSide = limits.maxCanvasSide ?? 16384;
|
|
15
|
+
const maxPixels = limits.maxCanvasPixels ?? 67108864;
|
|
16
|
+
const w = Math.ceil(width);
|
|
17
|
+
const h = Math.ceil(height);
|
|
18
|
+
if (w > maxSide || h > maxSide) return `${w} × ${h} pixels is larger than the ${maxSide}-pixel limit per side`;
|
|
19
|
+
if (w * h > maxPixels) return `${w} × ${h} pixels is ${w * h} pixels, more than the limit of ${maxPixels}`;
|
|
20
|
+
}
|
|
4
21
|
const dangerousKeys = /* @__PURE__ */ new Set([
|
|
5
22
|
"__proto__",
|
|
6
23
|
"constructor",
|
|
@@ -14,8 +31,18 @@ function isSafeImageUrl(url) {
|
|
|
14
31
|
if (scheme === "data") return trimmed.startsWith("data:image/");
|
|
15
32
|
return false;
|
|
16
33
|
}
|
|
17
|
-
function withoutDangerousKeys(
|
|
18
|
-
return JSON.parse(
|
|
34
|
+
function withoutDangerousKeys(json) {
|
|
35
|
+
return JSON.parse(json, (key, nested) => dangerousKeys.has(key) ? void 0 : nested);
|
|
36
|
+
}
|
|
37
|
+
function pageSize(document) {
|
|
38
|
+
const canvas = document.canvas;
|
|
39
|
+
return typeof canvas === "object" && canvas !== null ? {
|
|
40
|
+
width: canvas.width,
|
|
41
|
+
height: canvas.height
|
|
42
|
+
} : {
|
|
43
|
+
width: document.width,
|
|
44
|
+
height: document.height
|
|
45
|
+
};
|
|
19
46
|
}
|
|
20
47
|
function deepestNesting(value) {
|
|
21
48
|
let deepest = 0;
|
|
@@ -56,8 +83,16 @@ function secureDocument(input, limits = {}) {
|
|
|
56
83
|
if (typeof input !== "object" || input === null) return input;
|
|
57
84
|
const maxDepth = limits.maxDepth ?? 100;
|
|
58
85
|
const maxObjects = limits.maxObjects ?? 5e4;
|
|
59
|
-
const
|
|
86
|
+
const json = JSON.stringify(input);
|
|
87
|
+
const maxLength = limits.maxDocumentLength ?? 1e8;
|
|
88
|
+
if (json.length > maxLength) refuseWhenUnsafe([unsafe("", `the document is ${json.length} characters of JSON, the limit is ${maxLength}`)]);
|
|
89
|
+
const document = withoutDangerousKeys(json);
|
|
60
90
|
const issues = [];
|
|
91
|
+
const { width, height } = pageSize(document);
|
|
92
|
+
if (typeof width === "number" && typeof height === "number") {
|
|
93
|
+
const problem = canvasSizeProblem(width, height, limits);
|
|
94
|
+
if (problem) issues.push(unsafe("canvas", `the page is ${problem}`));
|
|
95
|
+
}
|
|
61
96
|
const depth = deepestNesting(document);
|
|
62
97
|
if (depth > maxDepth) issues.push(unsafe("", `the document is nested ${depth} levels deep, the limit is ${maxDepth}`));
|
|
63
98
|
if (Array.isArray(document.objects) && issues.length === 0) {
|
|
@@ -81,6 +116,11 @@ function refuseUnsafeImageUrls(objects, isAllowedUrl = isSafeImageUrl) {
|
|
|
81
116
|
refuseWhenUnsafe(issues);
|
|
82
117
|
}
|
|
83
118
|
//#endregion
|
|
119
|
+
exports.DEFAULT_MAX_CANVAS_PIXELS = DEFAULT_MAX_CANVAS_PIXELS;
|
|
120
|
+
exports.DEFAULT_MAX_CANVAS_SIDE = DEFAULT_MAX_CANVAS_SIDE;
|
|
121
|
+
exports.DEFAULT_MAX_DOCUMENT_LENGTH = DEFAULT_MAX_DOCUMENT_LENGTH;
|
|
122
|
+
exports.DEFAULT_MAX_IMAGE_PIXELS = DEFAULT_MAX_IMAGE_PIXELS;
|
|
123
|
+
exports.canvasSizeProblem = canvasSizeProblem;
|
|
84
124
|
exports.isSafeImageUrl = isSafeImageUrl;
|
|
85
125
|
exports.refuseUnsafeImageUrls = refuseUnsafeImageUrls;
|
|
86
126
|
exports.secureDocument = secureDocument;
|
|
@@ -4,6 +4,14 @@ export interface ContentLimits {
|
|
|
4
4
|
maxObjects?: number;
|
|
5
5
|
maxDepth?: number;
|
|
6
6
|
isAllowedUrl?: (url: string) => boolean;
|
|
7
|
+
/** Longest side of the page or of an export, in pixels. Default 16,384. */
|
|
8
|
+
maxCanvasSide?: number;
|
|
9
|
+
/** Largest page or export area, in pixels. Default 67,108,864 (8,192 × 8,192, what iOS 18 Safari can draw). */
|
|
10
|
+
maxCanvasPixels?: number;
|
|
11
|
+
/** Largest decoded image, in pixels. Default 67,108,864. */
|
|
12
|
+
maxImagePixels?: number;
|
|
13
|
+
/** Longest document, in characters of JSON. Default 100,000,000. */
|
|
14
|
+
maxDocumentLength?: number;
|
|
7
15
|
}
|
|
8
16
|
export declare function isSafeImageUrl(url: string): boolean;
|
|
9
17
|
export declare function secureDocument(input: unknown, limits?: ContentLimits): unknown;
|
|
@@ -4,6 +4,14 @@ export interface ContentLimits {
|
|
|
4
4
|
maxObjects?: number;
|
|
5
5
|
maxDepth?: number;
|
|
6
6
|
isAllowedUrl?: (url: string) => boolean;
|
|
7
|
+
/** Longest side of the page or of an export, in pixels. Default 16,384. */
|
|
8
|
+
maxCanvasSide?: number;
|
|
9
|
+
/** Largest page or export area, in pixels. Default 67,108,864 (8,192 × 8,192, what iOS 18 Safari can draw). */
|
|
10
|
+
maxCanvasPixels?: number;
|
|
11
|
+
/** Largest decoded image, in pixels. Default 67,108,864. */
|
|
12
|
+
maxImagePixels?: number;
|
|
13
|
+
/** Longest document, in characters of JSON. Default 100,000,000. */
|
|
14
|
+
maxDocumentLength?: number;
|
|
7
15
|
}
|
|
8
16
|
export declare function isSafeImageUrl(url: string): boolean;
|
|
9
17
|
export declare function secureDocument(input: unknown, limits?: ContentLimits): unknown;
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { findImageReferences } from "../assets/asset-references.js";
|
|
2
2
|
import { DocumentEngineError } from "../engine/errors.js";
|
|
3
3
|
//#region src/security/content-limits.ts
|
|
4
|
+
const DEFAULT_MAX_CANVAS_SIDE = 16384;
|
|
5
|
+
const DEFAULT_MAX_CANVAS_PIXELS = 67108864;
|
|
6
|
+
const DEFAULT_MAX_IMAGE_PIXELS = 67108864;
|
|
7
|
+
const DEFAULT_MAX_DOCUMENT_LENGTH = 1e8;
|
|
8
|
+
/**
|
|
9
|
+
* Why a drawing surface of this size is refused, or undefined when it fits.
|
|
10
|
+
* Pure arithmetic, run before any canvas is created: a canvas over the
|
|
11
|
+
* browser's limit fails silently or crashes the tab.
|
|
12
|
+
*/
|
|
13
|
+
function canvasSizeProblem(width, height, limits = {}) {
|
|
14
|
+
const maxSide = limits.maxCanvasSide ?? 16384;
|
|
15
|
+
const maxPixels = limits.maxCanvasPixels ?? 67108864;
|
|
16
|
+
const w = Math.ceil(width);
|
|
17
|
+
const h = Math.ceil(height);
|
|
18
|
+
if (w > maxSide || h > maxSide) return `${w} × ${h} pixels is larger than the ${maxSide}-pixel limit per side`;
|
|
19
|
+
if (w * h > maxPixels) return `${w} × ${h} pixels is ${w * h} pixels, more than the limit of ${maxPixels}`;
|
|
20
|
+
}
|
|
4
21
|
const dangerousKeys = /* @__PURE__ */ new Set([
|
|
5
22
|
"__proto__",
|
|
6
23
|
"constructor",
|
|
@@ -14,8 +31,18 @@ function isSafeImageUrl(url) {
|
|
|
14
31
|
if (scheme === "data") return trimmed.startsWith("data:image/");
|
|
15
32
|
return false;
|
|
16
33
|
}
|
|
17
|
-
function withoutDangerousKeys(
|
|
18
|
-
return JSON.parse(
|
|
34
|
+
function withoutDangerousKeys(json) {
|
|
35
|
+
return JSON.parse(json, (key, nested) => dangerousKeys.has(key) ? void 0 : nested);
|
|
36
|
+
}
|
|
37
|
+
function pageSize(document) {
|
|
38
|
+
const canvas = document.canvas;
|
|
39
|
+
return typeof canvas === "object" && canvas !== null ? {
|
|
40
|
+
width: canvas.width,
|
|
41
|
+
height: canvas.height
|
|
42
|
+
} : {
|
|
43
|
+
width: document.width,
|
|
44
|
+
height: document.height
|
|
45
|
+
};
|
|
19
46
|
}
|
|
20
47
|
function deepestNesting(value) {
|
|
21
48
|
let deepest = 0;
|
|
@@ -56,8 +83,16 @@ function secureDocument(input, limits = {}) {
|
|
|
56
83
|
if (typeof input !== "object" || input === null) return input;
|
|
57
84
|
const maxDepth = limits.maxDepth ?? 100;
|
|
58
85
|
const maxObjects = limits.maxObjects ?? 5e4;
|
|
59
|
-
const
|
|
86
|
+
const json = JSON.stringify(input);
|
|
87
|
+
const maxLength = limits.maxDocumentLength ?? 1e8;
|
|
88
|
+
if (json.length > maxLength) refuseWhenUnsafe([unsafe("", `the document is ${json.length} characters of JSON, the limit is ${maxLength}`)]);
|
|
89
|
+
const document = withoutDangerousKeys(json);
|
|
60
90
|
const issues = [];
|
|
91
|
+
const { width, height } = pageSize(document);
|
|
92
|
+
if (typeof width === "number" && typeof height === "number") {
|
|
93
|
+
const problem = canvasSizeProblem(width, height, limits);
|
|
94
|
+
if (problem) issues.push(unsafe("canvas", `the page is ${problem}`));
|
|
95
|
+
}
|
|
61
96
|
const depth = deepestNesting(document);
|
|
62
97
|
if (depth > maxDepth) issues.push(unsafe("", `the document is nested ${depth} levels deep, the limit is ${maxDepth}`));
|
|
63
98
|
if (Array.isArray(document.objects) && issues.length === 0) {
|
|
@@ -81,4 +116,4 @@ function refuseUnsafeImageUrls(objects, isAllowedUrl = isSafeImageUrl) {
|
|
|
81
116
|
refuseWhenUnsafe(issues);
|
|
82
117
|
}
|
|
83
118
|
//#endregion
|
|
84
|
-
export { isSafeImageUrl, refuseUnsafeImageUrls, secureDocument };
|
|
119
|
+
export { DEFAULT_MAX_CANVAS_PIXELS, DEFAULT_MAX_CANVAS_SIDE, DEFAULT_MAX_DOCUMENT_LENGTH, DEFAULT_MAX_IMAGE_PIXELS, canvasSizeProblem, isSafeImageUrl, refuseUnsafeImageUrls, secureDocument };
|
|
@@ -17,10 +17,23 @@ async function mapWithConcurrency(items, limit, work) {
|
|
|
17
17
|
await Promise.all(Array.from({ length: workers }, runWorker));
|
|
18
18
|
return results;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Lets the browser paint, handle input and run other queued work, such as a
|
|
22
|
+
* framework's re-render after a progress update, before the next piece of
|
|
23
|
+
* work. A message-channel task runs after the tasks already queued and,
|
|
24
|
+
* unlike `setTimeout`, is not delayed by timer clamping. `scheduler.yield()`
|
|
25
|
+
* is not used: it resumes ahead of other queued tasks, so a UI waiting to
|
|
26
|
+
* show progress would not render until the work had finished.
|
|
27
|
+
*/
|
|
21
28
|
function yieldToEventLoop() {
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
if (typeof MessageChannel === "function") return new Promise((resolve) => {
|
|
30
|
+
const channel = new MessageChannel();
|
|
31
|
+
channel.port1.onmessage = () => {
|
|
32
|
+
channel.port1.close();
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
channel.port2.postMessage(void 0);
|
|
36
|
+
});
|
|
24
37
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
25
38
|
}
|
|
26
39
|
//#endregion
|
package/dist/util/concurrency.js
CHANGED
|
@@ -17,10 +17,23 @@ async function mapWithConcurrency(items, limit, work) {
|
|
|
17
17
|
await Promise.all(Array.from({ length: workers }, runWorker));
|
|
18
18
|
return results;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Lets the browser paint, handle input and run other queued work, such as a
|
|
22
|
+
* framework's re-render after a progress update, before the next piece of
|
|
23
|
+
* work. A message-channel task runs after the tasks already queued and,
|
|
24
|
+
* unlike `setTimeout`, is not delayed by timer clamping. `scheduler.yield()`
|
|
25
|
+
* is not used: it resumes ahead of other queued tasks, so a UI waiting to
|
|
26
|
+
* show progress would not render until the work had finished.
|
|
27
|
+
*/
|
|
21
28
|
function yieldToEventLoop() {
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
if (typeof MessageChannel === "function") return new Promise((resolve) => {
|
|
30
|
+
const channel = new MessageChannel();
|
|
31
|
+
channel.port1.onmessage = () => {
|
|
32
|
+
channel.port1.close();
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
channel.port2.postMessage(void 0);
|
|
36
|
+
});
|
|
24
37
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
25
38
|
}
|
|
26
39
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fabricjs-document-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Fabric.js save and load, undo and redo, and autosave for an existing canvas: stable object ids, custom objects, copy and paste, SVG import, PNG, SVG and PDF export, and recovery.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fabricjs",
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
"properties": {
|
|
19
19
|
"width": { "type": "number", "exclusiveMinimum": 0 },
|
|
20
20
|
"height": { "type": "number", "exclusiveMinimum": 0 },
|
|
21
|
-
"background": {}
|
|
21
|
+
"background": {},
|
|
22
|
+
"backgroundImage": { "$ref": "#/$defs/fabricObject" },
|
|
23
|
+
"overlay": {},
|
|
24
|
+
"overlayImage": { "$ref": "#/$defs/fabricObject" },
|
|
25
|
+
"clipPath": { "$ref": "#/$defs/fabricObject" }
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"objects": { "type": "array", "items": { "$ref": "#/$defs/fabricObject" } },
|