fabricjs-document-engine 1.1.0 → 1.3.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 +70 -1
- package/README.zh-CN.md +71 -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/commands/text-edit.cjs +223 -0
- package/dist/commands/text-edit.d.cts +23 -0
- package/dist/commands/text-edit.d.ts +23 -0
- package/dist/commands/text-edit.js +218 -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/object-registry.cjs +7 -1
- package/dist/fabric/object-registry.js +7 -1
- package/dist/fabric/page-state.cjs +69 -0
- package/dist/fabric/page-state.js +64 -0
- package/dist/filters/filter-worker.cjs +281 -0
- package/dist/filters/filter-worker.d.cts +26 -0
- package/dist/filters/filter-worker.d.ts +26 -0
- package/dist/filters/filter-worker.js +281 -0
- package/dist/filters/pipeline.cjs +91 -0
- package/dist/filters/pipeline.d.cts +35 -0
- package/dist/filters/pipeline.d.ts +35 -0
- package/dist/filters/pipeline.js +87 -0
- package/dist/filters/worker-entry.cjs +78 -0
- package/dist/filters/worker-entry.d.cts +29 -0
- package/dist/filters/worker-entry.d.ts +29 -0
- package/dist/filters/worker-entry.js +78 -0
- package/dist/filters.cjs +9 -0
- package/dist/filters.d.cts +3 -0
- package/dist/filters.d.ts +3 -0
- package/dist/filters.js +3 -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/import/svg-import.cjs +102 -0
- package/dist/import/svg-import.d.cts +1 -0
- package/dist/import/svg-import.d.ts +1 -0
- package/dist/import/svg-import.js +103 -1
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- 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/performance/batch.cjs +31 -0
- package/dist/performance/batch.d.cts +4 -0
- package/dist/performance/batch.d.ts +4 -0
- package/dist/performance/batch.js +31 -0
- package/dist/performance/dirty-regions.cjs +378 -0
- package/dist/performance/dirty-regions.d.cts +23 -0
- package/dist/performance/dirty-regions.d.ts +23 -0
- package/dist/performance/dirty-regions.js +378 -0
- package/dist/performance/monitor.cjs +60 -0
- package/dist/performance/monitor.d.cts +20 -0
- package/dist/performance/monitor.d.ts +20 -0
- package/dist/performance/monitor.js +60 -0
- package/dist/performance/spatial-index.cjs +78 -0
- package/dist/performance/spatial-index.d.cts +21 -0
- package/dist/performance/spatial-index.d.ts +21 -0
- package/dist/performance/spatial-index.js +78 -0
- package/dist/performance.cjs +9 -0
- package/dist/performance.d.cts +5 -0
- package/dist/performance.d.ts +5 -0
- package/dist/performance.js +5 -0
- 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/text/bounded-textbox.cjs +306 -0
- package/dist/text/bounded-textbox.d.cts +53 -0
- package/dist/text/bounded-textbox.d.ts +53 -0
- package/dist/text/bounded-textbox.js +306 -0
- package/dist/text/mobile-input.cjs +146 -0
- package/dist/text/mobile-input.d.cts +19 -0
- package/dist/text/mobile-input.d.ts +19 -0
- package/dist/text/mobile-input.js +145 -0
- package/dist/text/register.cjs +17 -0
- package/dist/text/register.d.cts +5 -0
- package/dist/text/register.d.ts +5 -0
- package/dist/text/register.js +16 -0
- package/dist/text/shaping.cjs +125 -0
- package/dist/text/shaping.d.cts +28 -0
- package/dist/text/shaping.d.ts +28 -0
- package/dist/text/shaping.js +121 -0
- package/dist/text/vertical-orientation.cjs +192 -0
- package/dist/text/vertical-orientation.d.cts +10 -0
- package/dist/text/vertical-orientation.d.ts +10 -0
- package/dist/text/vertical-orientation.js +190 -0
- package/dist/text/vertical-text.cjs +336 -0
- package/dist/text/vertical-text.d.cts +54 -0
- package/dist/text/vertical-text.d.ts +54 -0
- package/dist/text/vertical-text.js +336 -0
- package/dist/text.cjs +21 -0
- package/dist/text.d.cts +7 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.js +7 -0
- package/dist/util/concurrency.cjs +16 -3
- package/dist/util/concurrency.js +16 -3
- package/package.json +31 -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 };
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const require_shaping = require("./shaping.cjs");
|
|
2
|
+
let fabric = require("fabric");
|
|
3
|
+
//#region src/text/bounded-textbox.ts
|
|
4
|
+
const BOUNDED_PROPERTIES = [
|
|
5
|
+
"breakWords",
|
|
6
|
+
"maxHeight",
|
|
7
|
+
"overflow",
|
|
8
|
+
"fit",
|
|
9
|
+
"minFontSize",
|
|
10
|
+
"shaping"
|
|
11
|
+
];
|
|
12
|
+
const ELLIPSIS = "…";
|
|
13
|
+
let clipCounter = 0;
|
|
14
|
+
function internals(text) {
|
|
15
|
+
return text;
|
|
16
|
+
}
|
|
17
|
+
var BoundedTextbox = class BoundedTextbox extends fabric.Textbox {
|
|
18
|
+
static getDefaults() {
|
|
19
|
+
return {
|
|
20
|
+
...super.getDefaults(),
|
|
21
|
+
...BoundedTextbox.boundedDefaults
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
constructor(text, options = {}) {
|
|
25
|
+
super(text, {
|
|
26
|
+
...BoundedTextbox.boundedDefaults,
|
|
27
|
+
...options
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
getBaseFontSize() {
|
|
31
|
+
return this.baseFontSize;
|
|
32
|
+
}
|
|
33
|
+
initDimensions() {
|
|
34
|
+
if (!internals(this).initialized) return;
|
|
35
|
+
this.midWordBreaks ?? (this.midWordBreaks = /* @__PURE__ */ new Set());
|
|
36
|
+
this.overflowCache = void 0;
|
|
37
|
+
this.fitScale = 1;
|
|
38
|
+
super.initDimensions();
|
|
39
|
+
if (this.fit === "shrink" && this.maxHeight !== void 0 && this.calcTextHeight() > this.maxHeight) this.shrinkToFit();
|
|
40
|
+
if (this.maxHeight !== void 0 && this.overflow !== "visible") this.height = Math.min(this.height, this.maxHeight);
|
|
41
|
+
}
|
|
42
|
+
shrinkToFit() {
|
|
43
|
+
const base = this.baseFontSize;
|
|
44
|
+
const maxHeight = this.maxHeight;
|
|
45
|
+
let low = Math.max(1, Math.ceil(Math.min(this.minFontSize, base) * 2));
|
|
46
|
+
let high = Math.floor(base * 2) - 1;
|
|
47
|
+
let best = low;
|
|
48
|
+
const layOut = (halfPoints) => {
|
|
49
|
+
this.fitScale = halfPoints / 2 / base;
|
|
50
|
+
fabric.Textbox.prototype.initDimensions.call(this);
|
|
51
|
+
return this.calcTextHeight();
|
|
52
|
+
};
|
|
53
|
+
while (low <= high) {
|
|
54
|
+
const middle = low + high >> 1;
|
|
55
|
+
if (layOut(middle) <= maxHeight) {
|
|
56
|
+
best = middle;
|
|
57
|
+
low = middle + 1;
|
|
58
|
+
} else high = middle - 1;
|
|
59
|
+
}
|
|
60
|
+
if (this.fitScale !== best / 2 / base) layOut(best);
|
|
61
|
+
}
|
|
62
|
+
getValueOfPropertyAt(lineIndex, charIndex, property) {
|
|
63
|
+
const value = super.getValueOfPropertyAt(lineIndex, charIndex, property);
|
|
64
|
+
if (property === "fontSize" && this.fitScale !== 1 && this.hasOwnFontSize(lineIndex, charIndex)) return value * this.fitScale;
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
getCompleteStyleDeclaration(lineIndex, charIndex) {
|
|
68
|
+
const style = super.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
69
|
+
if (this.fitScale !== 1 && this.hasOwnFontSize(lineIndex, charIndex)) style.fontSize *= this.fitScale;
|
|
70
|
+
return style;
|
|
71
|
+
}
|
|
72
|
+
hasOwnFontSize(lineIndex, charIndex) {
|
|
73
|
+
return internals(this)._getStyleDeclaration(lineIndex, charIndex)?.fontSize !== void 0;
|
|
74
|
+
}
|
|
75
|
+
_wrapText(lines, desiredWidth) {
|
|
76
|
+
const self = internals(this);
|
|
77
|
+
this.midWordBreaks = /* @__PURE__ */ new Set();
|
|
78
|
+
self.isWrapping = true;
|
|
79
|
+
const data = self.getGraphemeDataForRender(lines);
|
|
80
|
+
const wrapped = [];
|
|
81
|
+
for (let index = 0; index < data.wordsData.length; index += 1) {
|
|
82
|
+
this.lineBase = wrapped.length;
|
|
83
|
+
wrapped.push(...this._wrapLine(index, desiredWidth, data));
|
|
84
|
+
}
|
|
85
|
+
self.isWrapping = false;
|
|
86
|
+
return wrapped;
|
|
87
|
+
}
|
|
88
|
+
_wrapLine(lineIndex, desiredWidth, data, reservedSpace = 0) {
|
|
89
|
+
if (this.breakWords === "never" || this.splitByGrapheme) return super._wrapLine(lineIndex, desiredWidth, data, reservedSpace);
|
|
90
|
+
const self = internals(this);
|
|
91
|
+
const { wordsData } = data;
|
|
92
|
+
const additionalSpace = self._getWidthOfCharSpacing();
|
|
93
|
+
const available = desiredWidth - reservedSpace;
|
|
94
|
+
const lines = [];
|
|
95
|
+
let line = [];
|
|
96
|
+
let lineWidth = 0;
|
|
97
|
+
let offset = 0;
|
|
98
|
+
let infixWidth = 0;
|
|
99
|
+
let lineJustStarted = true;
|
|
100
|
+
let widestGrapheme = 0;
|
|
101
|
+
const words = wordsData[lineIndex] ?? [];
|
|
102
|
+
for (const { word, width: wordWidth } of words) {
|
|
103
|
+
const wordStart = offset;
|
|
104
|
+
offset += word.length;
|
|
105
|
+
if (wordWidth - additionalSpace > available && word.length > 1) {
|
|
106
|
+
if (!lineJustStarted) lines.push(line);
|
|
107
|
+
line = [];
|
|
108
|
+
lineWidth = 0;
|
|
109
|
+
let previous;
|
|
110
|
+
for (let index = 0; index < word.length; index += 1) {
|
|
111
|
+
const grapheme = word[index];
|
|
112
|
+
const graphemeWidth = self._getGraphemeBox(grapheme, lineIndex, wordStart + index, previous, true).kernedWidth;
|
|
113
|
+
widestGrapheme = Math.max(widestGrapheme, graphemeWidth - additionalSpace);
|
|
114
|
+
if (line.length > 0 && lineWidth + graphemeWidth - additionalSpace > available) {
|
|
115
|
+
this.midWordBreaks.add(this.lineBase + lines.length);
|
|
116
|
+
lines.push(line);
|
|
117
|
+
line = [];
|
|
118
|
+
lineWidth = 0;
|
|
119
|
+
}
|
|
120
|
+
line.push(grapheme);
|
|
121
|
+
lineWidth += graphemeWidth;
|
|
122
|
+
previous = grapheme;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
if (word.length === 1) widestGrapheme = Math.max(widestGrapheme, wordWidth - additionalSpace);
|
|
126
|
+
lineWidth += infixWidth + wordWidth - additionalSpace;
|
|
127
|
+
if (lineWidth > available && !lineJustStarted) {
|
|
128
|
+
lines.push(line);
|
|
129
|
+
line = [];
|
|
130
|
+
lineWidth = wordWidth;
|
|
131
|
+
lineJustStarted = true;
|
|
132
|
+
} else lineWidth += additionalSpace;
|
|
133
|
+
if (!lineJustStarted) line.push(" ");
|
|
134
|
+
line = line.concat(word);
|
|
135
|
+
}
|
|
136
|
+
infixWidth = self._measureWord([" "], lineIndex, offset);
|
|
137
|
+
offset += 1;
|
|
138
|
+
lineJustStarted = false;
|
|
139
|
+
}
|
|
140
|
+
if (words.length > 0) lines.push(line);
|
|
141
|
+
self.dynamicMinWidth = Math.max(self.dynamicMinWidth, widestGrapheme);
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
_measureLine(lineIndex) {
|
|
145
|
+
const measured = super._measureLine(lineIndex);
|
|
146
|
+
return this.shaping ? require_shaping.shapeLine(this, lineIndex, measured) : measured;
|
|
147
|
+
}
|
|
148
|
+
_measureWord(word, lineIndex, charOffset = 0) {
|
|
149
|
+
const width = super._measureWord(word, lineIndex, charOffset);
|
|
150
|
+
return this.shaping ? require_shaping.shapeWord(this, word, lineIndex, charOffset, width) : width;
|
|
151
|
+
}
|
|
152
|
+
missingNewlineOffset(lineIndex, skipWrapping) {
|
|
153
|
+
if (!skipWrapping && this.midWordBreaks?.has(lineIndex)) return 0;
|
|
154
|
+
return super.missingNewlineOffset(lineIndex, skipWrapping);
|
|
155
|
+
}
|
|
156
|
+
overflowLayout() {
|
|
157
|
+
if (this.overflowCache !== void 0) return this.overflowCache;
|
|
158
|
+
const maxHeight = this.maxHeight;
|
|
159
|
+
if (maxHeight === void 0 || this.overflow === "visible") return this.overflowCache = null;
|
|
160
|
+
const lineCount = internals(this)._textLines.length;
|
|
161
|
+
let visibleLines = 0;
|
|
162
|
+
let top = 0;
|
|
163
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
164
|
+
const lineHeight = this.getHeightOfLine(index);
|
|
165
|
+
if (top + lineHeight / this.lineHeight > maxHeight + .5) break;
|
|
166
|
+
visibleLines = index + 1;
|
|
167
|
+
top += lineHeight;
|
|
168
|
+
}
|
|
169
|
+
const clipped = visibleLines < lineCount;
|
|
170
|
+
const layout = {
|
|
171
|
+
visibleLines,
|
|
172
|
+
clipped,
|
|
173
|
+
visibleHeight: top
|
|
174
|
+
};
|
|
175
|
+
if (clipped && visibleLines > 0 && this.overflow === "ellipsis" && !this.isEditing && this.direction !== "rtl") layout.ellipsis = this.placeEllipsis(visibleLines - 1, top - this.getHeightOfLine(visibleLines - 1));
|
|
176
|
+
return this.overflowCache = layout;
|
|
177
|
+
}
|
|
178
|
+
placeEllipsis(line, lineTop) {
|
|
179
|
+
const self = internals(this);
|
|
180
|
+
this.getLineWidth(line);
|
|
181
|
+
const bounds = self.__charBounds[line] ?? [];
|
|
182
|
+
const graphemes = self._textLines[line] ?? [];
|
|
183
|
+
const lineLeft = self._getLineLeftOffset(line);
|
|
184
|
+
const styleAt = (index) => this.getCompleteStyleDeclaration(line, Math.max(0, Math.min(index, graphemes.length - 1)));
|
|
185
|
+
const ellipsisWidth = self._measureChar(ELLIPSIS, styleAt(graphemes.length - 1)).width;
|
|
186
|
+
let low = 0;
|
|
187
|
+
let high = graphemes.length;
|
|
188
|
+
while (low < high) {
|
|
189
|
+
const middle = low + high + 1 >> 1;
|
|
190
|
+
if (lineLeft + (bounds[middle]?.left ?? 0) + ellipsisWidth <= this.width) low = middle;
|
|
191
|
+
else high = middle - 1;
|
|
192
|
+
}
|
|
193
|
+
let cut = low;
|
|
194
|
+
while (cut > 0 && /\s/.test(graphemes[cut - 1])) cut -= 1;
|
|
195
|
+
const lineHeight = this.getHeightOfLine(line) / this.lineHeight;
|
|
196
|
+
return {
|
|
197
|
+
line,
|
|
198
|
+
cut,
|
|
199
|
+
x: self._getLeftOffset() + lineLeft + (bounds[cut]?.left ?? 0),
|
|
200
|
+
baseline: self._getTopOffset() + lineTop + lineHeight * (1 - self._fontSizeFraction),
|
|
201
|
+
style: styleAt(cut - 1)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
clipRects(layout) {
|
|
205
|
+
const self = internals(this);
|
|
206
|
+
const left = self._getLeftOffset();
|
|
207
|
+
const top = self._getTopOffset();
|
|
208
|
+
const wide = this.width + this.fontSize * 2;
|
|
209
|
+
const x = left - this.fontSize;
|
|
210
|
+
if (!layout.ellipsis) {
|
|
211
|
+
const bottom = this.overflow === "clip" || this.isEditing ? this.maxHeight : layout.visibleHeight;
|
|
212
|
+
return [[
|
|
213
|
+
x,
|
|
214
|
+
top - this.fontSize,
|
|
215
|
+
wide,
|
|
216
|
+
bottom + this.fontSize
|
|
217
|
+
]];
|
|
218
|
+
}
|
|
219
|
+
const { line, x: cutX } = layout.ellipsis;
|
|
220
|
+
const lineTop = layout.visibleHeight - this.getHeightOfLine(line);
|
|
221
|
+
const rects = [];
|
|
222
|
+
if (lineTop > 0) rects.push([
|
|
223
|
+
x,
|
|
224
|
+
top - this.fontSize,
|
|
225
|
+
wide,
|
|
226
|
+
lineTop + this.fontSize
|
|
227
|
+
]);
|
|
228
|
+
rects.push([
|
|
229
|
+
x,
|
|
230
|
+
top + lineTop,
|
|
231
|
+
cutX - x,
|
|
232
|
+
this.getHeightOfLine(line)
|
|
233
|
+
]);
|
|
234
|
+
return rects;
|
|
235
|
+
}
|
|
236
|
+
_render(ctx) {
|
|
237
|
+
const layout = this.overflowLayout();
|
|
238
|
+
if (!layout || !layout.clipped && this.overflow !== "clip") {
|
|
239
|
+
super._render(ctx);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
ctx.save();
|
|
243
|
+
ctx.beginPath();
|
|
244
|
+
for (const [x, y, width, height] of this.clipRects(layout)) ctx.rect(x, y, width, height);
|
|
245
|
+
ctx.clip();
|
|
246
|
+
super._render(ctx);
|
|
247
|
+
ctx.restore();
|
|
248
|
+
const ellipsis = layout.ellipsis;
|
|
249
|
+
if (!ellipsis) return;
|
|
250
|
+
ctx.save();
|
|
251
|
+
ctx.font = internals(this)._getFontDeclaration(ellipsis.style);
|
|
252
|
+
ctx.textBaseline = "alphabetic";
|
|
253
|
+
ctx.textAlign = "left";
|
|
254
|
+
ctx.fillStyle = typeof ellipsis.style.fill === "string" ? ellipsis.style.fill : typeof this.fill === "string" ? this.fill : "#000";
|
|
255
|
+
ctx.fillText(ELLIPSIS, ellipsis.x, ellipsis.baseline - (ellipsis.style.deltaY ?? 0));
|
|
256
|
+
ctx.restore();
|
|
257
|
+
}
|
|
258
|
+
_toSVG() {
|
|
259
|
+
const markup = super._toSVG();
|
|
260
|
+
const layout = this.overflowLayout();
|
|
261
|
+
if (!layout || !layout.clipped && this.overflow !== "clip") return markup;
|
|
262
|
+
clipCounter += 1;
|
|
263
|
+
const id = `fde-bounded-text-${clipCounter}`;
|
|
264
|
+
const result = [
|
|
265
|
+
`<clipPath id="${id}">${this.clipRects(layout).map(([x, y, width, height]) => `<rect x="${x}" y="${y}" width="${width}" height="${height}" />`).join("")}</clipPath>\n<g clip-path="url(#${id})">\n`,
|
|
266
|
+
...markup,
|
|
267
|
+
"</g>\n"
|
|
268
|
+
];
|
|
269
|
+
const ellipsis = layout.ellipsis;
|
|
270
|
+
if (ellipsis) {
|
|
271
|
+
const style = ellipsis.style;
|
|
272
|
+
const fill = typeof style.fill === "string" ? style.fill : typeof this.fill === "string" ? this.fill : "#000";
|
|
273
|
+
const family = String(style.fontFamily ?? this.fontFamily).replace(/"/g, "'");
|
|
274
|
+
result.push(`<text xml:space="preserve" x="${ellipsis.x}" y="${ellipsis.baseline - (style.deltaY ?? 0)}" font-family="${family}" font-size="${style.fontSize}" font-style="${style.fontStyle ?? "normal"}" font-weight="${style.fontWeight ?? "normal"}" fill="${fill}">${ELLIPSIS}</text>\n`);
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
toObject(propertiesToInclude = []) {
|
|
279
|
+
const object = super.toObject([...BOUNDED_PROPERTIES, ...propertiesToInclude]);
|
|
280
|
+
if ("fontSize" in object) object.fontSize = this.baseFontSize;
|
|
281
|
+
return object;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
BoundedTextbox.type = "BoundedTextbox";
|
|
285
|
+
BoundedTextbox.textLayoutProperties = [...fabric.Textbox.textLayoutProperties, ...BOUNDED_PROPERTIES];
|
|
286
|
+
BoundedTextbox.cacheProperties = [...fabric.Textbox.cacheProperties, ...BOUNDED_PROPERTIES];
|
|
287
|
+
BoundedTextbox.boundedDefaults = {
|
|
288
|
+
breakWords: "anywhere",
|
|
289
|
+
maxHeight: void 0,
|
|
290
|
+
overflow: "visible",
|
|
291
|
+
fit: "none",
|
|
292
|
+
minFontSize: 6,
|
|
293
|
+
shaping: false
|
|
294
|
+
};
|
|
295
|
+
Object.defineProperty(BoundedTextbox.prototype, "fontSize", {
|
|
296
|
+
configurable: true,
|
|
297
|
+
enumerable: true,
|
|
298
|
+
get() {
|
|
299
|
+
return this.baseFontSize * (this.fitScale ?? 1);
|
|
300
|
+
},
|
|
301
|
+
set(value) {
|
|
302
|
+
this.baseFontSize = value;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
//#endregion
|
|
306
|
+
exports.BoundedTextbox = BoundedTextbox;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TextStyleDeclaration, Textbox } from "fabric";
|
|
2
|
+
//#region src/text/bounded-textbox.d.ts
|
|
3
|
+
export type BreakWords = "anywhere" | "never";
|
|
4
|
+
export type TextOverflow = "visible" | "clip" | "ellipsis";
|
|
5
|
+
export type TextFit = "none" | "shrink";
|
|
6
|
+
export interface BoundedTextboxProps {
|
|
7
|
+
breakWords: BreakWords;
|
|
8
|
+
maxHeight: number | undefined;
|
|
9
|
+
overflow: TextOverflow;
|
|
10
|
+
fit: TextFit;
|
|
11
|
+
minFontSize: number;
|
|
12
|
+
shaping: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class BoundedTextbox extends Textbox {
|
|
15
|
+
static override type: string;
|
|
16
|
+
static override textLayoutProperties: string[];
|
|
17
|
+
static override cacheProperties: string[];
|
|
18
|
+
static boundedDefaults: BoundedTextboxProps;
|
|
19
|
+
static override getDefaults(): Record<string, unknown>;
|
|
20
|
+
breakWords: BreakWords;
|
|
21
|
+
maxHeight: number | undefined;
|
|
22
|
+
overflow: TextOverflow;
|
|
23
|
+
fit: TextFit;
|
|
24
|
+
minFontSize: number;
|
|
25
|
+
shaping: boolean;
|
|
26
|
+
fitScale: number;
|
|
27
|
+
private baseFontSize;
|
|
28
|
+
private midWordBreaks;
|
|
29
|
+
private lineBase;
|
|
30
|
+
private overflowCache;
|
|
31
|
+
constructor(text: string, options?: Partial<BoundedTextboxProps> & ConstructorParameters<typeof Textbox>[1]);
|
|
32
|
+
getBaseFontSize(): number;
|
|
33
|
+
override initDimensions(): void;
|
|
34
|
+
private shrinkToFit;
|
|
35
|
+
override getValueOfPropertyAt<T extends keyof TextStyleDeclaration>(lineIndex: number, charIndex: number, property: T): this[T];
|
|
36
|
+
override getCompleteStyleDeclaration(lineIndex: number, charIndex: number): ReturnType<Textbox["getCompleteStyleDeclaration"]>;
|
|
37
|
+
private hasOwnFontSize;
|
|
38
|
+
override _wrapText(lines: string[], desiredWidth: number): string[][];
|
|
39
|
+
override _wrapLine(lineIndex: number, desiredWidth: number, data: never, reservedSpace?: number): string[][];
|
|
40
|
+
override _measureLine(lineIndex: number): {
|
|
41
|
+
width: number;
|
|
42
|
+
numOfSpaces: number;
|
|
43
|
+
};
|
|
44
|
+
override _measureWord(word: string[], lineIndex: number, charOffset?: number): number;
|
|
45
|
+
override missingNewlineOffset(lineIndex: number, skipWrapping?: boolean): 0 | 1;
|
|
46
|
+
private overflowLayout;
|
|
47
|
+
private placeEllipsis;
|
|
48
|
+
private clipRects;
|
|
49
|
+
override _render(ctx: CanvasRenderingContext2D): void;
|
|
50
|
+
override _toSVG(): string[];
|
|
51
|
+
override toObject(propertiesToInclude?: PropertyKey[]): any;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { TextStyleDeclaration, Textbox } from "fabric";
|
|
2
|
+
//#region src/text/bounded-textbox.d.ts
|
|
3
|
+
export type BreakWords = "anywhere" | "never";
|
|
4
|
+
export type TextOverflow = "visible" | "clip" | "ellipsis";
|
|
5
|
+
export type TextFit = "none" | "shrink";
|
|
6
|
+
export interface BoundedTextboxProps {
|
|
7
|
+
breakWords: BreakWords;
|
|
8
|
+
maxHeight: number | undefined;
|
|
9
|
+
overflow: TextOverflow;
|
|
10
|
+
fit: TextFit;
|
|
11
|
+
minFontSize: number;
|
|
12
|
+
shaping: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare class BoundedTextbox extends Textbox {
|
|
15
|
+
static override type: string;
|
|
16
|
+
static override textLayoutProperties: string[];
|
|
17
|
+
static override cacheProperties: string[];
|
|
18
|
+
static boundedDefaults: BoundedTextboxProps;
|
|
19
|
+
static override getDefaults(): Record<string, unknown>;
|
|
20
|
+
breakWords: BreakWords;
|
|
21
|
+
maxHeight: number | undefined;
|
|
22
|
+
overflow: TextOverflow;
|
|
23
|
+
fit: TextFit;
|
|
24
|
+
minFontSize: number;
|
|
25
|
+
shaping: boolean;
|
|
26
|
+
fitScale: number;
|
|
27
|
+
private baseFontSize;
|
|
28
|
+
private midWordBreaks;
|
|
29
|
+
private lineBase;
|
|
30
|
+
private overflowCache;
|
|
31
|
+
constructor(text: string, options?: Partial<BoundedTextboxProps> & ConstructorParameters<typeof Textbox>[1]);
|
|
32
|
+
getBaseFontSize(): number;
|
|
33
|
+
override initDimensions(): void;
|
|
34
|
+
private shrinkToFit;
|
|
35
|
+
override getValueOfPropertyAt<T extends keyof TextStyleDeclaration>(lineIndex: number, charIndex: number, property: T): this[T];
|
|
36
|
+
override getCompleteStyleDeclaration(lineIndex: number, charIndex: number): ReturnType<Textbox["getCompleteStyleDeclaration"]>;
|
|
37
|
+
private hasOwnFontSize;
|
|
38
|
+
override _wrapText(lines: string[], desiredWidth: number): string[][];
|
|
39
|
+
override _wrapLine(lineIndex: number, desiredWidth: number, data: never, reservedSpace?: number): string[][];
|
|
40
|
+
override _measureLine(lineIndex: number): {
|
|
41
|
+
width: number;
|
|
42
|
+
numOfSpaces: number;
|
|
43
|
+
};
|
|
44
|
+
override _measureWord(word: string[], lineIndex: number, charOffset?: number): number;
|
|
45
|
+
override missingNewlineOffset(lineIndex: number, skipWrapping?: boolean): 0 | 1;
|
|
46
|
+
private overflowLayout;
|
|
47
|
+
private placeEllipsis;
|
|
48
|
+
private clipRects;
|
|
49
|
+
override _render(ctx: CanvasRenderingContext2D): void;
|
|
50
|
+
override _toSVG(): string[];
|
|
51
|
+
override toObject(propertiesToInclude?: PropertyKey[]): any;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|