fabricjs-document-engine 1.0.1 → 1.1.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 (89) hide show
  1. package/README.md +277 -36
  2. package/README.zh-CN.md +697 -0
  3. package/dist/assets/asset-manifest.d.cts +3 -0
  4. package/dist/assets/asset-manifest.d.ts +3 -0
  5. package/dist/assets/asset-pipeline.cjs +22 -10
  6. package/dist/assets/asset-pipeline.d.cts +5 -1
  7. package/dist/assets/asset-pipeline.d.ts +5 -1
  8. package/dist/assets/asset-pipeline.js +22 -10
  9. package/dist/assets/font-check.cjs +9 -1
  10. package/dist/assets/font-check.js +9 -1
  11. package/dist/assets/image-check.cjs +88 -6
  12. package/dist/assets/image-check.d.cts +17 -0
  13. package/dist/assets/image-check.d.ts +17 -0
  14. package/dist/assets/image-check.js +88 -7
  15. package/dist/commands/clipboard.cjs +136 -0
  16. package/dist/commands/clipboard.d.cts +47 -0
  17. package/dist/commands/clipboard.d.ts +47 -0
  18. package/dist/commands/clipboard.js +135 -0
  19. package/dist/commands/layers.cjs +97 -0
  20. package/dist/commands/layers.d.cts +39 -0
  21. package/dist/commands/layers.d.ts +39 -0
  22. package/dist/commands/layers.js +92 -0
  23. package/dist/engine/create-document-engine.cjs +85 -14
  24. package/dist/engine/create-document-engine.d.cts +20 -1
  25. package/dist/engine/create-document-engine.d.ts +20 -1
  26. package/dist/engine/create-document-engine.js +86 -15
  27. package/dist/engine/errors.d.cts +1 -1
  28. package/dist/engine/errors.d.ts +1 -1
  29. package/dist/export/batch-render.cjs +172 -0
  30. package/dist/export/batch-render.d.cts +43 -0
  31. package/dist/export/batch-render.d.ts +43 -0
  32. package/dist/export/batch-render.js +171 -0
  33. package/dist/export/export-options.cjs +18 -1
  34. package/dist/export/export-options.d.cts +23 -0
  35. package/dist/export/export-options.d.ts +23 -0
  36. package/dist/export/export-options.js +18 -2
  37. package/dist/export/preflight-export.cjs +11 -2
  38. package/dist/export/preflight-export.d.cts +1 -1
  39. package/dist/export/preflight-export.d.ts +1 -1
  40. package/dist/export/preflight-export.js +11 -2
  41. package/dist/export/render-export.cjs +18 -3
  42. package/dist/export/render-export.js +18 -4
  43. package/dist/export/svg/embed-assets.cjs +249 -0
  44. package/dist/export/svg/embed-assets.js +249 -0
  45. package/dist/export/svg/overrides.cjs +32 -0
  46. package/dist/export/svg/overrides.js +32 -0
  47. package/dist/export/svg/text-on-path.cjs +252 -0
  48. package/dist/export/svg/text-on-path.js +249 -0
  49. package/dist/fabric/fabric-adapter.cjs +46 -5
  50. package/dist/fabric/fabric-adapter.js +46 -5
  51. package/dist/fabric/object-registry.cjs +1 -1
  52. package/dist/fabric/object-registry.js +1 -1
  53. package/dist/import/svg-import.cjs +256 -0
  54. package/dist/import/svg-import.d.cts +50 -0
  55. package/dist/import/svg-import.d.ts +50 -0
  56. package/dist/import/svg-import.js +256 -0
  57. package/dist/index.cjs +12 -0
  58. package/dist/index.d.cts +9 -4
  59. package/dist/index.d.ts +9 -4
  60. package/dist/index.js +4 -1
  61. package/dist/pdf/export-pdf.cjs +440 -0
  62. package/dist/pdf/export-pdf.d.cts +73 -0
  63. package/dist/pdf/export-pdf.d.ts +73 -0
  64. package/dist/pdf/export-pdf.js +440 -0
  65. package/dist/pdf/fonts.cjs +115 -0
  66. package/dist/pdf/fonts.d.cts +12 -0
  67. package/dist/pdf/fonts.d.ts +12 -0
  68. package/dist/pdf/fonts.js +112 -0
  69. package/dist/pdf/page-layout.cjs +79 -0
  70. package/dist/pdf/page-layout.d.cts +51 -0
  71. package/dist/pdf/page-layout.d.ts +51 -0
  72. package/dist/pdf/page-layout.js +76 -0
  73. package/dist/pdf/text-decorations.cjs +108 -0
  74. package/dist/pdf/text-decorations.js +107 -0
  75. package/dist/pdf.cjs +7 -0
  76. package/dist/pdf.d.cts +4 -0
  77. package/dist/pdf.d.ts +4 -0
  78. package/dist/pdf.js +3 -0
  79. package/dist/react/use-layers.cjs +53 -0
  80. package/dist/react/use-layers.d.cts +10 -0
  81. package/dist/react/use-layers.d.ts +10 -0
  82. package/dist/react/use-layers.js +53 -0
  83. package/dist/react.cjs +2 -0
  84. package/dist/react.d.cts +3 -1
  85. package/dist/react.d.ts +3 -1
  86. package/dist/react.js +2 -1
  87. package/dist/util/concurrency.cjs +28 -0
  88. package/dist/util/concurrency.js +27 -0
  89. package/package.json +55 -14
@@ -0,0 +1,252 @@
1
+ const require_walk_objects = require("../../fabric/walk-objects.cjs");
2
+ const require_overrides = require("./overrides.cjs");
3
+ let fabric = require("fabric");
4
+ //#region src/export/svg/text-on-path.ts
5
+ function isTextOnPath(object) {
6
+ const candidate = object;
7
+ return Boolean(candidate.path) && Array.isArray(candidate._textLines) && typeof candidate.getCompleteStyleDeclaration === "function";
8
+ }
9
+ const DIGITS = 4;
10
+ function num(value) {
11
+ if (!Number.isFinite(value)) return "0";
12
+ const fixed = value.toFixed(DIGITS);
13
+ return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") || "0" : fixed;
14
+ }
15
+ function escapeXml(value) {
16
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
17
+ }
18
+ /** CSS values that could break out of a style attribute are dropped, as Fabric 7 does. */
19
+ function safeToken(value) {
20
+ if (value === void 0 || value === null) return void 0;
21
+ const text = String(value).trim();
22
+ if (text.length === 0 || /[;<>{}\\]|url\(|expression\(|javascript:|data:/i.test(text)) return void 0;
23
+ return text;
24
+ }
25
+ function isFiller(value) {
26
+ return typeof value === "object" && value !== null && typeof value.toSVG === "function";
27
+ }
28
+ /** `fill: rgb(...); fill-opacity: a` for a color, `url(#SVGID_id)` for a gradient or pattern. */
29
+ function paint(property, value) {
30
+ if (isFiller(value)) return `${property}: url(#SVGID_${value.id}); `;
31
+ const token = typeof value === "string" ? safeToken(value) : void 0;
32
+ if (token === void 0 || token === "none" || token === "transparent") return `${property}: none; `;
33
+ const color = new fabric.Color(token);
34
+ const alpha = color.getAlpha();
35
+ const rgb = color.toRgb();
36
+ return alpha === 1 ? `${property}: ${rgb}; ` : `${property}: ${rgb}; ${property}-opacity: ${num(alpha)}; `;
37
+ }
38
+ function strokeDetails(text, strokeWidth) {
39
+ let style = `stroke-width: ${num(strokeWidth)}; `;
40
+ if (text.strokeDashArray && text.strokeDashArray.length > 0) style += `stroke-dasharray: ${text.strokeDashArray.map(num).join(" ")}; `;
41
+ const cap = safeToken(text.strokeLineCap);
42
+ const join = safeToken(text.strokeLineJoin);
43
+ if (cap) style += `stroke-linecap: ${cap}; `;
44
+ if (join) style += `stroke-linejoin: ${join}; `;
45
+ if (text.strokeMiterLimit !== void 0) style += `stroke-miterlimit: ${num(text.strokeMiterLimit)}; `;
46
+ return style;
47
+ }
48
+ function fontStyle(style) {
49
+ const family = safeToken(String(style.fontFamily ?? "").replace(/"/g, "'"));
50
+ const weight = safeToken(style.fontWeight);
51
+ const slant = safeToken(style.fontStyle);
52
+ let css = "";
53
+ if (family) css += `font-family: ${family}; `;
54
+ css += `font-size: ${num(style.fontSize ?? 0)}px; `;
55
+ if (weight) css += `font-weight: ${weight}; `;
56
+ if (slant) css += `font-style: ${slant}; `;
57
+ return css;
58
+ }
59
+ function frame(box) {
60
+ const angle = box.angle ?? 0;
61
+ const cos = Math.cos(angle);
62
+ const sin = Math.sin(angle);
63
+ return `matrix(${num(cos)} ${num(sin)} ${num(-sin)} ${num(cos)} ${num(box.renderLeft ?? 0)} ${num(box.renderTop ?? 0)})`;
64
+ }
65
+ let measuringContext;
66
+ function measuring() {
67
+ if (measuringContext === void 0) measuringContext = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
68
+ return measuringContext;
69
+ }
70
+ const baselineShifts = /* @__PURE__ */ new Map();
71
+ /**
72
+ * How far below the path the alphabetic baseline sits when Fabric draws with
73
+ * `pathAlign` (the canvas `textBaseline`). SVG has no portable way to say
74
+ * this, so the shift is measured and written as a plain offset.
75
+ */
76
+ function baselineShift(font, pathAlign) {
77
+ const baseline = pathAlign === "center" ? "middle" : pathAlign === "ascender" ? "top" : pathAlign === "descender" ? "bottom" : void 0;
78
+ if (baseline === void 0) return 0;
79
+ const key = `${baseline}|${font}`;
80
+ const known = baselineShifts.get(key);
81
+ if (known !== void 0) return known;
82
+ const context = measuring();
83
+ let shift = 0;
84
+ if (context) {
85
+ context.font = font;
86
+ context.textBaseline = baseline;
87
+ shift = -context.measureText("M").alphabeticBaseline;
88
+ if (!Number.isFinite(shift)) shift = 0;
89
+ }
90
+ baselineShifts.set(key, shift);
91
+ return shift;
92
+ }
93
+ function guidePath(text) {
94
+ const path = text.path;
95
+ if (!path || !path.visible || path.opacity === 0) return "";
96
+ const pathData = fabric.util.joinPath(path.path, DIGITS);
97
+ const strokeWidth = path.strokeWidth ?? 1;
98
+ let style = paint("fill", path.fill);
99
+ if (path.fillRule === "evenodd") style += "fill-rule: evenodd; ";
100
+ style += paint("stroke", path.stroke);
101
+ if (path.stroke) style += strokeDetails(path, strokeWidth);
102
+ return `\t\t<path d="${escapeXml(pathData)}" transform="translate(${num(-path.pathOffset.x)} ${num(-path.pathOffset.y)})" style="${style}" />\n`;
103
+ }
104
+ function fillerDefinitions(text) {
105
+ const seen = /* @__PURE__ */ new Set();
106
+ if (isFiller(text.fill)) seen.add(text.fill.id);
107
+ if (isFiller(text.stroke)) seen.add(text.stroke.id);
108
+ const path = text.path;
109
+ let markup = "";
110
+ const add = (value, owner) => {
111
+ if (!isFiller(value) || seen.has(value.id)) return;
112
+ seen.add(value.id);
113
+ markup += value.toSVG(owner);
114
+ };
115
+ if (path && path.visible && path.opacity !== 0) {
116
+ add(path.fill, path);
117
+ add(path.stroke, path);
118
+ }
119
+ text._textLines.forEach((line, lineIndex) => line.forEach((_, charIndex) => {
120
+ const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
121
+ add(style.fill, text);
122
+ add(style.stroke, text);
123
+ }));
124
+ return markup;
125
+ }
126
+ function decorationRects(text, type) {
127
+ const aligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0;
128
+ let markup = "";
129
+ text._textLines.forEach((line, lineIndex) => {
130
+ line.forEach((_, charIndex) => {
131
+ const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
132
+ if (!style[type] || !style.fill) return;
133
+ const box = text.__charBounds[lineIndex][charIndex];
134
+ const thickness = text.fontSize * (style.textDecorationThickness ?? 66.667) / 1e3;
135
+ const size = text.getHeightOfChar(lineIndex, charIndex);
136
+ const top = text.offsets[type] * size + (style.deltaY ?? 0) - aligner * thickness;
137
+ const color = style.textDecorationColor || style.fill;
138
+ markup += `\t\t<rect transform="${frame(box)}" x="${num(-box.kernedWidth / 2)}" y="${num(top)}" width="${num(box.kernedWidth)}" height="${num(thickness)}" style="${paint("fill", color)}" />\n`;
139
+ });
140
+ });
141
+ return markup;
142
+ }
143
+ function textBackgrounds(text) {
144
+ let markup = "";
145
+ text._textLines.forEach((line, lineIndex) => {
146
+ const height = text.getHeightOfLineImpl(lineIndex);
147
+ line.forEach((_, charIndex) => {
148
+ const color = text.getCompleteStyleDeclaration(lineIndex, charIndex).textBackgroundColor;
149
+ if (!color) return;
150
+ const box = text.__charBounds[lineIndex][charIndex];
151
+ markup += `\t\t<rect transform="${frame(box)}" x="${num(-box.width / 2)}" y="${num(-height * (1 - text._fontSizeFraction))}" width="${num(box.width)}" height="${num(height)}" style="${paint("fill", color)}" />\n`;
152
+ });
153
+ });
154
+ return markup;
155
+ }
156
+ /** One pass of characters: Fabric fills every character, then strokes every character (or the reverse). */
157
+ function glyphs(text, pass) {
158
+ const rtl = text.direction === "rtl";
159
+ const nonScaling = text.strokeUniform ? " vector-effect=\"non-scaling-stroke\"" : "";
160
+ let markup = "";
161
+ text._textLines.forEach((line, lineIndex) => {
162
+ line.forEach((grapheme, charIndex) => {
163
+ if (grapheme.trim().length === 0) return;
164
+ const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
165
+ const strokeWidth = style.strokeWidth ?? 0;
166
+ if (pass === "fill" && !style.fill) return;
167
+ if (pass === "stroke" && (!style.stroke || strokeWidth === 0)) return;
168
+ const box = text.__charBounds[lineIndex][charIndex];
169
+ const y = baselineShift(text._getFontDeclaration(style), text.pathAlign) + (style.deltaY ?? 0);
170
+ const paintStyle = pass === "fill" ? `${paint("fill", style.fill)}stroke: none; ` : `fill: none; ${paint("stroke", style.stroke)}${strokeDetails(text, strokeWidth)}`;
171
+ const anchor = rtl ? " text-anchor=\"end\"" : "";
172
+ markup += `\t\t<text xml:space="preserve" transform="${frame(box)}" x="${num(-box.width / 2)}" y="${num(y)}"${anchor}${nonScaling} style="${fontStyle(style)}${paintStyle}white-space: pre;">${escapeXml(grapheme)}</text>\n`;
173
+ });
174
+ });
175
+ return markup;
176
+ }
177
+ function objectStyle(text) {
178
+ const parts = [];
179
+ if (text.opacity !== 1) parts.push(`opacity: ${num(text.opacity)};`);
180
+ if (!text.visible) parts.push("visibility: hidden;");
181
+ return parts.length > 0 ? ` style="${parts.join(" ")}"` : "";
182
+ }
183
+ function boxBackground(text) {
184
+ const color = text.backgroundColor;
185
+ if (!color) return "";
186
+ const { x, y } = text._getNonTransformedDimensions();
187
+ return `\t\t<rect x="${num(-x / 2)}" y="${num(-y / 2)}" width="${num(x)}" height="${num(y)}" style="${paint("fill", color)}" />\n`;
188
+ }
189
+ /**
190
+ * Makes sure every character has its place on the path. Fabric measures
191
+ * characters when it first draws them, and not at all when `path` was
192
+ * assigned directly instead of through `set` or the constructor.
193
+ */
194
+ function layOut(text) {
195
+ const measured = () => text._textLines.every((line, index) => line.length === 0 || text.__charBounds?.[index]?.[0]?.renderLeft !== void 0);
196
+ if (!text.path?.segmentsInfo) {
197
+ text.setPathInfo();
198
+ text.initDimensions();
199
+ }
200
+ text._textLines.forEach((_, index) => {
201
+ if (text.__charBounds?.[index]?.[0]?.renderLeft === void 0) text.getLineWidth(index);
202
+ });
203
+ if (!measured()) {
204
+ text.setPathInfo();
205
+ text.initDimensions();
206
+ text._textLines.forEach((_, index) => text.getLineWidth(index));
207
+ }
208
+ }
209
+ /** The SVG for one text on a path, in the same order Fabric draws it on the canvas. */
210
+ function textOnPathToSVG(object, reviver) {
211
+ const text = object;
212
+ layOut(text);
213
+ const strokeFirst = text.paintFirst === "stroke";
214
+ const body = [
215
+ fillerDefinitions(text),
216
+ `\t<g${objectStyle(text)}>\n`,
217
+ boxBackground(text),
218
+ guidePath(text),
219
+ textBackgrounds(text),
220
+ decorationRects(text, "underline"),
221
+ strokeFirst ? glyphs(text, "stroke") : glyphs(text, "fill"),
222
+ strokeFirst ? glyphs(text, "fill") : glyphs(text, "stroke"),
223
+ decorationRects(text, "overline"),
224
+ decorationRects(text, "linethrough"),
225
+ " </g>\n"
226
+ ];
227
+ return text._createBaseSVGMarkup(body, {
228
+ reviver,
229
+ noStyle: true,
230
+ withShadow: true
231
+ });
232
+ }
233
+ /**
234
+ * Runs `work` while every text on a path on the canvas, including text in
235
+ * groups, writes the SVG from this module. Everything else keeps Fabric's
236
+ * SVG. Returns how many objects were handled.
237
+ */
238
+ function withTextOnPathSVG(canvas, work) {
239
+ return require_overrides.withSvgOverrides(canvas, (object) => isTextOnPath(object) ? (reviver) => textOnPathToSVG(object, reviver) : void 0, work);
240
+ }
241
+ function findTextOnPath(canvas) {
242
+ const found = [];
243
+ require_walk_objects.walkObjects(canvas.getObjects(), (object) => {
244
+ if (isTextOnPath(object)) found.push(object);
245
+ });
246
+ return found;
247
+ }
248
+ //#endregion
249
+ exports.findTextOnPath = findTextOnPath;
250
+ exports.isTextOnPath = isTextOnPath;
251
+ exports.textOnPathToSVG = textOnPathToSVG;
252
+ exports.withTextOnPathSVG = withTextOnPathSVG;
@@ -0,0 +1,249 @@
1
+ import { walkObjects } from "../../fabric/walk-objects.js";
2
+ import { withSvgOverrides } from "./overrides.js";
3
+ import { Color, util } from "fabric";
4
+ //#region src/export/svg/text-on-path.ts
5
+ function isTextOnPath(object) {
6
+ const candidate = object;
7
+ return Boolean(candidate.path) && Array.isArray(candidate._textLines) && typeof candidate.getCompleteStyleDeclaration === "function";
8
+ }
9
+ const DIGITS = 4;
10
+ function num(value) {
11
+ if (!Number.isFinite(value)) return "0";
12
+ const fixed = value.toFixed(DIGITS);
13
+ return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") || "0" : fixed;
14
+ }
15
+ function escapeXml(value) {
16
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
17
+ }
18
+ /** CSS values that could break out of a style attribute are dropped, as Fabric 7 does. */
19
+ function safeToken(value) {
20
+ if (value === void 0 || value === null) return void 0;
21
+ const text = String(value).trim();
22
+ if (text.length === 0 || /[;<>{}\\]|url\(|expression\(|javascript:|data:/i.test(text)) return void 0;
23
+ return text;
24
+ }
25
+ function isFiller(value) {
26
+ return typeof value === "object" && value !== null && typeof value.toSVG === "function";
27
+ }
28
+ /** `fill: rgb(...); fill-opacity: a` for a color, `url(#SVGID_id)` for a gradient or pattern. */
29
+ function paint(property, value) {
30
+ if (isFiller(value)) return `${property}: url(#SVGID_${value.id}); `;
31
+ const token = typeof value === "string" ? safeToken(value) : void 0;
32
+ if (token === void 0 || token === "none" || token === "transparent") return `${property}: none; `;
33
+ const color = new Color(token);
34
+ const alpha = color.getAlpha();
35
+ const rgb = color.toRgb();
36
+ return alpha === 1 ? `${property}: ${rgb}; ` : `${property}: ${rgb}; ${property}-opacity: ${num(alpha)}; `;
37
+ }
38
+ function strokeDetails(text, strokeWidth) {
39
+ let style = `stroke-width: ${num(strokeWidth)}; `;
40
+ if (text.strokeDashArray && text.strokeDashArray.length > 0) style += `stroke-dasharray: ${text.strokeDashArray.map(num).join(" ")}; `;
41
+ const cap = safeToken(text.strokeLineCap);
42
+ const join = safeToken(text.strokeLineJoin);
43
+ if (cap) style += `stroke-linecap: ${cap}; `;
44
+ if (join) style += `stroke-linejoin: ${join}; `;
45
+ if (text.strokeMiterLimit !== void 0) style += `stroke-miterlimit: ${num(text.strokeMiterLimit)}; `;
46
+ return style;
47
+ }
48
+ function fontStyle(style) {
49
+ const family = safeToken(String(style.fontFamily ?? "").replace(/"/g, "'"));
50
+ const weight = safeToken(style.fontWeight);
51
+ const slant = safeToken(style.fontStyle);
52
+ let css = "";
53
+ if (family) css += `font-family: ${family}; `;
54
+ css += `font-size: ${num(style.fontSize ?? 0)}px; `;
55
+ if (weight) css += `font-weight: ${weight}; `;
56
+ if (slant) css += `font-style: ${slant}; `;
57
+ return css;
58
+ }
59
+ function frame(box) {
60
+ const angle = box.angle ?? 0;
61
+ const cos = Math.cos(angle);
62
+ const sin = Math.sin(angle);
63
+ return `matrix(${num(cos)} ${num(sin)} ${num(-sin)} ${num(cos)} ${num(box.renderLeft ?? 0)} ${num(box.renderTop ?? 0)})`;
64
+ }
65
+ let measuringContext;
66
+ function measuring() {
67
+ if (measuringContext === void 0) measuringContext = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
68
+ return measuringContext;
69
+ }
70
+ const baselineShifts = /* @__PURE__ */ new Map();
71
+ /**
72
+ * How far below the path the alphabetic baseline sits when Fabric draws with
73
+ * `pathAlign` (the canvas `textBaseline`). SVG has no portable way to say
74
+ * this, so the shift is measured and written as a plain offset.
75
+ */
76
+ function baselineShift(font, pathAlign) {
77
+ const baseline = pathAlign === "center" ? "middle" : pathAlign === "ascender" ? "top" : pathAlign === "descender" ? "bottom" : void 0;
78
+ if (baseline === void 0) return 0;
79
+ const key = `${baseline}|${font}`;
80
+ const known = baselineShifts.get(key);
81
+ if (known !== void 0) return known;
82
+ const context = measuring();
83
+ let shift = 0;
84
+ if (context) {
85
+ context.font = font;
86
+ context.textBaseline = baseline;
87
+ shift = -context.measureText("M").alphabeticBaseline;
88
+ if (!Number.isFinite(shift)) shift = 0;
89
+ }
90
+ baselineShifts.set(key, shift);
91
+ return shift;
92
+ }
93
+ function guidePath(text) {
94
+ const path = text.path;
95
+ if (!path || !path.visible || path.opacity === 0) return "";
96
+ const pathData = util.joinPath(path.path, DIGITS);
97
+ const strokeWidth = path.strokeWidth ?? 1;
98
+ let style = paint("fill", path.fill);
99
+ if (path.fillRule === "evenodd") style += "fill-rule: evenodd; ";
100
+ style += paint("stroke", path.stroke);
101
+ if (path.stroke) style += strokeDetails(path, strokeWidth);
102
+ return `\t\t<path d="${escapeXml(pathData)}" transform="translate(${num(-path.pathOffset.x)} ${num(-path.pathOffset.y)})" style="${style}" />\n`;
103
+ }
104
+ function fillerDefinitions(text) {
105
+ const seen = /* @__PURE__ */ new Set();
106
+ if (isFiller(text.fill)) seen.add(text.fill.id);
107
+ if (isFiller(text.stroke)) seen.add(text.stroke.id);
108
+ const path = text.path;
109
+ let markup = "";
110
+ const add = (value, owner) => {
111
+ if (!isFiller(value) || seen.has(value.id)) return;
112
+ seen.add(value.id);
113
+ markup += value.toSVG(owner);
114
+ };
115
+ if (path && path.visible && path.opacity !== 0) {
116
+ add(path.fill, path);
117
+ add(path.stroke, path);
118
+ }
119
+ text._textLines.forEach((line, lineIndex) => line.forEach((_, charIndex) => {
120
+ const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
121
+ add(style.fill, text);
122
+ add(style.stroke, text);
123
+ }));
124
+ return markup;
125
+ }
126
+ function decorationRects(text, type) {
127
+ const aligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0;
128
+ let markup = "";
129
+ text._textLines.forEach((line, lineIndex) => {
130
+ line.forEach((_, charIndex) => {
131
+ const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
132
+ if (!style[type] || !style.fill) return;
133
+ const box = text.__charBounds[lineIndex][charIndex];
134
+ const thickness = text.fontSize * (style.textDecorationThickness ?? 66.667) / 1e3;
135
+ const size = text.getHeightOfChar(lineIndex, charIndex);
136
+ const top = text.offsets[type] * size + (style.deltaY ?? 0) - aligner * thickness;
137
+ const color = style.textDecorationColor || style.fill;
138
+ markup += `\t\t<rect transform="${frame(box)}" x="${num(-box.kernedWidth / 2)}" y="${num(top)}" width="${num(box.kernedWidth)}" height="${num(thickness)}" style="${paint("fill", color)}" />\n`;
139
+ });
140
+ });
141
+ return markup;
142
+ }
143
+ function textBackgrounds(text) {
144
+ let markup = "";
145
+ text._textLines.forEach((line, lineIndex) => {
146
+ const height = text.getHeightOfLineImpl(lineIndex);
147
+ line.forEach((_, charIndex) => {
148
+ const color = text.getCompleteStyleDeclaration(lineIndex, charIndex).textBackgroundColor;
149
+ if (!color) return;
150
+ const box = text.__charBounds[lineIndex][charIndex];
151
+ markup += `\t\t<rect transform="${frame(box)}" x="${num(-box.width / 2)}" y="${num(-height * (1 - text._fontSizeFraction))}" width="${num(box.width)}" height="${num(height)}" style="${paint("fill", color)}" />\n`;
152
+ });
153
+ });
154
+ return markup;
155
+ }
156
+ /** One pass of characters: Fabric fills every character, then strokes every character (or the reverse). */
157
+ function glyphs(text, pass) {
158
+ const rtl = text.direction === "rtl";
159
+ const nonScaling = text.strokeUniform ? " vector-effect=\"non-scaling-stroke\"" : "";
160
+ let markup = "";
161
+ text._textLines.forEach((line, lineIndex) => {
162
+ line.forEach((grapheme, charIndex) => {
163
+ if (grapheme.trim().length === 0) return;
164
+ const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
165
+ const strokeWidth = style.strokeWidth ?? 0;
166
+ if (pass === "fill" && !style.fill) return;
167
+ if (pass === "stroke" && (!style.stroke || strokeWidth === 0)) return;
168
+ const box = text.__charBounds[lineIndex][charIndex];
169
+ const y = baselineShift(text._getFontDeclaration(style), text.pathAlign) + (style.deltaY ?? 0);
170
+ const paintStyle = pass === "fill" ? `${paint("fill", style.fill)}stroke: none; ` : `fill: none; ${paint("stroke", style.stroke)}${strokeDetails(text, strokeWidth)}`;
171
+ const anchor = rtl ? " text-anchor=\"end\"" : "";
172
+ markup += `\t\t<text xml:space="preserve" transform="${frame(box)}" x="${num(-box.width / 2)}" y="${num(y)}"${anchor}${nonScaling} style="${fontStyle(style)}${paintStyle}white-space: pre;">${escapeXml(grapheme)}</text>\n`;
173
+ });
174
+ });
175
+ return markup;
176
+ }
177
+ function objectStyle(text) {
178
+ const parts = [];
179
+ if (text.opacity !== 1) parts.push(`opacity: ${num(text.opacity)};`);
180
+ if (!text.visible) parts.push("visibility: hidden;");
181
+ return parts.length > 0 ? ` style="${parts.join(" ")}"` : "";
182
+ }
183
+ function boxBackground(text) {
184
+ const color = text.backgroundColor;
185
+ if (!color) return "";
186
+ const { x, y } = text._getNonTransformedDimensions();
187
+ return `\t\t<rect x="${num(-x / 2)}" y="${num(-y / 2)}" width="${num(x)}" height="${num(y)}" style="${paint("fill", color)}" />\n`;
188
+ }
189
+ /**
190
+ * Makes sure every character has its place on the path. Fabric measures
191
+ * characters when it first draws them, and not at all when `path` was
192
+ * assigned directly instead of through `set` or the constructor.
193
+ */
194
+ function layOut(text) {
195
+ const measured = () => text._textLines.every((line, index) => line.length === 0 || text.__charBounds?.[index]?.[0]?.renderLeft !== void 0);
196
+ if (!text.path?.segmentsInfo) {
197
+ text.setPathInfo();
198
+ text.initDimensions();
199
+ }
200
+ text._textLines.forEach((_, index) => {
201
+ if (text.__charBounds?.[index]?.[0]?.renderLeft === void 0) text.getLineWidth(index);
202
+ });
203
+ if (!measured()) {
204
+ text.setPathInfo();
205
+ text.initDimensions();
206
+ text._textLines.forEach((_, index) => text.getLineWidth(index));
207
+ }
208
+ }
209
+ /** The SVG for one text on a path, in the same order Fabric draws it on the canvas. */
210
+ function textOnPathToSVG(object, reviver) {
211
+ const text = object;
212
+ layOut(text);
213
+ const strokeFirst = text.paintFirst === "stroke";
214
+ const body = [
215
+ fillerDefinitions(text),
216
+ `\t<g${objectStyle(text)}>\n`,
217
+ boxBackground(text),
218
+ guidePath(text),
219
+ textBackgrounds(text),
220
+ decorationRects(text, "underline"),
221
+ strokeFirst ? glyphs(text, "stroke") : glyphs(text, "fill"),
222
+ strokeFirst ? glyphs(text, "fill") : glyphs(text, "stroke"),
223
+ decorationRects(text, "overline"),
224
+ decorationRects(text, "linethrough"),
225
+ " </g>\n"
226
+ ];
227
+ return text._createBaseSVGMarkup(body, {
228
+ reviver,
229
+ noStyle: true,
230
+ withShadow: true
231
+ });
232
+ }
233
+ /**
234
+ * Runs `work` while every text on a path on the canvas, including text in
235
+ * groups, writes the SVG from this module. Everything else keeps Fabric's
236
+ * SVG. Returns how many objects were handled.
237
+ */
238
+ function withTextOnPathSVG(canvas, work) {
239
+ return withSvgOverrides(canvas, (object) => isTextOnPath(object) ? (reviver) => textOnPathToSVG(object, reviver) : void 0, work);
240
+ }
241
+ function findTextOnPath(canvas) {
242
+ const found = [];
243
+ walkObjects(canvas.getObjects(), (object) => {
244
+ if (isTextOnPath(object)) found.push(object);
245
+ });
246
+ return found;
247
+ }
248
+ //#endregion
249
+ export { findTextOnPath, isTextOnPath, textOnPathToSVG, withTextOnPathSVG };
@@ -1,3 +1,4 @@
1
+ const require_concurrency = require("../util/concurrency.cjs");
1
2
  const require_walk_objects = require("./walk-objects.cjs");
2
3
  let fabric = require("fabric");
3
4
  //#region src/fabric/fabric-adapter.ts
@@ -61,11 +62,51 @@ const strictReviver = refuseMissingObjects;
61
62
  function createObjects(serializedObjects) {
62
63
  return fabric.util.enlivenObjects(serializedObjects, { reviver: strictReviver });
63
64
  }
64
- async function loadIntoCanvas(canvas, content, signal) {
65
- const json = { objects: content.objects };
66
- if (content.version !== void 0) json.version = content.version;
67
- if (content.background !== void 0) json.background = content.background;
68
- await canvas.loadFromJSON(json, strictReviver, { signal });
65
+ function abortError() {
66
+ const error = /* @__PURE__ */ new Error("aborted");
67
+ error.name = "AbortError";
68
+ return error;
69
+ }
70
+ /**
71
+ * Loads content like `canvas.loadFromJSON`, but creates the objects a chunk at
72
+ * a time and yields to the browser in between, so big documents do not
73
+ * freeze the page. Like Fabric, every object is created before the canvas is
74
+ * cleared: a failure or cancel leaves the old content in place.
75
+ */
76
+ async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects }) {
77
+ const total = content.objects.length;
78
+ const size = Math.max(1, Math.floor(chunkSize));
79
+ const created = [];
80
+ const background = fabric.util.enlivenObjectEnlivables({ backgroundColor: content.background }, { signal });
81
+ background.catch(() => void 0);
82
+ try {
83
+ onObjects?.(0, total);
84
+ for (let start = 0; start < total; start += size) {
85
+ if (signal.aborted) throw abortError();
86
+ const chunk = content.objects.slice(start, start + size);
87
+ created.push(...await fabric.util.enlivenObjects(chunk, {
88
+ reviver: strictReviver,
89
+ signal
90
+ }));
91
+ onObjects?.(created.length, total);
92
+ if (start + size < total) await require_concurrency.yieldToEventLoop();
93
+ }
94
+ const enlivenedBackground = await background;
95
+ if (signal.aborted) throw abortError();
96
+ const renderOnAddRemove = canvas.renderOnAddRemove;
97
+ canvas.renderOnAddRemove = false;
98
+ try {
99
+ canvas.clear();
100
+ canvas.add(...created);
101
+ if (content.version !== void 0) canvas.set({ version: content.version });
102
+ canvas.set(enlivenedBackground);
103
+ } finally {
104
+ canvas.renderOnAddRemove = renderOnAddRemove;
105
+ }
106
+ } catch (error) {
107
+ if (!created.some((object) => object.canvas === canvas)) created.forEach((object) => object.dispose?.());
108
+ throw error;
109
+ }
69
110
  }
70
111
  //#endregion
71
112
  exports.createObjects = createObjects;
@@ -1,3 +1,4 @@
1
+ import { yieldToEventLoop } from "../util/concurrency.js";
1
2
  import { childrenOf, walkObjects } from "./walk-objects.js";
2
3
  import { util } from "fabric";
3
4
  //#region src/fabric/fabric-adapter.ts
@@ -61,11 +62,51 @@ const strictReviver = refuseMissingObjects;
61
62
  function createObjects(serializedObjects) {
62
63
  return util.enlivenObjects(serializedObjects, { reviver: strictReviver });
63
64
  }
64
- async function loadIntoCanvas(canvas, content, signal) {
65
- const json = { objects: content.objects };
66
- if (content.version !== void 0) json.version = content.version;
67
- if (content.background !== void 0) json.background = content.background;
68
- await canvas.loadFromJSON(json, strictReviver, { signal });
65
+ function abortError() {
66
+ const error = /* @__PURE__ */ new Error("aborted");
67
+ error.name = "AbortError";
68
+ return error;
69
+ }
70
+ /**
71
+ * Loads content like `canvas.loadFromJSON`, but creates the objects a chunk at
72
+ * a time and yields to the browser in between, so big documents do not
73
+ * freeze the page. Like Fabric, every object is created before the canvas is
74
+ * cleared: a failure or cancel leaves the old content in place.
75
+ */
76
+ async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects }) {
77
+ const total = content.objects.length;
78
+ const size = Math.max(1, Math.floor(chunkSize));
79
+ const created = [];
80
+ const background = util.enlivenObjectEnlivables({ backgroundColor: content.background }, { signal });
81
+ background.catch(() => void 0);
82
+ try {
83
+ onObjects?.(0, total);
84
+ for (let start = 0; start < total; start += size) {
85
+ if (signal.aborted) throw abortError();
86
+ const chunk = content.objects.slice(start, start + size);
87
+ created.push(...await util.enlivenObjects(chunk, {
88
+ reviver: strictReviver,
89
+ signal
90
+ }));
91
+ onObjects?.(created.length, total);
92
+ if (start + size < total) await yieldToEventLoop();
93
+ }
94
+ const enlivenedBackground = await background;
95
+ if (signal.aborted) throw abortError();
96
+ const renderOnAddRemove = canvas.renderOnAddRemove;
97
+ canvas.renderOnAddRemove = false;
98
+ try {
99
+ canvas.clear();
100
+ canvas.add(...created);
101
+ if (content.version !== void 0) canvas.set({ version: content.version });
102
+ canvas.set(enlivenedBackground);
103
+ } finally {
104
+ canvas.renderOnAddRemove = renderOnAddRemove;
105
+ }
106
+ } catch (error) {
107
+ if (!created.some((object) => object.canvas === canvas)) created.forEach((object) => object.dispose?.());
108
+ throw error;
109
+ }
69
110
  }
70
111
  //#endregion
71
112
  export { createObjects, loadIntoCanvas, serializeCanvas };
@@ -2,7 +2,7 @@ const require_errors = require("../engine/errors.cjs");
2
2
  let fabric = require("fabric");
3
3
  //#region src/fabric/object-registry.ts
4
4
  function createObjectRegistry(initialDefinitions = []) {
5
- const extraProperties = /* @__PURE__ */ new Set(["id"]);
5
+ const extraProperties = /* @__PURE__ */ new Set(["id", "name"]);
6
6
  function register(definition) {
7
7
  const { fabricClass } = definition;
8
8
  if (typeof fabricClass?.type !== "string" || fabricClass.type.length === 0) throw new require_errors.DocumentEngineError("INVALID_CUSTOM_OBJECT", "A custom object class needs a static \"type\" string, for example: static type = \"Sticker\"");
@@ -2,7 +2,7 @@ import { DocumentEngineError } from "../engine/errors.js";
2
2
  import { classRegistry } from "fabric";
3
3
  //#region src/fabric/object-registry.ts
4
4
  function createObjectRegistry(initialDefinitions = []) {
5
- const extraProperties = /* @__PURE__ */ new Set(["id"]);
5
+ const extraProperties = /* @__PURE__ */ new Set(["id", "name"]);
6
6
  function register(definition) {
7
7
  const { fabricClass } = definition;
8
8
  if (typeof fabricClass?.type !== "string" || fabricClass.type.length === 0) throw new DocumentEngineError("INVALID_CUSTOM_OBJECT", "A custom object class needs a static \"type\" string, for example: static type = \"Sticker\"");