fabricjs-document-engine 1.0.2 → 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.
- package/README.md +181 -10
- package/README.zh-CN.md +181 -10
- package/dist/assets/asset-manifest.d.cts +3 -0
- package/dist/assets/asset-manifest.d.ts +3 -0
- package/dist/assets/asset-pipeline.cjs +22 -10
- package/dist/assets/asset-pipeline.d.cts +5 -1
- package/dist/assets/asset-pipeline.d.ts +5 -1
- package/dist/assets/asset-pipeline.js +22 -10
- package/dist/assets/font-check.cjs +9 -1
- package/dist/assets/font-check.js +9 -1
- package/dist/assets/image-check.cjs +88 -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 +88 -7
- package/dist/commands/clipboard.cjs +136 -0
- package/dist/commands/clipboard.d.cts +47 -0
- package/dist/commands/clipboard.d.ts +47 -0
- package/dist/commands/clipboard.js +135 -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/engine/create-document-engine.cjs +85 -14
- package/dist/engine/create-document-engine.d.cts +20 -1
- package/dist/engine/create-document-engine.d.ts +20 -1
- package/dist/engine/create-document-engine.js +86 -15
- 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 +18 -1
- package/dist/export/export-options.d.cts +23 -0
- package/dist/export/export-options.d.ts +23 -0
- package/dist/export/export-options.js +18 -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 +18 -3
- package/dist/export/render-export.js +18 -4
- package/dist/export/svg/embed-assets.cjs +249 -0
- package/dist/export/svg/embed-assets.js +249 -0
- package/dist/export/svg/overrides.cjs +32 -0
- package/dist/export/svg/overrides.js +32 -0
- package/dist/export/svg/text-on-path.cjs +252 -0
- package/dist/export/svg/text-on-path.js +249 -0
- package/dist/fabric/fabric-adapter.cjs +46 -5
- package/dist/fabric/fabric-adapter.js +46 -5
- package/dist/fabric/object-registry.cjs +1 -1
- package/dist/fabric/object-registry.js +1 -1
- 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 +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -1
- package/dist/pdf/export-pdf.cjs +440 -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 +440 -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/text-decorations.cjs +108 -0
- package/dist/pdf/text-decorations.js +107 -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/util/concurrency.cjs +28 -0
- package/dist/util/concurrency.js +27 -0
- package/package.json +30 -3
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const require_errors = require("../engine/errors.cjs");
|
|
2
|
+
const require_content_limits = require("../security/content-limits.cjs");
|
|
3
|
+
let fabric = require("fabric");
|
|
4
|
+
//#region src/import/svg-import.ts
|
|
5
|
+
const XLINK = "http://www.w3.org/1999/xlink";
|
|
6
|
+
const REMOVED_ELEMENTS = [
|
|
7
|
+
"script",
|
|
8
|
+
"foreignObject",
|
|
9
|
+
"iframe",
|
|
10
|
+
"object",
|
|
11
|
+
"embed",
|
|
12
|
+
"audio",
|
|
13
|
+
"video",
|
|
14
|
+
"animate",
|
|
15
|
+
"set",
|
|
16
|
+
"animateTransform",
|
|
17
|
+
"animateMotion"
|
|
18
|
+
];
|
|
19
|
+
function refuse(message, cause) {
|
|
20
|
+
throw new require_errors.DocumentEngineError("SVG_IMPORT_FAILED", message, cause === void 0 ? {} : { cause });
|
|
21
|
+
}
|
|
22
|
+
function unsafe(message) {
|
|
23
|
+
throw new require_errors.DocumentEngineError("UNSAFE_DOCUMENT", `The SVG was refused: ${message}`, { issues: [{
|
|
24
|
+
code: "UNSAFE_DOCUMENT",
|
|
25
|
+
path: "",
|
|
26
|
+
message
|
|
27
|
+
}] });
|
|
28
|
+
}
|
|
29
|
+
function parse(svg) {
|
|
30
|
+
if (typeof DOMParser === "undefined") refuse("Importing SVG needs a browser DOM");
|
|
31
|
+
const document = new DOMParser().parseFromString(svg.trim(), "image/svg+xml");
|
|
32
|
+
const error = document.querySelector("parsererror");
|
|
33
|
+
if (error) refuse(`The SVG is not valid XML: ${(error.textContent ?? "").trim().slice(0, 200)}`);
|
|
34
|
+
if (document.documentElement.localName !== "svg") refuse("The file is not an SVG: its root element is not <svg>");
|
|
35
|
+
return document;
|
|
36
|
+
}
|
|
37
|
+
function depthOf(element) {
|
|
38
|
+
let deepest = 0;
|
|
39
|
+
const pending = [[element, 1]];
|
|
40
|
+
while (pending.length > 0) {
|
|
41
|
+
const [current, depth] = pending.pop();
|
|
42
|
+
deepest = Math.max(deepest, depth);
|
|
43
|
+
for (const child of Array.from(current.children)) pending.push([child, depth + 1]);
|
|
44
|
+
}
|
|
45
|
+
return deepest;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Removes what an SVG can use to run code or reach other servers. Fabric
|
|
49
|
+
* never runs scripts, but the same file may be shown elsewhere later.
|
|
50
|
+
*/
|
|
51
|
+
function sanitize(document, limits, warnings) {
|
|
52
|
+
const root = document.documentElement;
|
|
53
|
+
const elements = root.getElementsByTagName("*").length;
|
|
54
|
+
const maxObjects = limits.maxObjects ?? 5e4;
|
|
55
|
+
if (elements > maxObjects) unsafe(`it has ${elements} elements, the limit is ${maxObjects}`);
|
|
56
|
+
const maxDepth = limits.maxDepth ?? 100;
|
|
57
|
+
const depth = depthOf(root);
|
|
58
|
+
if (depth > maxDepth) unsafe(`it is nested ${depth} levels deep, the limit is ${maxDepth}`);
|
|
59
|
+
let removed = 0;
|
|
60
|
+
for (const name of REMOVED_ELEMENTS) for (const element of Array.from(root.getElementsByTagName(name))) {
|
|
61
|
+
element.remove();
|
|
62
|
+
removed += 1;
|
|
63
|
+
}
|
|
64
|
+
const isAllowed = limits.isAllowedUrl ?? require_content_limits.isSafeImageUrl;
|
|
65
|
+
for (const element of [root, ...Array.from(root.getElementsByTagName("*"))]) {
|
|
66
|
+
for (const attribute of Array.from(element.attributes)) if (/^on/i.test(attribute.name)) {
|
|
67
|
+
element.removeAttribute(attribute.name);
|
|
68
|
+
removed += 1;
|
|
69
|
+
}
|
|
70
|
+
const link = element.getAttribute("href") ?? element.getAttributeNS(XLINK, "href");
|
|
71
|
+
if (link === null) continue;
|
|
72
|
+
const target = link.trim();
|
|
73
|
+
if (element.localName === "image") {
|
|
74
|
+
if (!isAllowed(target) || /^javascript:/i.test(target)) {
|
|
75
|
+
warnings.push({
|
|
76
|
+
code: "SVG_IMAGE_BLOCKED",
|
|
77
|
+
message: `An image with the address ${target.slice(0, 80)} was left out because the address is not allowed`
|
|
78
|
+
});
|
|
79
|
+
element.remove();
|
|
80
|
+
}
|
|
81
|
+
} else if (!target.startsWith("#")) {
|
|
82
|
+
element.removeAttribute("href");
|
|
83
|
+
element.removeAttributeNS(XLINK, "href");
|
|
84
|
+
removed += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const style of Array.from(root.getElementsByTagName("style"))) {
|
|
88
|
+
const css = style.textContent ?? "";
|
|
89
|
+
const cleaned = css.replace(/@import[^;]*;?/gi, "").replace(/url\(\s*['"]?(?!#)[^)]*\)/gi, "none");
|
|
90
|
+
if (cleaned !== css) {
|
|
91
|
+
style.textContent = cleaned;
|
|
92
|
+
removed += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (removed > 0) warnings.push({
|
|
96
|
+
code: "SVG_CONTENT_REMOVED",
|
|
97
|
+
message: `${removed} scripts, event handlers, embedded pages or links to other files were removed from the SVG`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function intersectsViewport(object, width, height) {
|
|
101
|
+
object.setCoords();
|
|
102
|
+
const box = object.getBoundingRect();
|
|
103
|
+
return box.left < width && box.top < height && box.left + box.width > 0 && box.top + box.height > 0;
|
|
104
|
+
}
|
|
105
|
+
function contentBounds(objects) {
|
|
106
|
+
let left = Infinity;
|
|
107
|
+
let top = Infinity;
|
|
108
|
+
let right = -Infinity;
|
|
109
|
+
let bottom = -Infinity;
|
|
110
|
+
for (const object of objects) {
|
|
111
|
+
object.setCoords();
|
|
112
|
+
const box = object.getBoundingRect();
|
|
113
|
+
left = Math.min(left, box.left);
|
|
114
|
+
top = Math.min(top, box.top);
|
|
115
|
+
right = Math.max(right, box.left + box.width);
|
|
116
|
+
bottom = Math.max(bottom, box.top + box.height);
|
|
117
|
+
}
|
|
118
|
+
return Number.isFinite(left) ? {
|
|
119
|
+
left,
|
|
120
|
+
top,
|
|
121
|
+
width: right - left,
|
|
122
|
+
height: bottom - top
|
|
123
|
+
} : {
|
|
124
|
+
left: 0,
|
|
125
|
+
top: 0,
|
|
126
|
+
width: 0,
|
|
127
|
+
height: 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Parses and places an SVG. Adding the result to a canvas is up to the caller. */
|
|
131
|
+
async function readSvg(svg, options = {}, limits = {}) {
|
|
132
|
+
if (typeof svg !== "string" || svg.trim().length === 0) refuse("The SVG is empty");
|
|
133
|
+
const warnings = [];
|
|
134
|
+
const document = parse(svg);
|
|
135
|
+
sanitize(document, limits, warnings);
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = await (0, fabric.parseSVGDocument)(document, void 0, {
|
|
139
|
+
crossOrigin: options.crossOrigin === void 0 ? "anonymous" : options.crossOrigin,
|
|
140
|
+
signal: options.signal
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (require_errors.isDocumentEngineError(error)) throw error;
|
|
144
|
+
refuse(`Fabric could not read the SVG: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
145
|
+
}
|
|
146
|
+
if (options.signal?.aborted) throw new require_errors.DocumentEngineError("LOAD_ABORTED", "The SVG import was cancelled");
|
|
147
|
+
let objects = parsed.objects.filter((object) => object !== null && object !== void 0);
|
|
148
|
+
const declared = {
|
|
149
|
+
width: parsed.options.width ?? 0,
|
|
150
|
+
height: parsed.options.height ?? 0
|
|
151
|
+
};
|
|
152
|
+
const useContent = options.viewport === "content" || declared.width <= 0 || declared.height <= 0;
|
|
153
|
+
const bounds = useContent ? contentBounds(objects) : {
|
|
154
|
+
left: 0,
|
|
155
|
+
top: 0,
|
|
156
|
+
...declared
|
|
157
|
+
};
|
|
158
|
+
const offscreen = options.offscreen ?? "keep";
|
|
159
|
+
if (offscreen === "drop" && !useContent) {
|
|
160
|
+
const kept = objects.filter((object) => intersectsViewport(object, bounds.width, bounds.height));
|
|
161
|
+
if (kept.length < objects.length) warnings.push({
|
|
162
|
+
code: "SVG_OFFSCREEN_DROPPED",
|
|
163
|
+
message: `${objects.length - kept.length} elements outside the SVG's viewport were left out`
|
|
164
|
+
});
|
|
165
|
+
objects = kept;
|
|
166
|
+
}
|
|
167
|
+
const viewport = {
|
|
168
|
+
width: bounds.width,
|
|
169
|
+
height: bounds.height
|
|
170
|
+
};
|
|
171
|
+
if (objects.length === 0) return {
|
|
172
|
+
objects: [],
|
|
173
|
+
viewport,
|
|
174
|
+
warnings
|
|
175
|
+
};
|
|
176
|
+
const group = new fabric.Group([], {
|
|
177
|
+
layoutManager: new fabric.LayoutManager(new fabric.FixedLayout()),
|
|
178
|
+
width: bounds.width,
|
|
179
|
+
height: bounds.height,
|
|
180
|
+
left: bounds.left + bounds.width / 2,
|
|
181
|
+
top: bounds.top + bounds.height / 2,
|
|
182
|
+
originX: "center",
|
|
183
|
+
originY: "center"
|
|
184
|
+
});
|
|
185
|
+
group.add(...objects);
|
|
186
|
+
if (offscreen === "clip") group.clipPath = new fabric.Rect({
|
|
187
|
+
width: bounds.width,
|
|
188
|
+
height: bounds.height,
|
|
189
|
+
originX: "center",
|
|
190
|
+
originY: "center",
|
|
191
|
+
left: 0,
|
|
192
|
+
top: 0
|
|
193
|
+
});
|
|
194
|
+
let scaleX = 1;
|
|
195
|
+
let scaleY = 1;
|
|
196
|
+
let shiftX = 0;
|
|
197
|
+
let shiftY = 0;
|
|
198
|
+
const fit = options.fit;
|
|
199
|
+
if (fit && bounds.width > 0 && bounds.height > 0) {
|
|
200
|
+
const sx = fit.width / bounds.width;
|
|
201
|
+
const sy = fit.height / bounds.height;
|
|
202
|
+
const mode = fit.mode ?? "contain";
|
|
203
|
+
if (mode === "fill") {
|
|
204
|
+
scaleX = sx;
|
|
205
|
+
scaleY = sy;
|
|
206
|
+
} else {
|
|
207
|
+
scaleX = scaleY = mode === "cover" ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
208
|
+
shiftX = (fit.width - bounds.width * scaleX) / 2;
|
|
209
|
+
shiftY = (fit.height - bounds.height * scaleY) / 2;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
group.set({
|
|
213
|
+
scaleX,
|
|
214
|
+
scaleY,
|
|
215
|
+
originX: "left",
|
|
216
|
+
originY: "top",
|
|
217
|
+
left: (options.left ?? 0) + shiftX,
|
|
218
|
+
top: (options.top ?? 0) + shiftY
|
|
219
|
+
});
|
|
220
|
+
group.setCoords();
|
|
221
|
+
if ((options.as ?? "group") === "group") return {
|
|
222
|
+
objects: [group],
|
|
223
|
+
viewport,
|
|
224
|
+
warnings
|
|
225
|
+
};
|
|
226
|
+
const clip = group.clipPath;
|
|
227
|
+
group.clipPath = void 0;
|
|
228
|
+
const loose = group.removeAll();
|
|
229
|
+
if (clip) {
|
|
230
|
+
const frame = group.calcTransformMatrix();
|
|
231
|
+
for (const object of loose) {
|
|
232
|
+
const absoluteClip = new fabric.Rect({
|
|
233
|
+
width: bounds.width,
|
|
234
|
+
height: bounds.height,
|
|
235
|
+
originX: "center",
|
|
236
|
+
originY: "center",
|
|
237
|
+
absolutePositioned: true
|
|
238
|
+
});
|
|
239
|
+
absoluteClip.set({
|
|
240
|
+
left: frame[4],
|
|
241
|
+
top: frame[5],
|
|
242
|
+
scaleX,
|
|
243
|
+
scaleY
|
|
244
|
+
});
|
|
245
|
+
object.clipPath = absoluteClip;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
loose.forEach((object) => object.setCoords());
|
|
249
|
+
return {
|
|
250
|
+
objects: loose,
|
|
251
|
+
viewport,
|
|
252
|
+
warnings
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
exports.readSvg = readSvg;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { FabricObject } from "fabric";
|
|
2
|
+
//#region src/import/svg-import.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Opens SVG files as Fabric objects without moving them (fabric.js #10916).
|
|
5
|
+
*
|
|
6
|
+
* Fabric's parser already puts every element where the SVG's viewBox says.
|
|
7
|
+
* The usual next step, `util.groupSVGElements`, then sizes the group to the
|
|
8
|
+
* content, so an element outside the viewBox, or a hidden one, moves and
|
|
9
|
+
* resizes everything. This keeps the SVG's own viewport as the frame instead.
|
|
10
|
+
*/
|
|
11
|
+
export interface SvgImportOptions {
|
|
12
|
+
/**
|
|
13
|
+
* `preserve` (the default) keeps the SVG's viewport, so the artwork lands
|
|
14
|
+
* where it sits in the file. `content` uses the bounds of what is drawn,
|
|
15
|
+
* like `util.groupSVGElements`.
|
|
16
|
+
*/
|
|
17
|
+
viewport?: "preserve" | "content";
|
|
18
|
+
/** What to do with elements entirely outside the viewport: keep them, drop them, or clip them. Default `keep`. */
|
|
19
|
+
offscreen?: "keep" | "drop" | "clip";
|
|
20
|
+
/** One group (the default), or separate objects. */
|
|
21
|
+
as?: "group" | "objects";
|
|
22
|
+
/** Where the viewport's top-left corner lands on the canvas. Default 0, 0. */
|
|
23
|
+
left?: number;
|
|
24
|
+
top?: number;
|
|
25
|
+
/** Scales the viewport into this box. `contain` keeps the whole SVG visible, `cover` fills the box, `fill` stretches. */
|
|
26
|
+
fit?: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
mode?: "contain" | "cover" | "fill";
|
|
30
|
+
};
|
|
31
|
+
/** Passed to images in the SVG. Default `anonymous`, so the canvas can still be exported. */
|
|
32
|
+
crossOrigin?: "anonymous" | "use-credentials" | null;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
export type SvgImportWarningCode = "SVG_CONTENT_REMOVED" | "SVG_IMAGE_BLOCKED" | "SVG_OFFSCREEN_DROPPED";
|
|
36
|
+
export interface SvgImportWarning {
|
|
37
|
+
code: SvgImportWarningCode;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SvgImportResult {
|
|
41
|
+
/** The objects added to the canvas: one group, or each object. */
|
|
42
|
+
objects: FabricObject[];
|
|
43
|
+
/** The size of the SVG's viewport in canvas units, before `fit`. */
|
|
44
|
+
viewport: {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
};
|
|
48
|
+
warnings: SvgImportWarning[];
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { FabricObject } from "fabric";
|
|
2
|
+
//#region src/import/svg-import.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Opens SVG files as Fabric objects without moving them (fabric.js #10916).
|
|
5
|
+
*
|
|
6
|
+
* Fabric's parser already puts every element where the SVG's viewBox says.
|
|
7
|
+
* The usual next step, `util.groupSVGElements`, then sizes the group to the
|
|
8
|
+
* content, so an element outside the viewBox, or a hidden one, moves and
|
|
9
|
+
* resizes everything. This keeps the SVG's own viewport as the frame instead.
|
|
10
|
+
*/
|
|
11
|
+
export interface SvgImportOptions {
|
|
12
|
+
/**
|
|
13
|
+
* `preserve` (the default) keeps the SVG's viewport, so the artwork lands
|
|
14
|
+
* where it sits in the file. `content` uses the bounds of what is drawn,
|
|
15
|
+
* like `util.groupSVGElements`.
|
|
16
|
+
*/
|
|
17
|
+
viewport?: "preserve" | "content";
|
|
18
|
+
/** What to do with elements entirely outside the viewport: keep them, drop them, or clip them. Default `keep`. */
|
|
19
|
+
offscreen?: "keep" | "drop" | "clip";
|
|
20
|
+
/** One group (the default), or separate objects. */
|
|
21
|
+
as?: "group" | "objects";
|
|
22
|
+
/** Where the viewport's top-left corner lands on the canvas. Default 0, 0. */
|
|
23
|
+
left?: number;
|
|
24
|
+
top?: number;
|
|
25
|
+
/** Scales the viewport into this box. `contain` keeps the whole SVG visible, `cover` fills the box, `fill` stretches. */
|
|
26
|
+
fit?: {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
mode?: "contain" | "cover" | "fill";
|
|
30
|
+
};
|
|
31
|
+
/** Passed to images in the SVG. Default `anonymous`, so the canvas can still be exported. */
|
|
32
|
+
crossOrigin?: "anonymous" | "use-credentials" | null;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
export type SvgImportWarningCode = "SVG_CONTENT_REMOVED" | "SVG_IMAGE_BLOCKED" | "SVG_OFFSCREEN_DROPPED";
|
|
36
|
+
export interface SvgImportWarning {
|
|
37
|
+
code: SvgImportWarningCode;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SvgImportResult {
|
|
41
|
+
/** The objects added to the canvas: one group, or each object. */
|
|
42
|
+
objects: FabricObject[];
|
|
43
|
+
/** The size of the SVG's viewport in canvas units, before `fit`. */
|
|
44
|
+
viewport: {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
};
|
|
48
|
+
warnings: SvgImportWarning[];
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { DocumentEngineError, isDocumentEngineError } from "../engine/errors.js";
|
|
2
|
+
import { isSafeImageUrl } from "../security/content-limits.js";
|
|
3
|
+
import { FixedLayout, Group, LayoutManager, Rect, parseSVGDocument } from "fabric";
|
|
4
|
+
//#region src/import/svg-import.ts
|
|
5
|
+
const XLINK = "http://www.w3.org/1999/xlink";
|
|
6
|
+
const REMOVED_ELEMENTS = [
|
|
7
|
+
"script",
|
|
8
|
+
"foreignObject",
|
|
9
|
+
"iframe",
|
|
10
|
+
"object",
|
|
11
|
+
"embed",
|
|
12
|
+
"audio",
|
|
13
|
+
"video",
|
|
14
|
+
"animate",
|
|
15
|
+
"set",
|
|
16
|
+
"animateTransform",
|
|
17
|
+
"animateMotion"
|
|
18
|
+
];
|
|
19
|
+
function refuse(message, cause) {
|
|
20
|
+
throw new DocumentEngineError("SVG_IMPORT_FAILED", message, cause === void 0 ? {} : { cause });
|
|
21
|
+
}
|
|
22
|
+
function unsafe(message) {
|
|
23
|
+
throw new DocumentEngineError("UNSAFE_DOCUMENT", `The SVG was refused: ${message}`, { issues: [{
|
|
24
|
+
code: "UNSAFE_DOCUMENT",
|
|
25
|
+
path: "",
|
|
26
|
+
message
|
|
27
|
+
}] });
|
|
28
|
+
}
|
|
29
|
+
function parse(svg) {
|
|
30
|
+
if (typeof DOMParser === "undefined") refuse("Importing SVG needs a browser DOM");
|
|
31
|
+
const document = new DOMParser().parseFromString(svg.trim(), "image/svg+xml");
|
|
32
|
+
const error = document.querySelector("parsererror");
|
|
33
|
+
if (error) refuse(`The SVG is not valid XML: ${(error.textContent ?? "").trim().slice(0, 200)}`);
|
|
34
|
+
if (document.documentElement.localName !== "svg") refuse("The file is not an SVG: its root element is not <svg>");
|
|
35
|
+
return document;
|
|
36
|
+
}
|
|
37
|
+
function depthOf(element) {
|
|
38
|
+
let deepest = 0;
|
|
39
|
+
const pending = [[element, 1]];
|
|
40
|
+
while (pending.length > 0) {
|
|
41
|
+
const [current, depth] = pending.pop();
|
|
42
|
+
deepest = Math.max(deepest, depth);
|
|
43
|
+
for (const child of Array.from(current.children)) pending.push([child, depth + 1]);
|
|
44
|
+
}
|
|
45
|
+
return deepest;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Removes what an SVG can use to run code or reach other servers. Fabric
|
|
49
|
+
* never runs scripts, but the same file may be shown elsewhere later.
|
|
50
|
+
*/
|
|
51
|
+
function sanitize(document, limits, warnings) {
|
|
52
|
+
const root = document.documentElement;
|
|
53
|
+
const elements = root.getElementsByTagName("*").length;
|
|
54
|
+
const maxObjects = limits.maxObjects ?? 5e4;
|
|
55
|
+
if (elements > maxObjects) unsafe(`it has ${elements} elements, the limit is ${maxObjects}`);
|
|
56
|
+
const maxDepth = limits.maxDepth ?? 100;
|
|
57
|
+
const depth = depthOf(root);
|
|
58
|
+
if (depth > maxDepth) unsafe(`it is nested ${depth} levels deep, the limit is ${maxDepth}`);
|
|
59
|
+
let removed = 0;
|
|
60
|
+
for (const name of REMOVED_ELEMENTS) for (const element of Array.from(root.getElementsByTagName(name))) {
|
|
61
|
+
element.remove();
|
|
62
|
+
removed += 1;
|
|
63
|
+
}
|
|
64
|
+
const isAllowed = limits.isAllowedUrl ?? isSafeImageUrl;
|
|
65
|
+
for (const element of [root, ...Array.from(root.getElementsByTagName("*"))]) {
|
|
66
|
+
for (const attribute of Array.from(element.attributes)) if (/^on/i.test(attribute.name)) {
|
|
67
|
+
element.removeAttribute(attribute.name);
|
|
68
|
+
removed += 1;
|
|
69
|
+
}
|
|
70
|
+
const link = element.getAttribute("href") ?? element.getAttributeNS(XLINK, "href");
|
|
71
|
+
if (link === null) continue;
|
|
72
|
+
const target = link.trim();
|
|
73
|
+
if (element.localName === "image") {
|
|
74
|
+
if (!isAllowed(target) || /^javascript:/i.test(target)) {
|
|
75
|
+
warnings.push({
|
|
76
|
+
code: "SVG_IMAGE_BLOCKED",
|
|
77
|
+
message: `An image with the address ${target.slice(0, 80)} was left out because the address is not allowed`
|
|
78
|
+
});
|
|
79
|
+
element.remove();
|
|
80
|
+
}
|
|
81
|
+
} else if (!target.startsWith("#")) {
|
|
82
|
+
element.removeAttribute("href");
|
|
83
|
+
element.removeAttributeNS(XLINK, "href");
|
|
84
|
+
removed += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const style of Array.from(root.getElementsByTagName("style"))) {
|
|
88
|
+
const css = style.textContent ?? "";
|
|
89
|
+
const cleaned = css.replace(/@import[^;]*;?/gi, "").replace(/url\(\s*['"]?(?!#)[^)]*\)/gi, "none");
|
|
90
|
+
if (cleaned !== css) {
|
|
91
|
+
style.textContent = cleaned;
|
|
92
|
+
removed += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (removed > 0) warnings.push({
|
|
96
|
+
code: "SVG_CONTENT_REMOVED",
|
|
97
|
+
message: `${removed} scripts, event handlers, embedded pages or links to other files were removed from the SVG`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function intersectsViewport(object, width, height) {
|
|
101
|
+
object.setCoords();
|
|
102
|
+
const box = object.getBoundingRect();
|
|
103
|
+
return box.left < width && box.top < height && box.left + box.width > 0 && box.top + box.height > 0;
|
|
104
|
+
}
|
|
105
|
+
function contentBounds(objects) {
|
|
106
|
+
let left = Infinity;
|
|
107
|
+
let top = Infinity;
|
|
108
|
+
let right = -Infinity;
|
|
109
|
+
let bottom = -Infinity;
|
|
110
|
+
for (const object of objects) {
|
|
111
|
+
object.setCoords();
|
|
112
|
+
const box = object.getBoundingRect();
|
|
113
|
+
left = Math.min(left, box.left);
|
|
114
|
+
top = Math.min(top, box.top);
|
|
115
|
+
right = Math.max(right, box.left + box.width);
|
|
116
|
+
bottom = Math.max(bottom, box.top + box.height);
|
|
117
|
+
}
|
|
118
|
+
return Number.isFinite(left) ? {
|
|
119
|
+
left,
|
|
120
|
+
top,
|
|
121
|
+
width: right - left,
|
|
122
|
+
height: bottom - top
|
|
123
|
+
} : {
|
|
124
|
+
left: 0,
|
|
125
|
+
top: 0,
|
|
126
|
+
width: 0,
|
|
127
|
+
height: 0
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Parses and places an SVG. Adding the result to a canvas is up to the caller. */
|
|
131
|
+
async function readSvg(svg, options = {}, limits = {}) {
|
|
132
|
+
if (typeof svg !== "string" || svg.trim().length === 0) refuse("The SVG is empty");
|
|
133
|
+
const warnings = [];
|
|
134
|
+
const document = parse(svg);
|
|
135
|
+
sanitize(document, limits, warnings);
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = await parseSVGDocument(document, void 0, {
|
|
139
|
+
crossOrigin: options.crossOrigin === void 0 ? "anonymous" : options.crossOrigin,
|
|
140
|
+
signal: options.signal
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (isDocumentEngineError(error)) throw error;
|
|
144
|
+
refuse(`Fabric could not read the SVG: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
145
|
+
}
|
|
146
|
+
if (options.signal?.aborted) throw new DocumentEngineError("LOAD_ABORTED", "The SVG import was cancelled");
|
|
147
|
+
let objects = parsed.objects.filter((object) => object !== null && object !== void 0);
|
|
148
|
+
const declared = {
|
|
149
|
+
width: parsed.options.width ?? 0,
|
|
150
|
+
height: parsed.options.height ?? 0
|
|
151
|
+
};
|
|
152
|
+
const useContent = options.viewport === "content" || declared.width <= 0 || declared.height <= 0;
|
|
153
|
+
const bounds = useContent ? contentBounds(objects) : {
|
|
154
|
+
left: 0,
|
|
155
|
+
top: 0,
|
|
156
|
+
...declared
|
|
157
|
+
};
|
|
158
|
+
const offscreen = options.offscreen ?? "keep";
|
|
159
|
+
if (offscreen === "drop" && !useContent) {
|
|
160
|
+
const kept = objects.filter((object) => intersectsViewport(object, bounds.width, bounds.height));
|
|
161
|
+
if (kept.length < objects.length) warnings.push({
|
|
162
|
+
code: "SVG_OFFSCREEN_DROPPED",
|
|
163
|
+
message: `${objects.length - kept.length} elements outside the SVG's viewport were left out`
|
|
164
|
+
});
|
|
165
|
+
objects = kept;
|
|
166
|
+
}
|
|
167
|
+
const viewport = {
|
|
168
|
+
width: bounds.width,
|
|
169
|
+
height: bounds.height
|
|
170
|
+
};
|
|
171
|
+
if (objects.length === 0) return {
|
|
172
|
+
objects: [],
|
|
173
|
+
viewport,
|
|
174
|
+
warnings
|
|
175
|
+
};
|
|
176
|
+
const group = new Group([], {
|
|
177
|
+
layoutManager: new LayoutManager(new FixedLayout()),
|
|
178
|
+
width: bounds.width,
|
|
179
|
+
height: bounds.height,
|
|
180
|
+
left: bounds.left + bounds.width / 2,
|
|
181
|
+
top: bounds.top + bounds.height / 2,
|
|
182
|
+
originX: "center",
|
|
183
|
+
originY: "center"
|
|
184
|
+
});
|
|
185
|
+
group.add(...objects);
|
|
186
|
+
if (offscreen === "clip") group.clipPath = new Rect({
|
|
187
|
+
width: bounds.width,
|
|
188
|
+
height: bounds.height,
|
|
189
|
+
originX: "center",
|
|
190
|
+
originY: "center",
|
|
191
|
+
left: 0,
|
|
192
|
+
top: 0
|
|
193
|
+
});
|
|
194
|
+
let scaleX = 1;
|
|
195
|
+
let scaleY = 1;
|
|
196
|
+
let shiftX = 0;
|
|
197
|
+
let shiftY = 0;
|
|
198
|
+
const fit = options.fit;
|
|
199
|
+
if (fit && bounds.width > 0 && bounds.height > 0) {
|
|
200
|
+
const sx = fit.width / bounds.width;
|
|
201
|
+
const sy = fit.height / bounds.height;
|
|
202
|
+
const mode = fit.mode ?? "contain";
|
|
203
|
+
if (mode === "fill") {
|
|
204
|
+
scaleX = sx;
|
|
205
|
+
scaleY = sy;
|
|
206
|
+
} else {
|
|
207
|
+
scaleX = scaleY = mode === "cover" ? Math.max(sx, sy) : Math.min(sx, sy);
|
|
208
|
+
shiftX = (fit.width - bounds.width * scaleX) / 2;
|
|
209
|
+
shiftY = (fit.height - bounds.height * scaleY) / 2;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
group.set({
|
|
213
|
+
scaleX,
|
|
214
|
+
scaleY,
|
|
215
|
+
originX: "left",
|
|
216
|
+
originY: "top",
|
|
217
|
+
left: (options.left ?? 0) + shiftX,
|
|
218
|
+
top: (options.top ?? 0) + shiftY
|
|
219
|
+
});
|
|
220
|
+
group.setCoords();
|
|
221
|
+
if ((options.as ?? "group") === "group") return {
|
|
222
|
+
objects: [group],
|
|
223
|
+
viewport,
|
|
224
|
+
warnings
|
|
225
|
+
};
|
|
226
|
+
const clip = group.clipPath;
|
|
227
|
+
group.clipPath = void 0;
|
|
228
|
+
const loose = group.removeAll();
|
|
229
|
+
if (clip) {
|
|
230
|
+
const frame = group.calcTransformMatrix();
|
|
231
|
+
for (const object of loose) {
|
|
232
|
+
const absoluteClip = new Rect({
|
|
233
|
+
width: bounds.width,
|
|
234
|
+
height: bounds.height,
|
|
235
|
+
originX: "center",
|
|
236
|
+
originY: "center",
|
|
237
|
+
absolutePositioned: true
|
|
238
|
+
});
|
|
239
|
+
absoluteClip.set({
|
|
240
|
+
left: frame[4],
|
|
241
|
+
top: frame[5],
|
|
242
|
+
scaleX,
|
|
243
|
+
scaleY
|
|
244
|
+
});
|
|
245
|
+
object.clipPath = absoluteClip;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
loose.forEach((object) => object.setCoords());
|
|
249
|
+
return {
|
|
250
|
+
objects: loose,
|
|
251
|
+
viewport,
|
|
252
|
+
warnings
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
export { readSvg };
|
package/dist/index.cjs
CHANGED
|
@@ -8,19 +8,31 @@ const require_create_document_engine = require("./engine/create-document-engine.
|
|
|
8
8
|
const require_keyboard_shortcuts = require("./history/keyboard-shortcuts.cjs");
|
|
9
9
|
const require_unsaved_changes_warning = require("./save/unsaved-changes-warning.cjs");
|
|
10
10
|
const require_download_export = require("./export/download-export.cjs");
|
|
11
|
+
const require_batch_render = require("./export/batch-render.cjs");
|
|
11
12
|
const require_document_state_store = require("./state/document-state-store.cjs");
|
|
13
|
+
const require_clipboard = require("./commands/clipboard.cjs");
|
|
14
|
+
const require_layers = require("./commands/layers.cjs");
|
|
15
|
+
exports.CLIPBOARD_FORMAT = require_clipboard.CLIPBOARD_FORMAT;
|
|
12
16
|
exports.CURRENT_SCHEMA_VERSION = require_document_format.CURRENT_SCHEMA_VERSION;
|
|
13
17
|
exports.DocumentEngineError = require_errors.DocumentEngineError;
|
|
14
18
|
exports.bindKeyboardShortcuts = require_keyboard_shortcuts.bindKeyboardShortcuts;
|
|
15
19
|
exports.bindUnsavedChangesWarning = require_unsaved_changes_warning.bindUnsavedChangesWarning;
|
|
20
|
+
exports.bringForward = require_layers.bringForward;
|
|
21
|
+
exports.bringToFront = require_layers.bringToFront;
|
|
22
|
+
exports.createClipboard = require_clipboard.createClipboard;
|
|
16
23
|
exports.createConflictError = require_errors.createConflictError;
|
|
17
24
|
exports.createDocumentEngine = require_create_document_engine.createDocumentEngine;
|
|
18
25
|
exports.createDocumentStateStore = require_document_state_store.createDocumentStateStore;
|
|
19
26
|
exports.detectSchemaVersion = require_migrate_document.detectSchemaVersion;
|
|
20
27
|
exports.downloadExport = require_download_export.downloadExport;
|
|
28
|
+
exports.getLayers = require_layers.getLayers;
|
|
21
29
|
exports.isDocumentEngineError = require_errors.isDocumentEngineError;
|
|
22
30
|
exports.isSafeImageUrl = require_content_limits.isSafeImageUrl;
|
|
23
31
|
exports.migrateDocument = require_migrate_document.migrateDocument;
|
|
32
|
+
exports.moveToIndex = require_layers.moveToIndex;
|
|
24
33
|
exports.refuseUnsafeImageUrls = require_content_limits.refuseUnsafeImageUrls;
|
|
34
|
+
exports.renderDocuments = require_batch_render.renderDocuments;
|
|
25
35
|
exports.secureDocument = require_content_limits.secureDocument;
|
|
36
|
+
exports.sendBackward = require_layers.sendBackward;
|
|
37
|
+
exports.sendToBack = require_layers.sendToBack;
|
|
26
38
|
exports.validateDocument = require_validate_document.validateDocument;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { CURRENT_SCHEMA_VERSION, DocumentCanvas, DocumentInfo, FabricDocument, SerializedFabricObject } from "./document/document-format.cjs";
|
|
2
|
+
import { ImageFailureReason, ImageLoadFailure } from "./assets/image-check.cjs";
|
|
2
3
|
import { AssetManifest, FontAsset, ImageAsset } from "./assets/asset-manifest.cjs";
|
|
3
4
|
import { FontLoader } from "./assets/font-check.cjs";
|
|
4
5
|
import { AssetOptions, AssetReport, AssetWarning, AssetWarningCode, UploadRequest } from "./assets/asset-pipeline.cjs";
|
|
5
|
-
import { ExportArea, ExportBackground, ExportFormat, ExportOptions, ExportRect } from "./export/export-options.cjs";
|
|
6
|
+
import { ExportArea, ExportBackground, ExportFormat, ExportOptions, ExportRect, FontSource, SvgExportOptions } from "./export/export-options.cjs";
|
|
6
7
|
import { ExportPreflight, ExportProblem, ExportProblemCode } from "./export/preflight-export.cjs";
|
|
8
|
+
import { ContentLimits, isSafeImageUrl, refuseUnsafeImageUrls, secureDocument } from "./security/content-limits.cjs";
|
|
9
|
+
import { SvgImportOptions, SvgImportResult, SvgImportWarning, SvgImportWarningCode } from "./import/svg-import.cjs";
|
|
7
10
|
import { NewDocumentOptions } from "./document/create-document.cjs";
|
|
8
11
|
import { CustomObjectDefinition } from "./fabric/object-registry.cjs";
|
|
9
12
|
import { HistoryOptions, HistoryState } from "./history/create-history.cjs";
|
|
10
|
-
import { ContentLimits, isSafeImageUrl, refuseUnsafeImageUrls, secureDocument } from "./security/content-limits.cjs";
|
|
11
13
|
import { DocumentVersion, VersionKind, VersionOptions, VersionStorage, VersionSummary } from "./versions/document-version.cjs";
|
|
12
14
|
import { RecoveryStore } from "./recovery/recovery-store.cjs";
|
|
13
15
|
import { InterruptedLoad, RecoveryOptions, RecoveryRecord } from "./recovery/recovery-controller.cjs";
|
|
@@ -17,11 +19,14 @@ import { DocumentEngineError, DocumentEngineErrorDetails, DocumentErrorCode, Doc
|
|
|
17
19
|
import { DocumentStorage, SaveContext, SaveResult } from "./storage/storage-contract.cjs";
|
|
18
20
|
import { SaveOptions, SaveRetryEvent, SaveState, SaveStatus } from "./save/save-controller.cjs";
|
|
19
21
|
import { Unsubscribe } from "./engine/event-emitter.cjs";
|
|
20
|
-
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, NewDocumentRequest, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
22
|
+
import { DocumentEngine, DocumentEngineEvents, DocumentEngineOptions, ExportResult, ImportOptions, LoadOptions, LoadProgress, LoadStage, NewDocumentRequest, createDocumentEngine } from "./engine/create-document-engine.cjs";
|
|
21
23
|
import { validateDocument } from "./document/validate-document.cjs";
|
|
22
24
|
import { KeyboardShortcutOptions, UndoRedoTarget, bindKeyboardShortcuts } from "./history/keyboard-shortcuts.cjs";
|
|
23
25
|
import { UnsavedChangesSource, bindUnsavedChangesWarning } from "./save/unsaved-changes-warning.cjs";
|
|
24
26
|
import { DownloadableExport, downloadExport } from "./export/download-export.cjs";
|
|
27
|
+
import { RenderDocumentsOptions, RenderedDocument, renderDocuments } from "./export/batch-render.cjs";
|
|
25
28
|
import { Migration, MigrationContext, MigrationResult, detectSchemaVersion, migrateDocument } from "./migrations/migrate-document.cjs";
|
|
26
29
|
import { DocumentState, DocumentStateStore, createDocumentStateStore } from "./state/document-state-store.cjs";
|
|
27
|
-
|
|
30
|
+
import { CLIPBOARD_FORMAT, Clipboard, ClipboardContent, ClipboardOptions, PasteOptions, createClipboard } from "./commands/clipboard.cjs";
|
|
31
|
+
import { LayerInfo, LayerOptions, LayerPin, bringForward, bringToFront, getLayers, moveToIndex, sendBackward, sendToBack } from "./commands/layers.cjs";
|
|
32
|
+
export { type AssetManifest, type AssetOptions, type AssetReport, type AssetWarning, type AssetWarningCode, type AutosaveOptions, CLIPBOARD_FORMAT, CURRENT_SCHEMA_VERSION, type Clipboard, type ClipboardContent, type ClipboardOptions, type ContentLimits, type CustomObjectDefinition, type DocumentCanvas, type DocumentEngine, DocumentEngineError, type DocumentEngineErrorDetails, type DocumentEngineEvents, type DocumentEngineOptions, type DocumentErrorCode, type DocumentInfo, type DocumentIssue, type DocumentState, type DocumentStateStore, type DocumentStorage, type DocumentVersion, type DownloadableExport, type ExportArea, type ExportBackground, type ExportFormat, type ExportOptions, type ExportPreflight, type ExportProblem, type ExportProblemCode, type ExportRect, type ExportResult, type FabricDocument, type FontAsset, type FontLoader, type FontSource, type HistoryOptions, type HistoryState, type ImageAsset, type ImageFailureReason, type ImageLoadFailure, type ImportOptions, type InterruptedLoad, type KeyboardShortcutOptions, type LayerInfo, type LayerOptions, type LayerPin, type LoadOptions, type LoadProgress, type LoadStage, type Migration, type MigrationContext, type MigrationResult, type NewDocumentOptions, type NewDocumentRequest, type PasteOptions, type RecoveryOptions, type RecoveryRecord, type RecoveryStore, type RenderDocumentsOptions, type RenderedDocument, type RetryOptions, type SaveContext, type SaveOptions, type SaveResult, type SaveRetryEvent, type SaveState, type SaveStatus, type SerializedFabricObject, type SvgExportOptions, type SvgImportOptions, type SvgImportResult, type SvgImportWarning, type SvgImportWarningCode, type UndoRedoTarget, type UnsavedChangesSource, type Unsubscribe, type UploadRequest, type VersionKind, type VersionOptions, type VersionStorage, type VersionSummary, bindKeyboardShortcuts, bindUnsavedChangesWarning, bringForward, bringToFront, createClipboard, createConflictError, createDocumentEngine, createDocumentStateStore, detectSchemaVersion, downloadExport, getLayers, isDocumentEngineError, isSafeImageUrl, migrateDocument, moveToIndex, refuseUnsafeImageUrls, renderDocuments, secureDocument, sendBackward, sendToBack, validateDocument };
|