fabricjs-document-engine 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -11
- package/README.zh-CN.md +220 -11
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +31 -17
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +31 -17
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +89 -6
- package/dist/assets/image-check.d.cts +17 -0
- package/dist/assets/image-check.d.ts +17 -0
- package/dist/assets/image-check.js +89 -7
- package/dist/commands/clipboard.cjs +141 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +140 -0
- package/dist/commands/layers.cjs +97 -0
- package/dist/commands/layers.d.cts +39 -0
- package/dist/commands/layers.d.ts +39 -0
- package/dist/commands/layers.js +92 -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 +196 -31
- package/dist/engine/create-document-engine.d.cts +44 -7
- package/dist/engine/create-document-engine.d.ts +44 -7
- package/dist/engine/create-document-engine.js +197 -32
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/batch-render.cjs +172 -0
- package/dist/export/batch-render.d.cts +43 -0
- package/dist/export/batch-render.d.ts +43 -0
- package/dist/export/batch-render.js +171 -0
- package/dist/export/export-options.cjs +21 -1
- package/dist/export/export-options.d.cts +29 -0
- package/dist/export/export-options.d.ts +29 -0
- package/dist/export/export-options.js +21 -2
- package/dist/export/preflight-export.cjs +11 -2
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/preflight-export.js +11 -2
- package/dist/export/render-export.cjs +57 -4
- package/dist/export/render-export.js +57 -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 +250 -0
- package/dist/export/svg/embed-assets.js +250 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/raster-object.cjs +82 -0
- package/dist/export/svg/raster-object.js +81 -0
- package/dist/export/svg/text-decorations.cjs +108 -0
- package/dist/export/svg/text-decorations.js +107 -0
- package/dist/export/svg/text-on-path.cjs +242 -0
- package/dist/export/svg/text-on-path.js +240 -0
- package/dist/fabric/fabric-adapter.cjs +49 -6
- package/dist/fabric/fabric-adapter.js +49 -6
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- package/dist/fabric/page-state.cjs +69 -0
- package/dist/fabric/page-state.js +64 -0
- package/dist/history/apply-state.cjs +19 -2
- package/dist/history/apply-state.js +19 -2
- package/dist/history/create-history.cjs +53 -13
- package/dist/history/create-history.d.cts +8 -0
- package/dist/history/create-history.d.ts +8 -0
- package/dist/history/create-history.js +53 -13
- package/dist/history/snapshot.cjs +10 -1
- package/dist/history/snapshot.js +10 -2
- package/dist/import/svg-import.cjs +256 -0
- package/dist/import/svg-import.d.cts +50 -0
- package/dist/import/svg-import.d.ts +50 -0
- package/dist/import/svg-import.js +256 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +4 -1
- package/dist/migrations/migrate-document.cjs +2 -1
- package/dist/migrations/migrate-document.js +2 -1
- package/dist/pdf/export-pdf.cjs +381 -0
- package/dist/pdf/export-pdf.d.cts +73 -0
- package/dist/pdf/export-pdf.d.ts +73 -0
- package/dist/pdf/export-pdf.js +381 -0
- package/dist/pdf/fonts.cjs +115 -0
- package/dist/pdf/fonts.d.cts +12 -0
- package/dist/pdf/fonts.d.ts +12 -0
- package/dist/pdf/fonts.js +112 -0
- package/dist/pdf/page-layout.cjs +79 -0
- package/dist/pdf/page-layout.d.cts +51 -0
- package/dist/pdf/page-layout.d.ts +51 -0
- package/dist/pdf/page-layout.js +76 -0
- package/dist/pdf.cjs +7 -0
- package/dist/pdf.d.cts +4 -0
- package/dist/pdf.d.ts +4 -0
- package/dist/pdf.js +3 -0
- package/dist/react/use-layers.cjs +53 -0
- package/dist/react/use-layers.d.cts +10 -0
- package/dist/react/use-layers.d.ts +10 -0
- package/dist/react/use-layers.js +53 -0
- package/dist/react.cjs +2 -0
- package/dist/react.d.cts +3 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +2 -1
- package/dist/recovery/recovery-controller.cjs +148 -29
- package/dist/recovery/recovery-controller.d.cts +8 -0
- package/dist/recovery/recovery-controller.d.ts +8 -0
- package/dist/recovery/recovery-controller.js +148 -29
- package/dist/security/content-limits.cjs +43 -3
- package/dist/security/content-limits.d.cts +8 -0
- package/dist/security/content-limits.d.ts +8 -0
- package/dist/security/content-limits.js +39 -4
- package/dist/util/concurrency.cjs +41 -0
- package/dist/util/concurrency.js +40 -0
- package/package.json +30 -3
- package/schema/document-v1.schema.json +5 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { walkObjects } from "../../fabric/walk-objects.js";
|
|
2
|
+
import { Color, util } from "fabric";
|
|
3
|
+
//#region src/export/svg/text-on-path.ts
|
|
4
|
+
function isTextOnPath(object) {
|
|
5
|
+
const candidate = object;
|
|
6
|
+
return Boolean(candidate.path) && Array.isArray(candidate._textLines) && typeof candidate.getCompleteStyleDeclaration === "function";
|
|
7
|
+
}
|
|
8
|
+
const DIGITS = 4;
|
|
9
|
+
function num(value) {
|
|
10
|
+
if (!Number.isFinite(value)) return "0";
|
|
11
|
+
const fixed = value.toFixed(DIGITS);
|
|
12
|
+
return fixed.includes(".") ? fixed.replace(/\.?0+$/, "") || "0" : fixed;
|
|
13
|
+
}
|
|
14
|
+
function escapeXml(value) {
|
|
15
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
16
|
+
}
|
|
17
|
+
/** CSS values that could break out of a style attribute are dropped, as Fabric 7 does. */
|
|
18
|
+
function safeToken(value) {
|
|
19
|
+
if (value === void 0 || value === null) return void 0;
|
|
20
|
+
const text = String(value).trim();
|
|
21
|
+
if (text.length === 0 || /[;<>{}\\]|url\(|expression\(|javascript:|data:/i.test(text)) return void 0;
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
function isFiller(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && typeof value.toSVG === "function";
|
|
26
|
+
}
|
|
27
|
+
/** `fill: rgb(...); fill-opacity: a` for a color, `url(#SVGID_id)` for a gradient or pattern. */
|
|
28
|
+
function paint(property, value) {
|
|
29
|
+
if (isFiller(value)) return `${property}: url(#SVGID_${value.id}); `;
|
|
30
|
+
const token = typeof value === "string" ? safeToken(value) : void 0;
|
|
31
|
+
if (token === void 0 || token === "none" || token === "transparent") return `${property}: none; `;
|
|
32
|
+
const color = new Color(token);
|
|
33
|
+
const alpha = color.getAlpha();
|
|
34
|
+
const rgb = color.toRgb();
|
|
35
|
+
return alpha === 1 ? `${property}: ${rgb}; ` : `${property}: ${rgb}; ${property}-opacity: ${num(alpha)}; `;
|
|
36
|
+
}
|
|
37
|
+
function strokeDetails(text, strokeWidth) {
|
|
38
|
+
let style = `stroke-width: ${num(strokeWidth)}; `;
|
|
39
|
+
if (text.strokeDashArray && text.strokeDashArray.length > 0) style += `stroke-dasharray: ${text.strokeDashArray.map(num).join(" ")}; `;
|
|
40
|
+
const cap = safeToken(text.strokeLineCap);
|
|
41
|
+
const join = safeToken(text.strokeLineJoin);
|
|
42
|
+
if (cap) style += `stroke-linecap: ${cap}; `;
|
|
43
|
+
if (join) style += `stroke-linejoin: ${join}; `;
|
|
44
|
+
if (text.strokeMiterLimit !== void 0) style += `stroke-miterlimit: ${num(text.strokeMiterLimit)}; `;
|
|
45
|
+
return style;
|
|
46
|
+
}
|
|
47
|
+
function fontStyle(style) {
|
|
48
|
+
const family = safeToken(String(style.fontFamily ?? "").replace(/"/g, "'"));
|
|
49
|
+
const weight = safeToken(style.fontWeight);
|
|
50
|
+
const slant = safeToken(style.fontStyle);
|
|
51
|
+
let css = "";
|
|
52
|
+
if (family) css += `font-family: ${family}; `;
|
|
53
|
+
css += `font-size: ${num(style.fontSize ?? 0)}px; `;
|
|
54
|
+
if (weight) css += `font-weight: ${weight}; `;
|
|
55
|
+
if (slant) css += `font-style: ${slant}; `;
|
|
56
|
+
return css;
|
|
57
|
+
}
|
|
58
|
+
function frame(box) {
|
|
59
|
+
const angle = box.angle ?? 0;
|
|
60
|
+
const cos = Math.cos(angle);
|
|
61
|
+
const sin = Math.sin(angle);
|
|
62
|
+
return `matrix(${num(cos)} ${num(sin)} ${num(-sin)} ${num(cos)} ${num(box.renderLeft ?? 0)} ${num(box.renderTop ?? 0)})`;
|
|
63
|
+
}
|
|
64
|
+
let measuringContext;
|
|
65
|
+
function measuring() {
|
|
66
|
+
if (measuringContext === void 0) measuringContext = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
|
|
67
|
+
return measuringContext;
|
|
68
|
+
}
|
|
69
|
+
const baselineShifts = /* @__PURE__ */ new Map();
|
|
70
|
+
/**
|
|
71
|
+
* How far below the path the alphabetic baseline sits when Fabric draws with
|
|
72
|
+
* `pathAlign` (the canvas `textBaseline`). SVG has no portable way to say
|
|
73
|
+
* this, so the shift is measured and written as a plain offset.
|
|
74
|
+
*/
|
|
75
|
+
function baselineShift(font, pathAlign) {
|
|
76
|
+
const baseline = pathAlign === "center" ? "middle" : pathAlign === "ascender" ? "top" : pathAlign === "descender" ? "bottom" : void 0;
|
|
77
|
+
if (baseline === void 0) return 0;
|
|
78
|
+
const key = `${baseline}|${font}`;
|
|
79
|
+
const known = baselineShifts.get(key);
|
|
80
|
+
if (known !== void 0) return known;
|
|
81
|
+
const context = measuring();
|
|
82
|
+
let shift = 0;
|
|
83
|
+
if (context) {
|
|
84
|
+
context.font = font;
|
|
85
|
+
context.textBaseline = baseline;
|
|
86
|
+
shift = -context.measureText("M").alphabeticBaseline;
|
|
87
|
+
if (!Number.isFinite(shift)) shift = 0;
|
|
88
|
+
}
|
|
89
|
+
baselineShifts.set(key, shift);
|
|
90
|
+
return shift;
|
|
91
|
+
}
|
|
92
|
+
function guidePath(text) {
|
|
93
|
+
const path = text.path;
|
|
94
|
+
if (!path || !path.visible || path.opacity === 0) return "";
|
|
95
|
+
const pathData = util.joinPath(path.path, DIGITS);
|
|
96
|
+
const strokeWidth = path.strokeWidth ?? 1;
|
|
97
|
+
let style = paint("fill", path.fill);
|
|
98
|
+
if (path.fillRule === "evenodd") style += "fill-rule: evenodd; ";
|
|
99
|
+
style += paint("stroke", path.stroke);
|
|
100
|
+
if (path.stroke) style += strokeDetails(path, strokeWidth);
|
|
101
|
+
return `\t\t<path d="${escapeXml(pathData)}" transform="translate(${num(-path.pathOffset.x)} ${num(-path.pathOffset.y)})" style="${style}" />\n`;
|
|
102
|
+
}
|
|
103
|
+
function fillerDefinitions(text) {
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
if (isFiller(text.fill)) seen.add(text.fill.id);
|
|
106
|
+
if (isFiller(text.stroke)) seen.add(text.stroke.id);
|
|
107
|
+
const path = text.path;
|
|
108
|
+
let markup = "";
|
|
109
|
+
const add = (value, owner) => {
|
|
110
|
+
if (!isFiller(value) || seen.has(value.id)) return;
|
|
111
|
+
seen.add(value.id);
|
|
112
|
+
markup += value.toSVG(owner);
|
|
113
|
+
};
|
|
114
|
+
if (path && path.visible && path.opacity !== 0) {
|
|
115
|
+
add(path.fill, path);
|
|
116
|
+
add(path.stroke, path);
|
|
117
|
+
}
|
|
118
|
+
text._textLines.forEach((line, lineIndex) => line.forEach((_, charIndex) => {
|
|
119
|
+
const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
120
|
+
add(style.fill, text);
|
|
121
|
+
add(style.stroke, text);
|
|
122
|
+
}));
|
|
123
|
+
return markup;
|
|
124
|
+
}
|
|
125
|
+
function decorationRects(text, type) {
|
|
126
|
+
const aligner = type === "linethrough" ? .5 : type === "overline" ? 1 : 0;
|
|
127
|
+
let markup = "";
|
|
128
|
+
text._textLines.forEach((line, lineIndex) => {
|
|
129
|
+
line.forEach((_, charIndex) => {
|
|
130
|
+
const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
131
|
+
if (!style[type] || !style.fill) return;
|
|
132
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
133
|
+
const thickness = text.fontSize * (style.textDecorationThickness ?? 66.667) / 1e3;
|
|
134
|
+
const size = text.getHeightOfChar(lineIndex, charIndex);
|
|
135
|
+
const top = text.offsets[type] * size + (style.deltaY ?? 0) - aligner * thickness;
|
|
136
|
+
const color = style.textDecorationColor || style.fill;
|
|
137
|
+
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`;
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
return markup;
|
|
141
|
+
}
|
|
142
|
+
function textBackgrounds(text) {
|
|
143
|
+
let markup = "";
|
|
144
|
+
text._textLines.forEach((line, lineIndex) => {
|
|
145
|
+
const height = text.getHeightOfLineImpl(lineIndex);
|
|
146
|
+
line.forEach((_, charIndex) => {
|
|
147
|
+
const color = text.getCompleteStyleDeclaration(lineIndex, charIndex).textBackgroundColor;
|
|
148
|
+
if (!color) return;
|
|
149
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
150
|
+
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`;
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
return markup;
|
|
154
|
+
}
|
|
155
|
+
/** One pass of characters: Fabric fills every character, then strokes every character (or the reverse). */
|
|
156
|
+
function glyphs(text, pass) {
|
|
157
|
+
const rtl = text.direction === "rtl";
|
|
158
|
+
const nonScaling = text.strokeUniform ? " vector-effect=\"non-scaling-stroke\"" : "";
|
|
159
|
+
let markup = "";
|
|
160
|
+
text._textLines.forEach((line, lineIndex) => {
|
|
161
|
+
line.forEach((grapheme, charIndex) => {
|
|
162
|
+
if (grapheme.trim().length === 0) return;
|
|
163
|
+
const style = text.getCompleteStyleDeclaration(lineIndex, charIndex);
|
|
164
|
+
const strokeWidth = style.strokeWidth ?? 0;
|
|
165
|
+
if (pass === "fill" && !style.fill) return;
|
|
166
|
+
if (pass === "stroke" && (!style.stroke || strokeWidth === 0)) return;
|
|
167
|
+
const box = text.__charBounds[lineIndex][charIndex];
|
|
168
|
+
const y = baselineShift(text._getFontDeclaration(style), text.pathAlign) + (style.deltaY ?? 0);
|
|
169
|
+
const paintStyle = pass === "fill" ? `${paint("fill", style.fill)}stroke: none; ` : `fill: none; ${paint("stroke", style.stroke)}${strokeDetails(text, strokeWidth)}`;
|
|
170
|
+
const anchor = rtl ? " text-anchor=\"end\"" : "";
|
|
171
|
+
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`;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
return markup;
|
|
175
|
+
}
|
|
176
|
+
function objectStyle(text) {
|
|
177
|
+
const parts = [];
|
|
178
|
+
if (text.opacity !== 1) parts.push(`opacity: ${num(text.opacity)};`);
|
|
179
|
+
if (!text.visible) parts.push("visibility: hidden;");
|
|
180
|
+
return parts.length > 0 ? ` style="${parts.join(" ")}"` : "";
|
|
181
|
+
}
|
|
182
|
+
function boxBackground(text) {
|
|
183
|
+
const color = text.backgroundColor;
|
|
184
|
+
if (!color) return "";
|
|
185
|
+
const { x, y } = text._getNonTransformedDimensions();
|
|
186
|
+
return `\t\t<rect x="${num(-x / 2)}" y="${num(-y / 2)}" width="${num(x)}" height="${num(y)}" style="${paint("fill", color)}" />\n`;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Makes sure every character has its place on the path. Fabric measures
|
|
190
|
+
* characters when it first draws them, and not at all when `path` was
|
|
191
|
+
* assigned directly instead of through `set` or the constructor.
|
|
192
|
+
*/
|
|
193
|
+
function layOut(text) {
|
|
194
|
+
const measured = () => text._textLines.every((line, index) => line.length === 0 || text.__charBounds?.[index]?.[0]?.renderLeft !== void 0);
|
|
195
|
+
if (!text.path?.segmentsInfo) {
|
|
196
|
+
text.setPathInfo();
|
|
197
|
+
text.initDimensions();
|
|
198
|
+
}
|
|
199
|
+
text._textLines.forEach((_, index) => {
|
|
200
|
+
if (text.__charBounds?.[index]?.[0]?.renderLeft === void 0) text.getLineWidth(index);
|
|
201
|
+
});
|
|
202
|
+
if (!measured()) {
|
|
203
|
+
text.setPathInfo();
|
|
204
|
+
text.initDimensions();
|
|
205
|
+
text._textLines.forEach((_, index) => text.getLineWidth(index));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** The SVG for one text on a path, in the same order Fabric draws it on the canvas. */
|
|
209
|
+
function textOnPathToSVG(object, reviver) {
|
|
210
|
+
const text = object;
|
|
211
|
+
layOut(text);
|
|
212
|
+
const strokeFirst = text.paintFirst === "stroke";
|
|
213
|
+
const body = [
|
|
214
|
+
fillerDefinitions(text),
|
|
215
|
+
`\t<g${objectStyle(text)}>\n`,
|
|
216
|
+
boxBackground(text),
|
|
217
|
+
guidePath(text),
|
|
218
|
+
textBackgrounds(text),
|
|
219
|
+
decorationRects(text, "underline"),
|
|
220
|
+
strokeFirst ? glyphs(text, "stroke") : glyphs(text, "fill"),
|
|
221
|
+
strokeFirst ? glyphs(text, "fill") : glyphs(text, "stroke"),
|
|
222
|
+
decorationRects(text, "overline"),
|
|
223
|
+
decorationRects(text, "linethrough"),
|
|
224
|
+
" </g>\n"
|
|
225
|
+
];
|
|
226
|
+
return text._createBaseSVGMarkup(body, {
|
|
227
|
+
reviver,
|
|
228
|
+
noStyle: true,
|
|
229
|
+
withShadow: true
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function findTextOnPath(canvas) {
|
|
233
|
+
const found = [];
|
|
234
|
+
walkObjects(canvas.getObjects(), (object) => {
|
|
235
|
+
if (isTextOnPath(object)) found.push(object);
|
|
236
|
+
});
|
|
237
|
+
return found;
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
export { findTextOnPath, isTextOnPath, textOnPathToSVG };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const require_page_state = require("./page-state.cjs");
|
|
2
|
+
const require_concurrency = require("../util/concurrency.cjs");
|
|
1
3
|
const require_walk_objects = require("./walk-objects.cjs");
|
|
2
4
|
let fabric = require("fabric");
|
|
3
5
|
//#region src/fabric/fabric-adapter.ts
|
|
@@ -24,7 +26,7 @@ function serializeCanvas(canvas, propertiesToInclude) {
|
|
|
24
26
|
keepOriginExplicit(canvas.getObjects(), objects);
|
|
25
27
|
return {
|
|
26
28
|
version: serialized.version,
|
|
27
|
-
|
|
29
|
+
...require_page_state.pickPageFields(serialized),
|
|
28
30
|
objects
|
|
29
31
|
};
|
|
30
32
|
}
|
|
@@ -61,11 +63,52 @@ const strictReviver = refuseMissingObjects;
|
|
|
61
63
|
function createObjects(serializedObjects) {
|
|
62
64
|
return fabric.util.enlivenObjects(serializedObjects, { reviver: strictReviver });
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
function abortError() {
|
|
67
|
+
const error = /* @__PURE__ */ new Error("aborted");
|
|
68
|
+
error.name = "AbortError";
|
|
69
|
+
return error;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Loads content like `canvas.loadFromJSON`, but creates the objects a chunk at
|
|
73
|
+
* a time and yields to the browser in between, so big documents do not
|
|
74
|
+
* freeze the page. Like Fabric, every object is created before the canvas is
|
|
75
|
+
* cleared: a failure or cancel leaves the old content in place.
|
|
76
|
+
*/
|
|
77
|
+
async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects, beforeSwap }) {
|
|
78
|
+
const total = content.objects.length;
|
|
79
|
+
const size = Math.max(1, Math.floor(chunkSize));
|
|
80
|
+
const created = [];
|
|
81
|
+
const background = require_page_state.enlivenPage(content, signal);
|
|
82
|
+
background.catch(() => void 0);
|
|
83
|
+
try {
|
|
84
|
+
onObjects?.(0, total);
|
|
85
|
+
for (let start = 0; start < total; start += size) {
|
|
86
|
+
if (signal.aborted) throw abortError();
|
|
87
|
+
const chunk = content.objects.slice(start, start + size);
|
|
88
|
+
created.push(...await fabric.util.enlivenObjects(chunk, {
|
|
89
|
+
reviver: strictReviver,
|
|
90
|
+
signal
|
|
91
|
+
}));
|
|
92
|
+
onObjects?.(created.length, total);
|
|
93
|
+
if (start + size < total) await require_concurrency.yieldToEventLoop();
|
|
94
|
+
}
|
|
95
|
+
const enlivenedBackground = await background;
|
|
96
|
+
if (signal.aborted) throw abortError();
|
|
97
|
+
beforeSwap?.();
|
|
98
|
+
const renderOnAddRemove = canvas.renderOnAddRemove;
|
|
99
|
+
canvas.renderOnAddRemove = false;
|
|
100
|
+
try {
|
|
101
|
+
canvas.clear();
|
|
102
|
+
canvas.add(...created);
|
|
103
|
+
if (content.version !== void 0) canvas.set({ version: content.version });
|
|
104
|
+
canvas.set(enlivenedBackground);
|
|
105
|
+
} finally {
|
|
106
|
+
canvas.renderOnAddRemove = renderOnAddRemove;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!created.some((object) => object.canvas === canvas)) created.forEach((object) => object.dispose?.());
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
69
112
|
}
|
|
70
113
|
//#endregion
|
|
71
114
|
exports.createObjects = createObjects;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { enlivenPage, pickPageFields } from "./page-state.js";
|
|
2
|
+
import { yieldToEventLoop } from "../util/concurrency.js";
|
|
1
3
|
import { childrenOf, walkObjects } from "./walk-objects.js";
|
|
2
4
|
import { util } from "fabric";
|
|
3
5
|
//#region src/fabric/fabric-adapter.ts
|
|
@@ -24,7 +26,7 @@ function serializeCanvas(canvas, propertiesToInclude) {
|
|
|
24
26
|
keepOriginExplicit(canvas.getObjects(), objects);
|
|
25
27
|
return {
|
|
26
28
|
version: serialized.version,
|
|
27
|
-
|
|
29
|
+
...pickPageFields(serialized),
|
|
28
30
|
objects
|
|
29
31
|
};
|
|
30
32
|
}
|
|
@@ -61,11 +63,52 @@ const strictReviver = refuseMissingObjects;
|
|
|
61
63
|
function createObjects(serializedObjects) {
|
|
62
64
|
return util.enlivenObjects(serializedObjects, { reviver: strictReviver });
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
function abortError() {
|
|
67
|
+
const error = /* @__PURE__ */ new Error("aborted");
|
|
68
|
+
error.name = "AbortError";
|
|
69
|
+
return error;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Loads content like `canvas.loadFromJSON`, but creates the objects a chunk at
|
|
73
|
+
* a time and yields to the browser in between, so big documents do not
|
|
74
|
+
* freeze the page. Like Fabric, every object is created before the canvas is
|
|
75
|
+
* cleared: a failure or cancel leaves the old content in place.
|
|
76
|
+
*/
|
|
77
|
+
async function loadIntoCanvas(canvas, content, { signal, chunkSize = 100, onObjects, beforeSwap }) {
|
|
78
|
+
const total = content.objects.length;
|
|
79
|
+
const size = Math.max(1, Math.floor(chunkSize));
|
|
80
|
+
const created = [];
|
|
81
|
+
const background = enlivenPage(content, signal);
|
|
82
|
+
background.catch(() => void 0);
|
|
83
|
+
try {
|
|
84
|
+
onObjects?.(0, total);
|
|
85
|
+
for (let start = 0; start < total; start += size) {
|
|
86
|
+
if (signal.aborted) throw abortError();
|
|
87
|
+
const chunk = content.objects.slice(start, start + size);
|
|
88
|
+
created.push(...await util.enlivenObjects(chunk, {
|
|
89
|
+
reviver: strictReviver,
|
|
90
|
+
signal
|
|
91
|
+
}));
|
|
92
|
+
onObjects?.(created.length, total);
|
|
93
|
+
if (start + size < total) await yieldToEventLoop();
|
|
94
|
+
}
|
|
95
|
+
const enlivenedBackground = await background;
|
|
96
|
+
if (signal.aborted) throw abortError();
|
|
97
|
+
beforeSwap?.();
|
|
98
|
+
const renderOnAddRemove = canvas.renderOnAddRemove;
|
|
99
|
+
canvas.renderOnAddRemove = false;
|
|
100
|
+
try {
|
|
101
|
+
canvas.clear();
|
|
102
|
+
canvas.add(...created);
|
|
103
|
+
if (content.version !== void 0) canvas.set({ version: content.version });
|
|
104
|
+
canvas.set(enlivenedBackground);
|
|
105
|
+
} finally {
|
|
106
|
+
canvas.renderOnAddRemove = renderOnAddRemove;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (!created.some((object) => object.canvas === canvas)) created.forEach((object) => object.dispose?.());
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
69
112
|
}
|
|
70
113
|
//#endregion
|
|
71
114
|
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\"");
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
let fabric = require("fabric");
|
|
2
|
+
//#region src/fabric/page-state.ts
|
|
3
|
+
/** The page fields saved with a document, besides its size. */
|
|
4
|
+
const PAGE_FIELDS = [
|
|
5
|
+
"background",
|
|
6
|
+
"backgroundImage",
|
|
7
|
+
"overlay",
|
|
8
|
+
"overlayImage",
|
|
9
|
+
"clipPath"
|
|
10
|
+
];
|
|
11
|
+
/** Copies the page fields that are set from serialized canvas output. */
|
|
12
|
+
function pickPageFields(source) {
|
|
13
|
+
const picked = {};
|
|
14
|
+
for (const field of PAGE_FIELDS) if (source[field] !== void 0 && source[field] !== null) picked[field] = source[field];
|
|
15
|
+
return picked;
|
|
16
|
+
}
|
|
17
|
+
/** The canvas-level objects that can hold images or other objects: background and overlay images and the mask. */
|
|
18
|
+
function pageObjects(page) {
|
|
19
|
+
const objects = [];
|
|
20
|
+
for (const field of [
|
|
21
|
+
"backgroundImage",
|
|
22
|
+
"overlayImage",
|
|
23
|
+
"clipPath"
|
|
24
|
+
]) {
|
|
25
|
+
const value = page[field];
|
|
26
|
+
if (typeof value === "object" && value !== null && typeof value.type === "string") objects.push(value);
|
|
27
|
+
}
|
|
28
|
+
return objects;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates the live values for `canvas.set`: Fabric objects for the images
|
|
32
|
+
* and mask, gradients and patterns for colors. Missing fields become
|
|
33
|
+
* `undefined`, which clears them on the canvas.
|
|
34
|
+
*/
|
|
35
|
+
function enlivenPage(page, signal) {
|
|
36
|
+
return fabric.util.enlivenObjectEnlivables({
|
|
37
|
+
backgroundColor: page.background,
|
|
38
|
+
backgroundImage: page.backgroundImage,
|
|
39
|
+
overlayColor: page.overlay,
|
|
40
|
+
overlayImage: page.overlayImage,
|
|
41
|
+
clipPath: page.clipPath
|
|
42
|
+
}, signal ? { signal } : {});
|
|
43
|
+
}
|
|
44
|
+
/** Applies a page state: the size first, then the enlivened fields. */
|
|
45
|
+
async function applyPage(canvas, page) {
|
|
46
|
+
const live = await enlivenPage(page);
|
|
47
|
+
if (canvas.getWidth() !== page.width || canvas.getHeight() !== page.height) canvas.setDimensions({
|
|
48
|
+
width: page.width,
|
|
49
|
+
height: page.height
|
|
50
|
+
});
|
|
51
|
+
canvas.set(live);
|
|
52
|
+
canvas.requestRenderAll();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every top-level object a document holds: the objects on the page, then its
|
|
56
|
+
* background image, overlay image and mask. Asset checks, address checks and
|
|
57
|
+
* type checks walk this list, so they cover the page as well.
|
|
58
|
+
*/
|
|
59
|
+
function documentObjects(document) {
|
|
60
|
+
const page = document.canvas ? pageObjects(document.canvas) : [];
|
|
61
|
+
return page.length === 0 ? [...document.objects] : [...document.objects, ...page];
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
exports.PAGE_FIELDS = PAGE_FIELDS;
|
|
65
|
+
exports.applyPage = applyPage;
|
|
66
|
+
exports.documentObjects = documentObjects;
|
|
67
|
+
exports.enlivenPage = enlivenPage;
|
|
68
|
+
exports.pageObjects = pageObjects;
|
|
69
|
+
exports.pickPageFields = pickPageFields;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { util } from "fabric";
|
|
2
|
+
//#region src/fabric/page-state.ts
|
|
3
|
+
/** The page fields saved with a document, besides its size. */
|
|
4
|
+
const PAGE_FIELDS = [
|
|
5
|
+
"background",
|
|
6
|
+
"backgroundImage",
|
|
7
|
+
"overlay",
|
|
8
|
+
"overlayImage",
|
|
9
|
+
"clipPath"
|
|
10
|
+
];
|
|
11
|
+
/** Copies the page fields that are set from serialized canvas output. */
|
|
12
|
+
function pickPageFields(source) {
|
|
13
|
+
const picked = {};
|
|
14
|
+
for (const field of PAGE_FIELDS) if (source[field] !== void 0 && source[field] !== null) picked[field] = source[field];
|
|
15
|
+
return picked;
|
|
16
|
+
}
|
|
17
|
+
/** The canvas-level objects that can hold images or other objects: background and overlay images and the mask. */
|
|
18
|
+
function pageObjects(page) {
|
|
19
|
+
const objects = [];
|
|
20
|
+
for (const field of [
|
|
21
|
+
"backgroundImage",
|
|
22
|
+
"overlayImage",
|
|
23
|
+
"clipPath"
|
|
24
|
+
]) {
|
|
25
|
+
const value = page[field];
|
|
26
|
+
if (typeof value === "object" && value !== null && typeof value.type === "string") objects.push(value);
|
|
27
|
+
}
|
|
28
|
+
return objects;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Creates the live values for `canvas.set`: Fabric objects for the images
|
|
32
|
+
* and mask, gradients and patterns for colors. Missing fields become
|
|
33
|
+
* `undefined`, which clears them on the canvas.
|
|
34
|
+
*/
|
|
35
|
+
function enlivenPage(page, signal) {
|
|
36
|
+
return util.enlivenObjectEnlivables({
|
|
37
|
+
backgroundColor: page.background,
|
|
38
|
+
backgroundImage: page.backgroundImage,
|
|
39
|
+
overlayColor: page.overlay,
|
|
40
|
+
overlayImage: page.overlayImage,
|
|
41
|
+
clipPath: page.clipPath
|
|
42
|
+
}, signal ? { signal } : {});
|
|
43
|
+
}
|
|
44
|
+
/** Applies a page state: the size first, then the enlivened fields. */
|
|
45
|
+
async function applyPage(canvas, page) {
|
|
46
|
+
const live = await enlivenPage(page);
|
|
47
|
+
if (canvas.getWidth() !== page.width || canvas.getHeight() !== page.height) canvas.setDimensions({
|
|
48
|
+
width: page.width,
|
|
49
|
+
height: page.height
|
|
50
|
+
});
|
|
51
|
+
canvas.set(live);
|
|
52
|
+
canvas.requestRenderAll();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every top-level object a document holds: the objects on the page, then its
|
|
56
|
+
* background image, overlay image and mask. Asset checks, address checks and
|
|
57
|
+
* type checks walk this list, so they cover the page as well.
|
|
58
|
+
*/
|
|
59
|
+
function documentObjects(document) {
|
|
60
|
+
const page = document.canvas ? pageObjects(document.canvas) : [];
|
|
61
|
+
return page.length === 0 ? [...document.objects] : [...document.objects, ...page];
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { PAGE_FIELDS, applyPage, documentObjects, enlivenPage, pageObjects, pickPageFields };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const require_page_state = require("../fabric/page-state.cjs");
|
|
1
2
|
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
2
3
|
const require_fabric_adapter = require("../fabric/fabric-adapter.cjs");
|
|
4
|
+
const require_snapshot = require("./snapshot.cjs");
|
|
3
5
|
//#region src/history/apply-state.ts
|
|
4
6
|
function arrangeObjects(current, change, created) {
|
|
5
7
|
const arranged = [];
|
|
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
|
|
|
20
22
|
const position = new Map(change.order.map((id, index) => [id, index]));
|
|
21
23
|
return arranged.sort((first, second) => (position.get(require_object_ids.readObjectId(first) ?? "") ?? Infinity) - (position.get(require_object_ids.readObjectId(second) ?? "") ?? Infinity));
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Applies a change. `isStale` is checked after every wait: when it returns
|
|
27
|
+
* true the canvas now shows another document, so nothing is applied and the
|
|
28
|
+
* call returns false.
|
|
29
|
+
*/
|
|
30
|
+
async function applyStateChange(canvas, change, isStale = () => false) {
|
|
31
|
+
const page = change.objects.get(require_snapshot.PAGE_KEY);
|
|
32
|
+
if (page) {
|
|
33
|
+
if (isStale()) return false;
|
|
34
|
+
await require_page_state.applyPage(canvas, JSON.parse(page));
|
|
35
|
+
}
|
|
24
36
|
const serializedObjects = [];
|
|
25
|
-
for (const json of change.objects
|
|
37
|
+
for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
|
|
26
38
|
const createdObjects = await require_fabric_adapter.createObjects(serializedObjects);
|
|
39
|
+
if (isStale()) {
|
|
40
|
+
createdObjects.forEach((object) => object.dispose?.());
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
27
43
|
const createdById = new Map(createdObjects.map((object) => [require_object_ids.readObjectId(object) ?? "", object]));
|
|
28
44
|
canvas.discardActiveObject?.();
|
|
29
45
|
const current = canvas.getObjects();
|
|
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
|
|
|
45
61
|
canvas.renderOnAddRemove = renderOnAddRemove;
|
|
46
62
|
}
|
|
47
63
|
canvas.requestRenderAll();
|
|
64
|
+
return true;
|
|
48
65
|
}
|
|
49
66
|
//#endregion
|
|
50
67
|
exports.applyStateChange = applyStateChange;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { applyPage } from "../fabric/page-state.js";
|
|
1
2
|
import { readObjectId } from "../fabric/object-ids.js";
|
|
2
3
|
import { createObjects } from "../fabric/fabric-adapter.js";
|
|
4
|
+
import { PAGE_KEY } from "./snapshot.js";
|
|
3
5
|
//#region src/history/apply-state.ts
|
|
4
6
|
function arrangeObjects(current, change, created) {
|
|
5
7
|
const arranged = [];
|
|
@@ -20,10 +22,24 @@ function arrangeObjects(current, change, created) {
|
|
|
20
22
|
const position = new Map(change.order.map((id, index) => [id, index]));
|
|
21
23
|
return arranged.sort((first, second) => (position.get(readObjectId(first) ?? "") ?? Infinity) - (position.get(readObjectId(second) ?? "") ?? Infinity));
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Applies a change. `isStale` is checked after every wait: when it returns
|
|
27
|
+
* true the canvas now shows another document, so nothing is applied and the
|
|
28
|
+
* call returns false.
|
|
29
|
+
*/
|
|
30
|
+
async function applyStateChange(canvas, change, isStale = () => false) {
|
|
31
|
+
const page = change.objects.get(PAGE_KEY);
|
|
32
|
+
if (page) {
|
|
33
|
+
if (isStale()) return false;
|
|
34
|
+
await applyPage(canvas, JSON.parse(page));
|
|
35
|
+
}
|
|
24
36
|
const serializedObjects = [];
|
|
25
|
-
for (const json of change.objects
|
|
37
|
+
for (const [id, json] of change.objects) if (id !== "__page__" && json !== null) serializedObjects.push(JSON.parse(json));
|
|
26
38
|
const createdObjects = await createObjects(serializedObjects);
|
|
39
|
+
if (isStale()) {
|
|
40
|
+
createdObjects.forEach((object) => object.dispose?.());
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
27
43
|
const createdById = new Map(createdObjects.map((object) => [readObjectId(object) ?? "", object]));
|
|
28
44
|
canvas.discardActiveObject?.();
|
|
29
45
|
const current = canvas.getObjects();
|
|
@@ -45,6 +61,7 @@ async function applyStateChange(canvas, change) {
|
|
|
45
61
|
canvas.renderOnAddRemove = renderOnAddRemove;
|
|
46
62
|
}
|
|
47
63
|
canvas.requestRenderAll();
|
|
64
|
+
return true;
|
|
48
65
|
}
|
|
49
66
|
//#endregion
|
|
50
67
|
export { applyStateChange };
|