fabricjs-document-engine 1.1.0 → 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 +39 -1
- package/README.zh-CN.md +39 -1
- package/dist/assets/asset-pipeline.cjs +10 -8
- package/dist/assets/asset-pipeline.d.cts +1 -1
- package/dist/assets/asset-pipeline.d.ts +1 -1
- package/dist/assets/asset-pipeline.js +10 -8
- package/dist/assets/image-check.cjs +4 -3
- package/dist/assets/image-check.d.cts +1 -1
- package/dist/assets/image-check.d.ts +1 -1
- package/dist/assets/image-check.js +4 -3
- package/dist/commands/clipboard.cjs +5 -0
- package/dist/commands/clipboard.js +5 -0
- package/dist/document/document-format.d.cts +15 -0
- package/dist/document/document-format.d.ts +15 -0
- package/dist/document/validate-document.cjs +8 -0
- package/dist/document/validate-document.js +8 -0
- package/dist/engine/create-document-engine.cjs +115 -21
- package/dist/engine/create-document-engine.d.cts +24 -6
- package/dist/engine/create-document-engine.d.ts +24 -6
- package/dist/engine/create-document-engine.js +116 -22
- package/dist/engine/errors.d.cts +1 -1
- package/dist/engine/errors.d.ts +1 -1
- package/dist/export/export-options.cjs +3 -0
- package/dist/export/export-options.d.cts +6 -0
- package/dist/export/export-options.d.ts +6 -0
- package/dist/export/export-options.js +3 -0
- package/dist/export/preflight-export.d.cts +1 -1
- package/dist/export/preflight-export.d.ts +1 -1
- package/dist/export/render-export.cjs +42 -4
- package/dist/export/render-export.js +43 -5
- package/dist/export/svg/clip-masks.cjs +39 -0
- package/dist/export/svg/clip-masks.js +37 -0
- package/dist/export/svg/embed-assets.cjs +1 -0
- package/dist/export/svg/embed-assets.js +1 -0
- package/dist/export/svg/raster-object.cjs +82 -0
- package/dist/export/svg/raster-object.js +81 -0
- package/dist/{pdf → export/svg}/text-decorations.cjs +3 -3
- package/dist/{pdf → export/svg}/text-decorations.js +3 -3
- package/dist/export/svg/text-on-path.cjs +0 -10
- package/dist/export/svg/text-on-path.js +1 -10
- package/dist/fabric/fabric-adapter.cjs +5 -3
- package/dist/fabric/fabric-adapter.js +5 -3
- package/dist/fabric/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/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/migrations/migrate-document.cjs +2 -1
- package/dist/migrations/migrate-document.js +2 -1
- package/dist/pdf/export-pdf.cjs +8 -67
- package/dist/pdf/export-pdf.js +8 -67
- package/dist/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 +16 -3
- package/dist/util/concurrency.js +16 -3
- package/package.json +1 -1
- package/schema/document-v1.schema.json +5 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildAssetManifest } from "../assets/asset-manifest.js";
|
|
2
|
+
import { documentObjects, pickPageFields } from "../fabric/page-state.js";
|
|
2
3
|
import { DocumentEngineError, isDocumentEngineError } from "./errors.js";
|
|
3
|
-
import { isSafeImageUrl, secureDocument } from "../security/content-limits.js";
|
|
4
|
+
import { canvasSizeProblem, isSafeImageUrl, secureDocument } from "../security/content-limits.js";
|
|
4
5
|
import { isSameUrl } from "../assets/image-check.js";
|
|
5
6
|
import { inspectAssets, prepareAssetsForLoad, prepareAssetsForSave } from "../assets/asset-pipeline.js";
|
|
6
7
|
import { resolveExportArea } from "../export/export-area.js";
|
|
@@ -38,11 +39,21 @@ function createDocumentEngine(options) {
|
|
|
38
39
|
const registry = createObjectRegistry(options.customObjects);
|
|
39
40
|
const events = createEventEmitter();
|
|
40
41
|
const objectsById = /* @__PURE__ */ new Map();
|
|
41
|
-
const assetOptions =
|
|
42
|
+
const assetOptions = {
|
|
43
|
+
...options.assets,
|
|
44
|
+
maxImagePixels: options.limits?.maxImagePixels ?? 67108864
|
|
45
|
+
};
|
|
42
46
|
const uploadedUrls = /* @__PURE__ */ new Map();
|
|
43
47
|
let documentInfo = createDocumentInfo(options.document);
|
|
44
48
|
let activeLoad = null;
|
|
45
49
|
let destroyed = false;
|
|
50
|
+
let mutations = 0;
|
|
51
|
+
let session = 0;
|
|
52
|
+
/** Throws when async work started under `startedIn` would now change another document. */
|
|
53
|
+
function ensureSameSession(startedIn, what) {
|
|
54
|
+
ensureUsable();
|
|
55
|
+
if (session !== startedIn) throw new DocumentEngineError("DOCUMENT_CHANGED", `${what} was dropped because another document was opened while it ran`);
|
|
56
|
+
}
|
|
46
57
|
function ensureUsable() {
|
|
47
58
|
if (destroyed) throw new DocumentEngineError("ENGINE_DESTROYED", "This document engine was destroyed");
|
|
48
59
|
}
|
|
@@ -67,21 +78,41 @@ function createDocumentEngine(options) {
|
|
|
67
78
|
objectsById.clear();
|
|
68
79
|
canvas.getObjects().forEach(indexObjectTree);
|
|
69
80
|
}
|
|
70
|
-
const handleObjectAdded = ({ target }) =>
|
|
71
|
-
|
|
81
|
+
const handleObjectAdded = ({ target }) => {
|
|
82
|
+
mutations += 1;
|
|
83
|
+
indexObjectTree(target);
|
|
84
|
+
};
|
|
85
|
+
const handleObjectRemoved = ({ target }) => {
|
|
86
|
+
mutations += 1;
|
|
87
|
+
removeObjectTreeFromIndex(target);
|
|
88
|
+
};
|
|
89
|
+
const countMutation = () => {
|
|
90
|
+
mutations += 1;
|
|
91
|
+
};
|
|
72
92
|
canvas.on("object:added", handleObjectAdded);
|
|
73
93
|
canvas.on("object:removed", handleObjectRemoved);
|
|
94
|
+
canvas.on("object:modified", countMutation);
|
|
95
|
+
canvas.on("text:changed", countMutation);
|
|
74
96
|
rebuildIndex();
|
|
75
97
|
const history = createHistory({
|
|
76
98
|
canvas,
|
|
77
99
|
limit: options.history?.limit,
|
|
78
100
|
maxBytes: options.history?.maxBytes,
|
|
79
|
-
|
|
101
|
+
serializeState: () => {
|
|
80
102
|
rebuildIndex();
|
|
81
|
-
|
|
103
|
+
const serialized = serializeCanvas(canvas, registry.propertiesToInclude());
|
|
104
|
+
return {
|
|
105
|
+
objects: serialized.objects,
|
|
106
|
+
page: {
|
|
107
|
+
width: canvas.getWidth(),
|
|
108
|
+
height: canvas.getHeight(),
|
|
109
|
+
...pickPageFields(serialized)
|
|
110
|
+
}
|
|
111
|
+
};
|
|
82
112
|
},
|
|
83
113
|
onChange: (state) => events.emit("history:change", state),
|
|
84
|
-
onContentChange: () => noteContentChange()
|
|
114
|
+
onContentChange: () => noteContentChange(),
|
|
115
|
+
onLiveChange: () => noteContentChange()
|
|
85
116
|
});
|
|
86
117
|
const saving = createSaveController({
|
|
87
118
|
getStorage: () => requireStorage(),
|
|
@@ -116,6 +147,7 @@ function createDocumentEngine(options) {
|
|
|
116
147
|
}
|
|
117
148
|
}) : void 0;
|
|
118
149
|
function noteContentChange() {
|
|
150
|
+
mutations += 1;
|
|
119
151
|
saving.noteContentChange();
|
|
120
152
|
recovery?.schedule();
|
|
121
153
|
}
|
|
@@ -138,11 +170,14 @@ function createDocumentEngine(options) {
|
|
|
138
170
|
height: canvas.getHeight()
|
|
139
171
|
},
|
|
140
172
|
objects: serialized.objects,
|
|
141
|
-
assets: buildAssetManifest(
|
|
173
|
+
assets: buildAssetManifest(documentObjects({
|
|
174
|
+
objects: serialized.objects,
|
|
175
|
+
canvas: serialized
|
|
176
|
+
})),
|
|
142
177
|
metadata: { ...documentInfo.metadata }
|
|
143
178
|
};
|
|
144
179
|
if (serialized.version !== void 0) document.fabricVersion = serialized.version;
|
|
145
|
-
|
|
180
|
+
Object.assign(document.canvas, pickPageFields(serialized));
|
|
146
181
|
return document;
|
|
147
182
|
}
|
|
148
183
|
function toLoadError(error, controller) {
|
|
@@ -161,7 +196,7 @@ function createDocumentEngine(options) {
|
|
|
161
196
|
throw new DocumentEngineError(issues[0].code, `The document is not valid: ${summary}`, { issues });
|
|
162
197
|
}
|
|
163
198
|
const document = input;
|
|
164
|
-
const unknownTypes = registry.findUnknownTypes(collectSerializedTypes(document
|
|
199
|
+
const unknownTypes = registry.findUnknownTypes(collectSerializedTypes(documentObjects(document)));
|
|
165
200
|
if (unknownTypes.length > 0) throw new DocumentEngineError("UNKNOWN_OBJECT_TYPE", `The document uses object types that are not registered: ${unknownTypes.join(", ")}. Register them with customObjects or engine.registerObject before loading.`, { unknownTypes });
|
|
166
201
|
return document;
|
|
167
202
|
}
|
|
@@ -199,6 +234,7 @@ function createDocumentEngine(options) {
|
|
|
199
234
|
ensureUsable();
|
|
200
235
|
protectUnsavedChanges(loadOptions.discardUnsavedChanges);
|
|
201
236
|
const controller = startedLoad ?? startLoad(loadOptions.signal);
|
|
237
|
+
const mutationsAtStart = mutations;
|
|
202
238
|
const documentId = describeDocumentId(input);
|
|
203
239
|
const report = (stage, done, total) => {
|
|
204
240
|
if (controller.signal.aborted) return;
|
|
@@ -228,11 +264,15 @@ function createDocumentEngine(options) {
|
|
|
228
264
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
229
265
|
await history.withoutRecording(() => loadIntoCanvas(canvas, {
|
|
230
266
|
version: document.fabricVersion,
|
|
231
|
-
|
|
267
|
+
...pickPageFields(document.canvas),
|
|
232
268
|
objects: document.objects
|
|
233
269
|
}, {
|
|
234
270
|
signal: controller.signal,
|
|
235
|
-
onObjects: (done, total) => report("objects", done, total)
|
|
271
|
+
onObjects: (done, total) => report("objects", done, total),
|
|
272
|
+
beforeSwap: () => {
|
|
273
|
+
if (mutations !== mutationsAtStart && !loadOptions.discardUnsavedChanges) throw new DocumentEngineError("LOAD_CONFLICT", "The canvas was edited while the document loaded, so the load was stopped to keep the edits. Pass { discardUnsavedChanges: true } to replace them.");
|
|
274
|
+
session += 1;
|
|
275
|
+
}
|
|
236
276
|
}));
|
|
237
277
|
if (controller.signal.aborted) throw new Error("aborted");
|
|
238
278
|
if (loadOptions.restoreCanvasSize ?? true) canvas.setDimensions({
|
|
@@ -285,10 +325,37 @@ function createDocumentEngine(options) {
|
|
|
285
325
|
metadata
|
|
286
326
|
});
|
|
287
327
|
}
|
|
328
|
+
function setPage(changes, label = "Change page") {
|
|
329
|
+
ensureUsable();
|
|
330
|
+
const problem = canvasSizeProblem(changes.width ?? canvas.getWidth(), changes.height ?? canvas.getHeight(), options.limits);
|
|
331
|
+
if (problem) throw new DocumentEngineError("UNSAFE_DOCUMENT", `The page was not resized: it would be ${problem}`);
|
|
332
|
+
history.transaction(label, () => {
|
|
333
|
+
const width = changes.width ?? canvas.getWidth();
|
|
334
|
+
const height = changes.height ?? canvas.getHeight();
|
|
335
|
+
if (width !== canvas.getWidth() || height !== canvas.getHeight()) canvas.setDimensions({
|
|
336
|
+
width,
|
|
337
|
+
height
|
|
338
|
+
});
|
|
339
|
+
const live = {};
|
|
340
|
+
if (changes.background !== void 0) live.backgroundColor = changes.background ?? "";
|
|
341
|
+
if (changes.backgroundImage !== void 0) live.backgroundImage = changes.backgroundImage ?? void 0;
|
|
342
|
+
if (changes.overlay !== void 0) live.overlayColor = changes.overlay ?? "";
|
|
343
|
+
if (changes.overlayImage !== void 0) live.overlayImage = changes.overlayImage ?? void 0;
|
|
344
|
+
if (changes.clipPath !== void 0) live.clipPath = changes.clipPath ?? void 0;
|
|
345
|
+
canvas.set(live);
|
|
346
|
+
});
|
|
347
|
+
canvas.requestRenderAll();
|
|
348
|
+
}
|
|
288
349
|
async function importSvg(svg, importOptions = {}) {
|
|
289
350
|
ensureUsable();
|
|
351
|
+
const startedIn = session;
|
|
290
352
|
const result = await readSvg(svg, importOptions, options.limits);
|
|
291
|
-
|
|
353
|
+
try {
|
|
354
|
+
ensureSameSession(startedIn, "The SVG import");
|
|
355
|
+
} catch (error) {
|
|
356
|
+
result.objects.forEach((object) => object.dispose?.());
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
292
359
|
if (result.objects.length > 0) {
|
|
293
360
|
history.transaction("Import SVG", () => canvas.add(...result.objects));
|
|
294
361
|
canvas.requestRenderAll();
|
|
@@ -408,6 +475,7 @@ function createDocumentEngine(options) {
|
|
|
408
475
|
protectUnsavedChanges(newOptions.discardUnsavedChanges);
|
|
409
476
|
activeLoad?.abort();
|
|
410
477
|
history.withoutRecording(() => canvas.clear());
|
|
478
|
+
session += 1;
|
|
411
479
|
objectsById.clear();
|
|
412
480
|
documentInfo = createDocumentInfo(newOptions);
|
|
413
481
|
uploadedUrls.clear();
|
|
@@ -434,10 +502,12 @@ function createDocumentEngine(options) {
|
|
|
434
502
|
if (isReplaceableImage(object) && isSameUrl(object.getSrc(), oldUrl)) images.push(object);
|
|
435
503
|
});
|
|
436
504
|
if (images.length === 0) return 0;
|
|
505
|
+
const startedIn = session;
|
|
437
506
|
await history.transaction("Replace image", () => Promise.all(images.map(async (image) => {
|
|
438
507
|
const displayedWidth = image.width * image.scaleX;
|
|
439
508
|
const displayedHeight = image.height * image.scaleY;
|
|
440
509
|
await image.setSrc(newUrl, { crossOrigin: image.crossOrigin ?? null });
|
|
510
|
+
ensureSameSession(startedIn, "Replacing the image");
|
|
441
511
|
image.set({
|
|
442
512
|
scaleX: displayedWidth / image.width,
|
|
443
513
|
scaleY: displayedHeight / image.height
|
|
@@ -486,14 +556,31 @@ function createDocumentEngine(options) {
|
|
|
486
556
|
const warnings = [...check.warnings];
|
|
487
557
|
const rendered = withExportView(canvas, settings.background, format, () => {
|
|
488
558
|
const area = resolveExportArea(canvas, settings.area, settings.padding);
|
|
559
|
+
if (format !== "svg") {
|
|
560
|
+
const problem = canvasSizeProblem(area.width * settings.scale, area.height * settings.scale, options.limits);
|
|
561
|
+
if (problem) {
|
|
562
|
+
const message = `The export would be ${problem}. Lower the scale or export a smaller area.`;
|
|
563
|
+
throw new DocumentEngineError("EXPORT_BLOCKED", message, { problems: [{
|
|
564
|
+
code: "TOO_LARGE",
|
|
565
|
+
message,
|
|
566
|
+
objectIds: []
|
|
567
|
+
}] });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
489
570
|
return {
|
|
490
571
|
area,
|
|
491
|
-
content: format === "svg" ? Promise.resolve(renderSvg(canvas, area, settings.scale, settings.svg)) : renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
572
|
+
content: format === "svg" ? Promise.resolve(renderSvg(canvas, area, settings.scale, settings.svg, options.limits)) : renderRaster(canvas, area, format, settings.scale, settings.quality)
|
|
492
573
|
};
|
|
493
574
|
});
|
|
494
575
|
let blob;
|
|
495
576
|
if (format === "svg") {
|
|
496
|
-
const
|
|
577
|
+
const { svg, rasterizedObjectIds } = await rendered.content;
|
|
578
|
+
if (rasterizedObjectIds.length > 0) warnings.push({
|
|
579
|
+
code: "CLIP_PATH_RASTERIZED",
|
|
580
|
+
message: "Objects whose clip path has a clip path of its own were drawn as pictures: Fabric cannot write nested clip paths as SVG.",
|
|
581
|
+
objectIds: rasterizedObjectIds
|
|
582
|
+
});
|
|
583
|
+
const embedded = await embedSvgAssets(svg, canvas, settings.svg, settings.signal, options.limits?.isAllowedUrl ?? isSafeImageUrl);
|
|
497
584
|
stopIfCancelled();
|
|
498
585
|
if (settings.svg.embedImages === "require" && embedded.problems.length > 0) {
|
|
499
586
|
const problems = embedded.warnings.filter((warning) => warning.code === "IMAGE_NOT_EMBEDDED").map((warning) => ({
|
|
@@ -555,12 +642,14 @@ function createDocumentEngine(options) {
|
|
|
555
642
|
if (!recovery) throw new DocumentEngineError("RECOVERY_MISSING", "Pass recovery: { store } to createDocumentEngine to use recovery");
|
|
556
643
|
return recovery;
|
|
557
644
|
}
|
|
558
|
-
async function restoreRecovery(documentId,
|
|
645
|
+
async function restoreRecovery(documentId, restoreOptions = {}) {
|
|
559
646
|
const source = requireRecovery();
|
|
560
647
|
const id = documentId ?? documentInfo.id;
|
|
561
|
-
const
|
|
648
|
+
const { sessionId: branch, ...loadOptions } = restoreOptions;
|
|
649
|
+
const record = await source.read(id, branch);
|
|
562
650
|
if (!record) throw new DocumentEngineError("RECOVERY_NOT_FOUND", `There is no recovery copy for document "${id}"`);
|
|
563
651
|
const loaded = await loadDocument(await restoreRecordedFiles(record), loadOptions);
|
|
652
|
+
source.adopt(loaded.id, record.sessionId);
|
|
564
653
|
noteContentChange();
|
|
565
654
|
events.emit("recovery:restored", { document: loaded });
|
|
566
655
|
return loaded;
|
|
@@ -581,12 +670,15 @@ function createDocumentEngine(options) {
|
|
|
581
670
|
const { disposed } = canvas;
|
|
582
671
|
if (recovery && saving.state().isDirty && !disposed) recovery.writeNow();
|
|
583
672
|
destroyed = true;
|
|
673
|
+
session += 1;
|
|
584
674
|
activeLoad?.abort();
|
|
585
675
|
history.destroy();
|
|
586
676
|
saving.destroy();
|
|
587
677
|
recovery?.destroy();
|
|
588
678
|
canvas.off("object:added", handleObjectAdded);
|
|
589
679
|
canvas.off("object:removed", handleObjectRemoved);
|
|
680
|
+
canvas.off("object:modified", countMutation);
|
|
681
|
+
canvas.off("text:changed", countMutation);
|
|
590
682
|
objectsById.clear();
|
|
591
683
|
events.clear();
|
|
592
684
|
}
|
|
@@ -594,7 +686,8 @@ function createDocumentEngine(options) {
|
|
|
594
686
|
canvas,
|
|
595
687
|
getDocumentInfo: () => ({
|
|
596
688
|
...documentInfo,
|
|
597
|
-
metadata: { ...documentInfo.metadata }
|
|
689
|
+
metadata: { ...documentInfo.metadata },
|
|
690
|
+
session
|
|
598
691
|
}),
|
|
599
692
|
updateMetadata(changes) {
|
|
600
693
|
ensureUsable();
|
|
@@ -635,14 +728,15 @@ function createDocumentEngine(options) {
|
|
|
635
728
|
preflightExport: checkBeforeExport,
|
|
636
729
|
flushRecovery: async () => requireRecovery().flush(),
|
|
637
730
|
getRecoverableDocuments: async () => requireRecovery().list(),
|
|
638
|
-
getRecovery: async (documentId) => requireRecovery().read(documentId ?? documentInfo.id),
|
|
731
|
+
getRecovery: async (documentId, sessionId) => requireRecovery().read(documentId ?? documentInfo.id, sessionId),
|
|
639
732
|
restoreRecovery,
|
|
640
|
-
discardRecovery: async (documentId) => requireRecovery().
|
|
733
|
+
discardRecovery: async (documentId, sessionId) => requireRecovery().discard(documentId ?? documentInfo.id, sessionId),
|
|
641
734
|
getInterruptedLoad: async () => requireRecovery().interruptedLoad(),
|
|
642
|
-
transaction(label, work) {
|
|
735
|
+
transaction(label, work, transactionOptions) {
|
|
643
736
|
ensureUsable();
|
|
644
|
-
return history.transaction(label, work);
|
|
737
|
+
return history.transaction(label, work, transactionOptions);
|
|
645
738
|
},
|
|
739
|
+
setPage,
|
|
646
740
|
commit(label = "Edit") {
|
|
647
741
|
ensureUsable();
|
|
648
742
|
return history.commit(label);
|
package/dist/engine/errors.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.cjs";
|
|
2
2
|
import { ExportProblem } from "../export/preflight-export.cjs";
|
|
3
3
|
//#region src/engine/errors.d.ts
|
|
4
|
-
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "SVG_IMPORT_FAILED" | "PDF_UNAVAILABLE" | "PDF_FAILED" | "ENGINE_DESTROYED";
|
|
4
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "SVG_IMPORT_FAILED" | "LOAD_CONFLICT" | "DOCUMENT_CHANGED" | "PDF_UNAVAILABLE" | "PDF_FAILED" | "ENGINE_DESTROYED";
|
|
5
5
|
export interface DocumentIssue {
|
|
6
6
|
code: DocumentErrorCode;
|
|
7
7
|
path: string;
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FontAsset, ImageAsset } from "../assets/asset-manifest.js";
|
|
2
2
|
import { ExportProblem } from "../export/preflight-export.js";
|
|
3
3
|
//#region src/engine/errors.d.ts
|
|
4
|
-
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "SVG_IMPORT_FAILED" | "PDF_UNAVAILABLE" | "PDF_FAILED" | "ENGINE_DESTROYED";
|
|
4
|
+
export type DocumentErrorCode = "INVALID_DOCUMENT" | "UNSAFE_DOCUMENT" | "UNSUPPORTED_SCHEMA" | "UNKNOWN_OBJECT_TYPE" | "INVALID_CUSTOM_OBJECT" | "LOAD_ABORTED" | "LOAD_FAILED" | "MISSING_ASSETS" | "MISSING_FONTS" | "ASSET_UPLOAD_FAILED" | "STORAGE_MISSING" | "UNSAVED_CHANGES" | "SAVE_FAILED" | "SAVE_CONFLICT" | "SAVE_CANCELLED" | "DOCUMENT_NOT_FOUND" | "HISTORY_FAILED" | "RECOVERY_MISSING" | "RECOVERY_NOT_FOUND" | "RECOVERY_FAILED" | "MIGRATION_FAILED" | "VERSIONS_UNSUPPORTED" | "VERSION_NOT_FOUND" | "VERSION_FAILED" | "INVALID_EXPORT_OPTIONS" | "EXPORT_BLOCKED" | "EXPORT_FAILED" | "EXPORT_ABORTED" | "SVG_IMPORT_FAILED" | "LOAD_CONFLICT" | "DOCUMENT_CHANGED" | "PDF_UNAVAILABLE" | "PDF_FAILED" | "ENGINE_DESTROYED";
|
|
5
5
|
export interface DocumentIssue {
|
|
6
6
|
code: DocumentErrorCode;
|
|
7
7
|
path: string;
|
|
@@ -36,6 +36,8 @@ function normalizeExportOptions(options) {
|
|
|
36
36
|
const svg = options.svg ?? {};
|
|
37
37
|
const textOnPath = svg.textOnPath ?? "vector";
|
|
38
38
|
if (textOnPath !== "vector" && textOnPath !== "fabric") throw invalid("svg.textOnPath must be \"vector\" or \"fabric\"");
|
|
39
|
+
const textDecorations = svg.textDecorations ?? "shapes";
|
|
40
|
+
if (textDecorations !== "shapes" && textDecorations !== "css") throw invalid("svg.textDecorations must be \"shapes\" or \"css\"");
|
|
39
41
|
const embedImages = svg.embedImages ?? false;
|
|
40
42
|
if (embedImages !== true && embedImages !== false && embedImages !== "require") throw invalid("svg.embedImages must be true, false or \"require\"");
|
|
41
43
|
const maxEmbeddedImageBytes = svg.maxEmbeddedImageBytes ?? 26214400;
|
|
@@ -52,6 +54,7 @@ function normalizeExportOptions(options) {
|
|
|
52
54
|
signal: options.signal,
|
|
53
55
|
svg: {
|
|
54
56
|
textOnPath,
|
|
57
|
+
textDecorations,
|
|
55
58
|
embedImages,
|
|
56
59
|
maxEmbeddedImageBytes,
|
|
57
60
|
embedFonts
|
|
@@ -18,6 +18,12 @@ export interface SvgExportOptions {
|
|
|
18
18
|
* backgrounds and underlines straight.
|
|
19
19
|
*/
|
|
20
20
|
textOnPath?: "vector" | "fabric";
|
|
21
|
+
/**
|
|
22
|
+
* How underlines, overlines and line-throughs of ordinary text are written.
|
|
23
|
+
* `shapes` (the default) draws them where the canvas does; `css` keeps
|
|
24
|
+
* Fabric's `text-decoration`, which readers place in their own way.
|
|
25
|
+
*/
|
|
26
|
+
textDecorations?: "shapes" | "css";
|
|
21
27
|
/**
|
|
22
28
|
* Puts every image into the file as data, so the SVG opens anywhere
|
|
23
29
|
* without the original image URLs. `require` blocks the export when an
|
|
@@ -18,6 +18,12 @@ export interface SvgExportOptions {
|
|
|
18
18
|
* backgrounds and underlines straight.
|
|
19
19
|
*/
|
|
20
20
|
textOnPath?: "vector" | "fabric";
|
|
21
|
+
/**
|
|
22
|
+
* How underlines, overlines and line-throughs of ordinary text are written.
|
|
23
|
+
* `shapes` (the default) draws them where the canvas does; `css` keeps
|
|
24
|
+
* Fabric's `text-decoration`, which readers place in their own way.
|
|
25
|
+
*/
|
|
26
|
+
textDecorations?: "shapes" | "css";
|
|
21
27
|
/**
|
|
22
28
|
* Puts every image into the file as data, so the SVG opens anywhere
|
|
23
29
|
* without the original image URLs. `require` blocks the export when an
|
|
@@ -36,6 +36,8 @@ function normalizeExportOptions(options) {
|
|
|
36
36
|
const svg = options.svg ?? {};
|
|
37
37
|
const textOnPath = svg.textOnPath ?? "vector";
|
|
38
38
|
if (textOnPath !== "vector" && textOnPath !== "fabric") throw invalid("svg.textOnPath must be \"vector\" or \"fabric\"");
|
|
39
|
+
const textDecorations = svg.textDecorations ?? "shapes";
|
|
40
|
+
if (textDecorations !== "shapes" && textDecorations !== "css") throw invalid("svg.textDecorations must be \"shapes\" or \"css\"");
|
|
39
41
|
const embedImages = svg.embedImages ?? false;
|
|
40
42
|
if (embedImages !== true && embedImages !== false && embedImages !== "require") throw invalid("svg.embedImages must be true, false or \"require\"");
|
|
41
43
|
const maxEmbeddedImageBytes = svg.maxEmbeddedImageBytes ?? 26214400;
|
|
@@ -52,6 +54,7 @@ function normalizeExportOptions(options) {
|
|
|
52
54
|
signal: options.signal,
|
|
53
55
|
svg: {
|
|
54
56
|
textOnPath,
|
|
57
|
+
textDecorations,
|
|
55
58
|
embedImages,
|
|
56
59
|
maxEmbeddedImageBytes,
|
|
57
60
|
embedFonts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AssetWarning } from "../assets/asset-pipeline.cjs";
|
|
2
2
|
import { StaticCanvas } from "fabric";
|
|
3
3
|
//#region src/export/preflight-export.d.ts
|
|
4
|
-
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT" | "IMAGE_NOT_EMBEDDED";
|
|
4
|
+
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT" | "IMAGE_NOT_EMBEDDED" | "TOO_LARGE";
|
|
5
5
|
export interface ExportProblem {
|
|
6
6
|
code: ExportProblemCode;
|
|
7
7
|
message: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AssetWarning } from "../assets/asset-pipeline.js";
|
|
2
2
|
import { StaticCanvas } from "fabric";
|
|
3
3
|
//#region src/export/preflight-export.d.ts
|
|
4
|
-
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT" | "IMAGE_NOT_EMBEDDED";
|
|
4
|
+
export type ExportProblemCode = "MISSING_IMAGE" | "CROSS_ORIGIN_IMAGE" | "MISSING_FONT" | "IMAGE_NOT_EMBEDDED" | "TOO_LARGE";
|
|
5
5
|
export interface ExportProblem {
|
|
6
6
|
code: ExportProblemCode;
|
|
7
7
|
message: string;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
const require_object_ids = require("../fabric/object-ids.cjs");
|
|
2
|
+
const require_overrides = require("./svg/overrides.cjs");
|
|
1
3
|
const require_text_on_path = require("./svg/text-on-path.cjs");
|
|
4
|
+
const require_clip_masks = require("./svg/clip-masks.cjs");
|
|
5
|
+
const require_raster_object = require("./svg/raster-object.cjs");
|
|
6
|
+
const require_text_decorations = require("./svg/text-decorations.cjs");
|
|
2
7
|
//#region src/export/render-export.ts
|
|
3
8
|
const identity = [
|
|
4
9
|
1,
|
|
@@ -51,8 +56,38 @@ function renderRaster(canvas, area, format, scale, quality) {
|
|
|
51
56
|
function repairFabricSvg(svg) {
|
|
52
57
|
return svg.replace(/(rotate="[^"]*")(style=)/g, "$1 $2");
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Writes the SVG with the engine's corrections on top of Fabric's output:
|
|
61
|
+
* curved text and decorations as the canvas draws them, inverted clip paths
|
|
62
|
+
* as masks, and objects with nested clip paths as pictures. Every other
|
|
63
|
+
* object is written by Fabric unchanged.
|
|
64
|
+
*/
|
|
65
|
+
function renderSvg(canvas, area, scale, options, limits) {
|
|
66
|
+
const textOnPath = (options?.textOnPath ?? "vector") === "vector";
|
|
67
|
+
const decorations = (options?.textDecorations ?? "shapes") === "shapes";
|
|
68
|
+
const rasterizedObjectIds = [];
|
|
69
|
+
const pictures = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const object of canvas.getObjects()) {
|
|
71
|
+
if (!require_clip_masks.hasNestedClipPath(object)) continue;
|
|
72
|
+
pictures.set(object, require_raster_object.rasterMarkup(canvas, object, Math.max(2, scale * 2), limits));
|
|
73
|
+
const id = require_object_ids.readObjectId(object);
|
|
74
|
+
if (id !== void 0) rasterizedObjectIds.push(id);
|
|
75
|
+
}
|
|
76
|
+
const baseWriter = (object) => {
|
|
77
|
+
if (textOnPath && require_text_on_path.isTextOnPath(object)) return (reviver) => require_text_on_path.textOnPathToSVG(object, reviver);
|
|
78
|
+
if (decorations && require_text_decorations.hasStraightDecorations(object)) return (reviver) => require_text_decorations.decoratedTextToSVG(object, reviver);
|
|
79
|
+
};
|
|
80
|
+
const writerFor = (object) => {
|
|
81
|
+
const picture = pictures.get(object);
|
|
82
|
+
if (picture !== void 0) return () => picture;
|
|
83
|
+
const base = baseWriter(object);
|
|
84
|
+
if (require_clip_masks.ownClipPathIsInverted(object)) {
|
|
85
|
+
const prototypeWriter = (reviver) => Object.getPrototypeOf(object).toSVG.call(object, reviver);
|
|
86
|
+
return (reviver) => require_clip_masks.writeWithInvertedMask(object, base ?? prototypeWriter, reviver);
|
|
87
|
+
}
|
|
88
|
+
return base;
|
|
89
|
+
};
|
|
90
|
+
const svg = require_overrides.withSvgOverrides(canvas, writerFor, () => canvas.toSVG({
|
|
56
91
|
viewBox: {
|
|
57
92
|
x: area.left,
|
|
58
93
|
y: area.top,
|
|
@@ -61,8 +96,11 @@ function renderSvg(canvas, area, scale, options) {
|
|
|
61
96
|
},
|
|
62
97
|
width: String(area.width * scale),
|
|
63
98
|
height: String(area.height * scale)
|
|
64
|
-
});
|
|
65
|
-
return
|
|
99
|
+
})).result;
|
|
100
|
+
return {
|
|
101
|
+
svg: repairFabricSvg(svg),
|
|
102
|
+
rasterizedObjectIds
|
|
103
|
+
};
|
|
66
104
|
}
|
|
67
105
|
//#endregion
|
|
68
106
|
exports.mimeTypes = mimeTypes;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readObjectId } from "../fabric/object-ids.js";
|
|
2
|
+
import { withSvgOverrides } from "./svg/overrides.js";
|
|
3
|
+
import { isTextOnPath, textOnPathToSVG } from "./svg/text-on-path.js";
|
|
4
|
+
import { hasNestedClipPath, ownClipPathIsInverted, writeWithInvertedMask } from "./svg/clip-masks.js";
|
|
5
|
+
import { rasterMarkup } from "./svg/raster-object.js";
|
|
6
|
+
import { decoratedTextToSVG, hasStraightDecorations } from "./svg/text-decorations.js";
|
|
2
7
|
//#region src/export/render-export.ts
|
|
3
8
|
const identity = [
|
|
4
9
|
1,
|
|
@@ -51,8 +56,38 @@ function renderRaster(canvas, area, format, scale, quality) {
|
|
|
51
56
|
function repairFabricSvg(svg) {
|
|
52
57
|
return svg.replace(/(rotate="[^"]*")(style=)/g, "$1 $2");
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Writes the SVG with the engine's corrections on top of Fabric's output:
|
|
61
|
+
* curved text and decorations as the canvas draws them, inverted clip paths
|
|
62
|
+
* as masks, and objects with nested clip paths as pictures. Every other
|
|
63
|
+
* object is written by Fabric unchanged.
|
|
64
|
+
*/
|
|
65
|
+
function renderSvg(canvas, area, scale, options, limits) {
|
|
66
|
+
const textOnPath = (options?.textOnPath ?? "vector") === "vector";
|
|
67
|
+
const decorations = (options?.textDecorations ?? "shapes") === "shapes";
|
|
68
|
+
const rasterizedObjectIds = [];
|
|
69
|
+
const pictures = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const object of canvas.getObjects()) {
|
|
71
|
+
if (!hasNestedClipPath(object)) continue;
|
|
72
|
+
pictures.set(object, rasterMarkup(canvas, object, Math.max(2, scale * 2), limits));
|
|
73
|
+
const id = readObjectId(object);
|
|
74
|
+
if (id !== void 0) rasterizedObjectIds.push(id);
|
|
75
|
+
}
|
|
76
|
+
const baseWriter = (object) => {
|
|
77
|
+
if (textOnPath && isTextOnPath(object)) return (reviver) => textOnPathToSVG(object, reviver);
|
|
78
|
+
if (decorations && hasStraightDecorations(object)) return (reviver) => decoratedTextToSVG(object, reviver);
|
|
79
|
+
};
|
|
80
|
+
const writerFor = (object) => {
|
|
81
|
+
const picture = pictures.get(object);
|
|
82
|
+
if (picture !== void 0) return () => picture;
|
|
83
|
+
const base = baseWriter(object);
|
|
84
|
+
if (ownClipPathIsInverted(object)) {
|
|
85
|
+
const prototypeWriter = (reviver) => Object.getPrototypeOf(object).toSVG.call(object, reviver);
|
|
86
|
+
return (reviver) => writeWithInvertedMask(object, base ?? prototypeWriter, reviver);
|
|
87
|
+
}
|
|
88
|
+
return base;
|
|
89
|
+
};
|
|
90
|
+
const svg = withSvgOverrides(canvas, writerFor, () => canvas.toSVG({
|
|
56
91
|
viewBox: {
|
|
57
92
|
x: area.left,
|
|
58
93
|
y: area.top,
|
|
@@ -61,8 +96,11 @@ function renderSvg(canvas, area, scale, options) {
|
|
|
61
96
|
},
|
|
62
97
|
width: String(area.width * scale),
|
|
63
98
|
height: String(area.height * scale)
|
|
64
|
-
});
|
|
65
|
-
return
|
|
99
|
+
})).result;
|
|
100
|
+
return {
|
|
101
|
+
svg: repairFabricSvg(svg),
|
|
102
|
+
rasterizedObjectIds
|
|
103
|
+
};
|
|
66
104
|
}
|
|
67
105
|
//#endregion
|
|
68
106
|
export { mimeTypes, renderRaster, renderSvg, repairFabricSvg, withExportView };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const require_walk_objects = require("../../fabric/walk-objects.cjs");
|
|
2
|
+
//#region src/export/svg/clip-masks.ts
|
|
3
|
+
/** True when a clip path anywhere in the object's tree has a clip path of its own. */
|
|
4
|
+
function hasNestedClipPath(object) {
|
|
5
|
+
let nested = false;
|
|
6
|
+
require_walk_objects.walkObjects([object], (child) => {
|
|
7
|
+
if (child.clipPath?.clipPath) nested = true;
|
|
8
|
+
});
|
|
9
|
+
return nested;
|
|
10
|
+
}
|
|
11
|
+
/** True when the object's own clip path is inverted (and simple). */
|
|
12
|
+
function ownClipPathIsInverted(object) {
|
|
13
|
+
const clipPath = object.clipPath;
|
|
14
|
+
return Boolean(clipPath?.inverted) && !clipPath?.clipPath;
|
|
15
|
+
}
|
|
16
|
+
const FAR = 1e6;
|
|
17
|
+
/**
|
|
18
|
+
* Turns the object's own `<clipPath>` in Fabric's markup into an inverting
|
|
19
|
+
* `<mask>`. `write` produces the markup with the clip path not inverted; the
|
|
20
|
+
* id Fabric gave the clip path picks out exactly this object's definition.
|
|
21
|
+
*/
|
|
22
|
+
function writeWithInvertedMask(object, write, reviver) {
|
|
23
|
+
const clipPath = object.clipPath;
|
|
24
|
+
clipPath.inverted = false;
|
|
25
|
+
let markup;
|
|
26
|
+
try {
|
|
27
|
+
markup = write(reviver);
|
|
28
|
+
} finally {
|
|
29
|
+
clipPath.inverted = true;
|
|
30
|
+
}
|
|
31
|
+
const id = clipPath.clipPathId;
|
|
32
|
+
if (!id) return markup;
|
|
33
|
+
const definition = new RegExp(`<clipPath id="${id}"\\s*>([\\s\\S]*?)</clipPath>`);
|
|
34
|
+
return markup.replace(definition, (_, shape) => `<mask id="${id}" maskUnits="userSpaceOnUse" x="-1000000" y="-1000000" width="${2 * FAR}" height="${2 * FAR}">\n<rect x="-1000000" y="-1000000" width="${2 * FAR}" height="${2 * FAR}" fill="white" />\n<g fill="black" stroke="none">${shape}</g>\n</mask>`).replace(`clip-path="url(#${id})"`, `mask="url(#${id})"`);
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
exports.hasNestedClipPath = hasNestedClipPath;
|
|
38
|
+
exports.ownClipPathIsInverted = ownClipPathIsInverted;
|
|
39
|
+
exports.writeWithInvertedMask = writeWithInvertedMask;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { walkObjects } from "../../fabric/walk-objects.js";
|
|
2
|
+
//#region src/export/svg/clip-masks.ts
|
|
3
|
+
/** True when a clip path anywhere in the object's tree has a clip path of its own. */
|
|
4
|
+
function hasNestedClipPath(object) {
|
|
5
|
+
let nested = false;
|
|
6
|
+
walkObjects([object], (child) => {
|
|
7
|
+
if (child.clipPath?.clipPath) nested = true;
|
|
8
|
+
});
|
|
9
|
+
return nested;
|
|
10
|
+
}
|
|
11
|
+
/** True when the object's own clip path is inverted (and simple). */
|
|
12
|
+
function ownClipPathIsInverted(object) {
|
|
13
|
+
const clipPath = object.clipPath;
|
|
14
|
+
return Boolean(clipPath?.inverted) && !clipPath?.clipPath;
|
|
15
|
+
}
|
|
16
|
+
const FAR = 1e6;
|
|
17
|
+
/**
|
|
18
|
+
* Turns the object's own `<clipPath>` in Fabric's markup into an inverting
|
|
19
|
+
* `<mask>`. `write` produces the markup with the clip path not inverted; the
|
|
20
|
+
* id Fabric gave the clip path picks out exactly this object's definition.
|
|
21
|
+
*/
|
|
22
|
+
function writeWithInvertedMask(object, write, reviver) {
|
|
23
|
+
const clipPath = object.clipPath;
|
|
24
|
+
clipPath.inverted = false;
|
|
25
|
+
let markup;
|
|
26
|
+
try {
|
|
27
|
+
markup = write(reviver);
|
|
28
|
+
} finally {
|
|
29
|
+
clipPath.inverted = true;
|
|
30
|
+
}
|
|
31
|
+
const id = clipPath.clipPathId;
|
|
32
|
+
if (!id) return markup;
|
|
33
|
+
const definition = new RegExp(`<clipPath id="${id}"\\s*>([\\s\\S]*?)</clipPath>`);
|
|
34
|
+
return markup.replace(definition, (_, shape) => `<mask id="${id}" maskUnits="userSpaceOnUse" x="-1000000" y="-1000000" width="${2 * FAR}" height="${2 * FAR}">\n<rect x="-1000000" y="-1000000" width="${2 * FAR}" height="${2 * FAR}" fill="white" />\n<g fill="black" stroke="none">${shape}</g>\n</mask>`).replace(`clip-path="url(#${id})"`, `mask="url(#${id})"`);
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { hasNestedClipPath, ownClipPathIsInverted, writeWithInvertedMask };
|
|
@@ -77,6 +77,7 @@ function imagesOnCanvas(canvas) {
|
|
|
77
77
|
return images;
|
|
78
78
|
}
|
|
79
79
|
function drawToDataUrl(element) {
|
|
80
|
+
if (element.naturalWidth * element.naturalHeight > 67108864) throw new Error("the picture is too large to copy");
|
|
80
81
|
const canvas = document.createElement("canvas");
|
|
81
82
|
canvas.width = element.naturalWidth;
|
|
82
83
|
canvas.height = element.naturalHeight;
|
|
@@ -77,6 +77,7 @@ function imagesOnCanvas(canvas) {
|
|
|
77
77
|
return images;
|
|
78
78
|
}
|
|
79
79
|
function drawToDataUrl(element) {
|
|
80
|
+
if (element.naturalWidth * element.naturalHeight > 67108864) throw new Error("the picture is too large to copy");
|
|
80
81
|
const canvas = document.createElement("canvas");
|
|
81
82
|
canvas.width = element.naturalWidth;
|
|
82
83
|
canvas.height = element.naturalHeight;
|