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.
Files changed (146) hide show
  1. package/README.md +70 -1
  2. package/README.zh-CN.md +71 -1
  3. package/dist/assets/asset-pipeline.cjs +10 -8
  4. package/dist/assets/asset-pipeline.d.cts +1 -1
  5. package/dist/assets/asset-pipeline.d.ts +1 -1
  6. package/dist/assets/asset-pipeline.js +10 -8
  7. package/dist/assets/image-check.cjs +4 -3
  8. package/dist/assets/image-check.d.cts +1 -1
  9. package/dist/assets/image-check.d.ts +1 -1
  10. package/dist/assets/image-check.js +4 -3
  11. package/dist/commands/clipboard.cjs +5 -0
  12. package/dist/commands/clipboard.js +5 -0
  13. package/dist/commands/text-edit.cjs +223 -0
  14. package/dist/commands/text-edit.d.cts +23 -0
  15. package/dist/commands/text-edit.d.ts +23 -0
  16. package/dist/commands/text-edit.js +218 -0
  17. package/dist/document/document-format.d.cts +15 -0
  18. package/dist/document/document-format.d.ts +15 -0
  19. package/dist/document/validate-document.cjs +8 -0
  20. package/dist/document/validate-document.js +8 -0
  21. package/dist/engine/create-document-engine.cjs +115 -21
  22. package/dist/engine/create-document-engine.d.cts +24 -6
  23. package/dist/engine/create-document-engine.d.ts +24 -6
  24. package/dist/engine/create-document-engine.js +116 -22
  25. package/dist/engine/errors.d.cts +1 -1
  26. package/dist/engine/errors.d.ts +1 -1
  27. package/dist/export/export-options.cjs +3 -0
  28. package/dist/export/export-options.d.cts +6 -0
  29. package/dist/export/export-options.d.ts +6 -0
  30. package/dist/export/export-options.js +3 -0
  31. package/dist/export/preflight-export.d.cts +1 -1
  32. package/dist/export/preflight-export.d.ts +1 -1
  33. package/dist/export/render-export.cjs +42 -4
  34. package/dist/export/render-export.js +43 -5
  35. package/dist/export/svg/clip-masks.cjs +39 -0
  36. package/dist/export/svg/clip-masks.js +37 -0
  37. package/dist/export/svg/embed-assets.cjs +1 -0
  38. package/dist/export/svg/embed-assets.js +1 -0
  39. package/dist/export/svg/raster-object.cjs +82 -0
  40. package/dist/export/svg/raster-object.js +81 -0
  41. package/dist/{pdf → export/svg}/text-decorations.cjs +3 -3
  42. package/dist/{pdf → export/svg}/text-decorations.js +3 -3
  43. package/dist/export/svg/text-on-path.cjs +0 -10
  44. package/dist/export/svg/text-on-path.js +1 -10
  45. package/dist/fabric/fabric-adapter.cjs +5 -3
  46. package/dist/fabric/fabric-adapter.js +5 -3
  47. package/dist/fabric/object-registry.cjs +7 -1
  48. package/dist/fabric/object-registry.js +7 -1
  49. package/dist/fabric/page-state.cjs +69 -0
  50. package/dist/fabric/page-state.js +64 -0
  51. package/dist/filters/filter-worker.cjs +281 -0
  52. package/dist/filters/filter-worker.d.cts +26 -0
  53. package/dist/filters/filter-worker.d.ts +26 -0
  54. package/dist/filters/filter-worker.js +281 -0
  55. package/dist/filters/pipeline.cjs +91 -0
  56. package/dist/filters/pipeline.d.cts +35 -0
  57. package/dist/filters/pipeline.d.ts +35 -0
  58. package/dist/filters/pipeline.js +87 -0
  59. package/dist/filters/worker-entry.cjs +78 -0
  60. package/dist/filters/worker-entry.d.cts +29 -0
  61. package/dist/filters/worker-entry.d.ts +29 -0
  62. package/dist/filters/worker-entry.js +78 -0
  63. package/dist/filters.cjs +9 -0
  64. package/dist/filters.d.cts +3 -0
  65. package/dist/filters.d.ts +3 -0
  66. package/dist/filters.js +3 -0
  67. package/dist/history/apply-state.cjs +19 -2
  68. package/dist/history/apply-state.js +19 -2
  69. package/dist/history/create-history.cjs +53 -13
  70. package/dist/history/create-history.d.cts +8 -0
  71. package/dist/history/create-history.d.ts +8 -0
  72. package/dist/history/create-history.js +53 -13
  73. package/dist/history/snapshot.cjs +10 -1
  74. package/dist/history/snapshot.js +10 -2
  75. package/dist/import/svg-import.cjs +102 -0
  76. package/dist/import/svg-import.d.cts +1 -0
  77. package/dist/import/svg-import.d.ts +1 -0
  78. package/dist/import/svg-import.js +103 -1
  79. package/dist/index.cjs +7 -0
  80. package/dist/index.d.cts +4 -3
  81. package/dist/index.d.ts +4 -3
  82. package/dist/index.js +2 -1
  83. package/dist/migrations/migrate-document.cjs +2 -1
  84. package/dist/migrations/migrate-document.js +2 -1
  85. package/dist/pdf/export-pdf.cjs +8 -67
  86. package/dist/pdf/export-pdf.js +8 -67
  87. package/dist/performance/batch.cjs +31 -0
  88. package/dist/performance/batch.d.cts +4 -0
  89. package/dist/performance/batch.d.ts +4 -0
  90. package/dist/performance/batch.js +31 -0
  91. package/dist/performance/dirty-regions.cjs +378 -0
  92. package/dist/performance/dirty-regions.d.cts +23 -0
  93. package/dist/performance/dirty-regions.d.ts +23 -0
  94. package/dist/performance/dirty-regions.js +378 -0
  95. package/dist/performance/monitor.cjs +60 -0
  96. package/dist/performance/monitor.d.cts +20 -0
  97. package/dist/performance/monitor.d.ts +20 -0
  98. package/dist/performance/monitor.js +60 -0
  99. package/dist/performance/spatial-index.cjs +78 -0
  100. package/dist/performance/spatial-index.d.cts +21 -0
  101. package/dist/performance/spatial-index.d.ts +21 -0
  102. package/dist/performance/spatial-index.js +78 -0
  103. package/dist/performance.cjs +9 -0
  104. package/dist/performance.d.cts +5 -0
  105. package/dist/performance.d.ts +5 -0
  106. package/dist/performance.js +5 -0
  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/text/bounded-textbox.cjs +306 -0
  116. package/dist/text/bounded-textbox.d.cts +53 -0
  117. package/dist/text/bounded-textbox.d.ts +53 -0
  118. package/dist/text/bounded-textbox.js +306 -0
  119. package/dist/text/mobile-input.cjs +146 -0
  120. package/dist/text/mobile-input.d.cts +19 -0
  121. package/dist/text/mobile-input.d.ts +19 -0
  122. package/dist/text/mobile-input.js +145 -0
  123. package/dist/text/register.cjs +17 -0
  124. package/dist/text/register.d.cts +5 -0
  125. package/dist/text/register.d.ts +5 -0
  126. package/dist/text/register.js +16 -0
  127. package/dist/text/shaping.cjs +125 -0
  128. package/dist/text/shaping.d.cts +28 -0
  129. package/dist/text/shaping.d.ts +28 -0
  130. package/dist/text/shaping.js +121 -0
  131. package/dist/text/vertical-orientation.cjs +192 -0
  132. package/dist/text/vertical-orientation.d.cts +10 -0
  133. package/dist/text/vertical-orientation.d.ts +10 -0
  134. package/dist/text/vertical-orientation.js +190 -0
  135. package/dist/text/vertical-text.cjs +336 -0
  136. package/dist/text/vertical-text.d.cts +54 -0
  137. package/dist/text/vertical-text.d.ts +54 -0
  138. package/dist/text/vertical-text.js +336 -0
  139. package/dist/text.cjs +21 -0
  140. package/dist/text.d.cts +7 -0
  141. package/dist/text.d.ts +7 -0
  142. package/dist/text.js +7 -0
  143. package/dist/util/concurrency.cjs +16 -3
  144. package/dist/util/concurrency.js +16 -3
  145. package/package.json +31 -1
  146. 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(value) {
18
- return JSON.parse(JSON.stringify(value), (key, nested) => dangerousKeys.has(key) ? void 0 : nested);
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 document = withoutDangerousKeys(input);
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(value) {
18
- return JSON.parse(JSON.stringify(value), (key, nested) => dangerousKeys.has(key) ? void 0 : nested);
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 document = withoutDangerousKeys(input);
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