@bensitu/image-editor 1.4.2 → 1.5.1
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 +479 -130
- package/dist/image-editor.cjs +4185 -0
- package/dist/image-editor.cjs.map +7 -0
- package/dist/image-editor.esm.js +868 -377
- package/dist/image-editor.esm.js.map +3 -3
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -3
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +868 -377
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +868 -377
- package/dist/image-editor.js.map +3 -3
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +64 -22
- package/package.json +4 -3
- package/src/image-editor.js +818 -267
package/dist/image-editor.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file image-editor.js
|
|
5
5
|
* @module image-editor
|
|
6
|
-
* @version 1.
|
|
6
|
+
* @version 1.5.1
|
|
7
7
|
* @author Ben Situ
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
downsampleMimeType: null,
|
|
76
76
|
imageLoadTimeoutMs: 3e4,
|
|
77
77
|
exportMultiplier: 1,
|
|
78
|
+
maxExportPixels: 5e7,
|
|
78
79
|
exportImageAreaByDefault: true,
|
|
79
80
|
defaultMaskWidth: 50,
|
|
80
81
|
defaultMaskHeight: 80,
|
|
@@ -144,7 +145,9 @@
|
|
|
144
145
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
145
146
|
this._disposed = false;
|
|
146
147
|
this._initialized = false;
|
|
147
|
-
this.
|
|
148
|
+
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
149
|
+
this._cropRotationWarningEmitted = false;
|
|
150
|
+
this.onImageLoaded = typeof this.options.onImageLoaded === "function" ? this.options.onImageLoaded : null;
|
|
148
151
|
this.animationQueue = new AnimationQueue();
|
|
149
152
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
150
153
|
}
|
|
@@ -190,10 +193,12 @@
|
|
|
190
193
|
* Use this method to set up the editor UI before interacting with it.
|
|
191
194
|
*
|
|
192
195
|
* @param {Object} [idMap={}] - Optional mapping from logical element names to actual DOM element IDs.
|
|
193
|
-
* Supported keys include: canvas, canvasContainer,
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
196
|
+
* Supported keys include: canvas, canvasContainer, imagePlaceholder, scalePercentageInput,
|
|
197
|
+
* rotateLeftDegreesInput, rotateRightDegreesInput, rotateLeftButton, rotateRightButton,
|
|
198
|
+
* createMaskButton, removeSelectedMaskButton, removeAllMasksButton, mergeMasksButton,
|
|
199
|
+
* downloadImageButton, maskList, zoomInButton, zoomOutButton, resetImageTransformButton,
|
|
200
|
+
* undoButton, redoButton, imageInput, uploadArea, enterCropModeButton, applyCropButton,
|
|
201
|
+
* and cancelCropButton. Deprecated 1.x names remain supported as aliases.
|
|
197
202
|
*
|
|
198
203
|
* @returns {void}
|
|
199
204
|
*
|
|
@@ -202,11 +207,17 @@
|
|
|
202
207
|
* @example
|
|
203
208
|
* editor.init({
|
|
204
209
|
* canvas: 'myFabricCanvasId',
|
|
205
|
-
*
|
|
210
|
+
* downloadImageButton: 'myDownloadButtonId'
|
|
206
211
|
* });
|
|
207
212
|
*/
|
|
208
213
|
init(idMap = {}) {
|
|
209
|
-
if (!this._fabricLoaded)
|
|
214
|
+
if (!this._fabricLoaded) {
|
|
215
|
+
this._fabricLoaded = !!ensureFabric();
|
|
216
|
+
if (!this._fabricLoaded) {
|
|
217
|
+
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
210
221
|
if (this._initialized || this.canvas) this.dispose();
|
|
211
222
|
this._disposed = false;
|
|
212
223
|
this._initialized = true;
|
|
@@ -225,29 +236,49 @@
|
|
|
225
236
|
canvas: "fabricCanvas",
|
|
226
237
|
canvasContainer: null,
|
|
227
238
|
// Pass an ID here if you have a scrollable viewport container
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
imagePlaceholder: "imagePlaceholder",
|
|
240
|
+
imgPlaceholder: null,
|
|
241
|
+
scalePercentageInput: "scalePercentageInput",
|
|
242
|
+
scaleRate: null,
|
|
243
|
+
rotateLeftDegreesInput: "rotateLeftDegreesInput",
|
|
244
|
+
rotationLeftInput: null,
|
|
245
|
+
rotateRightDegreesInput: "rotateRightDegreesInput",
|
|
246
|
+
rotationRightInput: null,
|
|
247
|
+
rotateLeftButton: "rotateLeftButton",
|
|
248
|
+
rotateLeftBtn: null,
|
|
249
|
+
rotateRightButton: "rotateRightButton",
|
|
250
|
+
rotateRightBtn: null,
|
|
251
|
+
createMaskButton: "createMaskButton",
|
|
252
|
+
addMaskBtn: null,
|
|
253
|
+
removeSelectedMaskButton: "removeSelectedMaskButton",
|
|
254
|
+
removeMaskBtn: null,
|
|
255
|
+
removeAllMasksButton: "removeAllMasksButton",
|
|
256
|
+
removeAllMasksBtn: null,
|
|
257
|
+
mergeMasksButton: "mergeMasksButton",
|
|
258
|
+
mergeBtn: null,
|
|
259
|
+
downloadImageButton: "downloadImageButton",
|
|
260
|
+
downloadBtn: null,
|
|
239
261
|
maskList: "maskList",
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
zoomInButton: "zoomInButton",
|
|
263
|
+
zoomInBtn: null,
|
|
264
|
+
zoomOutButton: "zoomOutButton",
|
|
265
|
+
zoomOutBtn: null,
|
|
266
|
+
resetImageTransformButton: "resetImageTransformButton",
|
|
267
|
+
resetBtn: null,
|
|
268
|
+
undoButton: "undoButton",
|
|
269
|
+
undoBtn: null,
|
|
270
|
+
redoButton: "redoButton",
|
|
271
|
+
redoBtn: null,
|
|
245
272
|
imageInput: "imageInput",
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
uploadArea: null,
|
|
274
|
+
enterCropModeButton: "enterCropModeButton",
|
|
275
|
+
cropBtn: null,
|
|
276
|
+
applyCropButton: "applyCropButton",
|
|
277
|
+
applyCropBtn: null,
|
|
278
|
+
cancelCropButton: "cancelCropButton",
|
|
279
|
+
cancelCropBtn: null
|
|
249
280
|
};
|
|
250
|
-
this.elements = {
|
|
281
|
+
this.elements = this._resolveElementIdMap(idMap || {}, defaults);
|
|
251
282
|
this._elementCache = {};
|
|
252
283
|
this._initCanvas();
|
|
253
284
|
this._bindEvents();
|
|
@@ -255,11 +286,68 @@
|
|
|
255
286
|
this._updateMaskList();
|
|
256
287
|
this._updateUI();
|
|
257
288
|
if (this.options.initialImageBase64) {
|
|
258
|
-
this.loadImage(this.options.initialImageBase64);
|
|
289
|
+
this.loadImage(this.options.initialImageBase64).catch((error) => this._reportError("initialImageBase64 could not be loaded", error));
|
|
259
290
|
} else {
|
|
260
291
|
this._updatePlaceholderStatus();
|
|
261
292
|
}
|
|
262
293
|
}
|
|
294
|
+
_resolveElementIdMap(idMap, defaults) {
|
|
295
|
+
const resolved = { ...defaults, ...idMap };
|
|
296
|
+
this._resolveElementAliases(resolved, idMap, defaults, "imagePlaceholder", ["imgPlaceholder"]);
|
|
297
|
+
this._resolveElementAliases(resolved, idMap, defaults, "scalePercentageInput", ["scaleRate"]);
|
|
298
|
+
this._resolveElementAliases(resolved, idMap, defaults, "rotateLeftDegreesInput", ["rotationLeftInput"]);
|
|
299
|
+
this._resolveElementAliases(resolved, idMap, defaults, "rotateRightDegreesInput", ["rotationRightInput"]);
|
|
300
|
+
this._resolveElementAlias(resolved, idMap, defaults, "rotateLeftButton", "rotateLeftBtn");
|
|
301
|
+
this._resolveElementAlias(resolved, idMap, defaults, "rotateRightButton", "rotateRightBtn");
|
|
302
|
+
this._resolveElementAlias(resolved, idMap, defaults, "createMaskButton", "addMaskBtn");
|
|
303
|
+
this._resolveElementAliases(resolved, idMap, defaults, "removeSelectedMaskButton", ["removeMaskBtn"]);
|
|
304
|
+
this._resolveElementAlias(resolved, idMap, defaults, "removeAllMasksButton", "removeAllMasksBtn");
|
|
305
|
+
this._resolveElementAlias(resolved, idMap, defaults, "mergeMasksButton", "mergeBtn");
|
|
306
|
+
this._resolveElementAliases(resolved, idMap, defaults, "downloadImageButton", ["downloadBtn"]);
|
|
307
|
+
this._resolveElementAlias(resolved, idMap, defaults, "zoomInButton", "zoomInBtn");
|
|
308
|
+
this._resolveElementAlias(resolved, idMap, defaults, "zoomOutButton", "zoomOutBtn");
|
|
309
|
+
this._resolveElementAlias(resolved, idMap, defaults, "resetImageTransformButton", "resetBtn");
|
|
310
|
+
this._resolveElementAlias(resolved, idMap, defaults, "undoButton", "undoBtn");
|
|
311
|
+
this._resolveElementAlias(resolved, idMap, defaults, "redoButton", "redoBtn");
|
|
312
|
+
this._resolveElementAliases(resolved, idMap, defaults, "enterCropModeButton", ["cropBtn"]);
|
|
313
|
+
this._resolveElementAlias(resolved, idMap, defaults, "applyCropButton", "applyCropBtn");
|
|
314
|
+
this._resolveElementAlias(resolved, idMap, defaults, "cancelCropButton", "cancelCropBtn");
|
|
315
|
+
return resolved;
|
|
316
|
+
}
|
|
317
|
+
_resolveElementAlias(resolved, idMap, defaults, canonicalKey, deprecatedKey) {
|
|
318
|
+
this._resolveElementAliases(resolved, idMap, defaults, canonicalKey, [deprecatedKey]);
|
|
319
|
+
}
|
|
320
|
+
_resolveElementAliases(resolved, idMap, defaults, canonicalKey, deprecatedKeys) {
|
|
321
|
+
const hasCanonicalKey = Object.prototype.hasOwnProperty.call(idMap, canonicalKey);
|
|
322
|
+
if (hasCanonicalKey) {
|
|
323
|
+
resolved[canonicalKey] = idMap[canonicalKey];
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
let deprecatedValue;
|
|
327
|
+
let hasDeprecatedValue = false;
|
|
328
|
+
for (const deprecatedKey of deprecatedKeys) {
|
|
329
|
+
if (Object.prototype.hasOwnProperty.call(idMap, deprecatedKey)) {
|
|
330
|
+
if (!hasDeprecatedValue) {
|
|
331
|
+
deprecatedValue = idMap[deprecatedKey];
|
|
332
|
+
hasDeprecatedValue = true;
|
|
333
|
+
}
|
|
334
|
+
this._warnDeprecatedElementIdKey(deprecatedKey, canonicalKey);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (hasDeprecatedValue) {
|
|
338
|
+
resolved[canonicalKey] = deprecatedValue;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
resolved[canonicalKey] = defaults[canonicalKey];
|
|
342
|
+
}
|
|
343
|
+
_warnDeprecatedElementIdKey(deprecatedKey, canonicalKey) {
|
|
344
|
+
if (!this._deprecatedElementKeyWarnings) this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
345
|
+
if (this._deprecatedElementKeyWarnings.has(deprecatedKey)) return;
|
|
346
|
+
this._deprecatedElementKeyWarnings.add(deprecatedKey);
|
|
347
|
+
this._reportWarning(
|
|
348
|
+
`ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
263
351
|
_reportError(message, error = null) {
|
|
264
352
|
const handler = this.options && this.options.onError;
|
|
265
353
|
if (typeof handler !== "function") return;
|
|
@@ -276,6 +364,11 @@
|
|
|
276
364
|
} catch {
|
|
277
365
|
}
|
|
278
366
|
}
|
|
367
|
+
_notifyImageLoaded() {
|
|
368
|
+
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
369
|
+
const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
|
|
370
|
+
if (typeof callback === "function") callback();
|
|
371
|
+
}
|
|
279
372
|
/**
|
|
280
373
|
* Initializes the Fabric canvas, viewport elements, and selection event handlers.
|
|
281
374
|
*
|
|
@@ -298,7 +391,7 @@
|
|
|
298
391
|
} else {
|
|
299
392
|
this.containerElement = canvasElement.parentElement;
|
|
300
393
|
}
|
|
301
|
-
this.placeholderElement = this._getElement("
|
|
394
|
+
this.placeholderElement = this._getElement("imagePlaceholder") || null;
|
|
302
395
|
let initialWidth = this.options.canvasWidth;
|
|
303
396
|
let initialHeight = this.options.canvasHeight;
|
|
304
397
|
if (this.containerElement) {
|
|
@@ -394,13 +487,14 @@
|
|
|
394
487
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
395
488
|
this._captureContainerOverflowState();
|
|
396
489
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
397
|
-
|
|
490
|
+
const layoutMode = this._getImageLayoutMode();
|
|
491
|
+
if (layoutMode === "cover") {
|
|
398
492
|
this.containerElement.style.overflow = "scroll";
|
|
399
493
|
if (!shouldPreserveScroll) {
|
|
400
494
|
this.containerElement.scrollLeft = 0;
|
|
401
495
|
this.containerElement.scrollTop = 0;
|
|
402
496
|
}
|
|
403
|
-
} else if (
|
|
497
|
+
} else if (layoutMode === "fit") {
|
|
404
498
|
this.containerElement.style.overflow = "auto";
|
|
405
499
|
if (!shouldPreserveScroll) {
|
|
406
500
|
this.containerElement.scrollLeft = 0;
|
|
@@ -448,20 +542,20 @@
|
|
|
448
542
|
});
|
|
449
543
|
}
|
|
450
544
|
});
|
|
451
|
-
this._bindIfExists("
|
|
452
|
-
this._bindIfExists("
|
|
453
|
-
this._bindIfExists("
|
|
545
|
+
this._bindIfExists("zoomInButton", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
546
|
+
this._bindIfExists("zoomOutButton", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
547
|
+
this._bindIfExists("resetImageTransformButton", "click", () => {
|
|
454
548
|
this.resetImageTransform().catch((error) => this._reportError("resetImageTransform failed", error));
|
|
455
549
|
});
|
|
456
|
-
this._bindIfExists("
|
|
457
|
-
this._bindIfExists("
|
|
458
|
-
this._bindIfExists("
|
|
459
|
-
this._bindIfExists("
|
|
460
|
-
this._bindIfExists("
|
|
461
|
-
this._bindIfExists("
|
|
462
|
-
this._bindIfExists("
|
|
463
|
-
this._bindIfExists("
|
|
464
|
-
const rotationInputElement = this._getElement("
|
|
550
|
+
this._bindIfExists("createMaskButton", "click", () => this.createMask());
|
|
551
|
+
this._bindIfExists("removeSelectedMaskButton", "click", () => this.removeSelectedMask());
|
|
552
|
+
this._bindIfExists("removeAllMasksButton", "click", () => this.removeAllMasks());
|
|
553
|
+
this._bindIfExists("mergeMasksButton", "click", () => this.mergeMasks().catch((error) => this._reportError("merge error", error)));
|
|
554
|
+
this._bindIfExists("downloadImageButton", "click", () => this.downloadImage());
|
|
555
|
+
this._bindIfExists("undoButton", "click", () => this.undo().catch((error) => this._reportError("undo failed", error)));
|
|
556
|
+
this._bindIfExists("redoButton", "click", () => this.redo().catch((error) => this._reportError("redo failed", error)));
|
|
557
|
+
this._bindIfExists("rotateLeftButton", "click", () => {
|
|
558
|
+
const rotationInputElement = this._getElement("rotateLeftDegreesInput");
|
|
465
559
|
let step = this.options.rotationStep;
|
|
466
560
|
if (rotationInputElement) {
|
|
467
561
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -469,8 +563,8 @@
|
|
|
469
563
|
}
|
|
470
564
|
this.rotateImage(this.currentRotation - step).catch((error) => this._reportError("rotateImage failed", error));
|
|
471
565
|
});
|
|
472
|
-
this._bindIfExists("
|
|
473
|
-
const rotationInputElement = this._getElement("
|
|
566
|
+
this._bindIfExists("rotateRightButton", "click", () => {
|
|
567
|
+
const rotationInputElement = this._getElement("rotateRightDegreesInput");
|
|
474
568
|
let step = this.options.rotationStep;
|
|
475
569
|
if (rotationInputElement) {
|
|
476
570
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -478,11 +572,11 @@
|
|
|
478
572
|
}
|
|
479
573
|
this.rotateImage(this.currentRotation + step).catch((error) => this._reportError("rotateImage failed", error));
|
|
480
574
|
});
|
|
481
|
-
this._bindIfExists("
|
|
482
|
-
this._bindIfExists("
|
|
575
|
+
this._bindIfExists("enterCropModeButton", "click", () => this.enterCropMode());
|
|
576
|
+
this._bindIfExists("applyCropButton", "click", () => {
|
|
483
577
|
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
484
578
|
});
|
|
485
|
-
this._bindIfExists("
|
|
579
|
+
this._bindIfExists("cancelCropButton", "click", () => this.cancelCrop());
|
|
486
580
|
this._bindIfExists("maskList", "click", (event) => this._handleMaskListClick(event));
|
|
487
581
|
}
|
|
488
582
|
/**
|
|
@@ -551,6 +645,12 @@
|
|
|
551
645
|
`Only one image layout mode should be enabled. Active modes: ${activeModes.join(", ")}.`
|
|
552
646
|
);
|
|
553
647
|
}
|
|
648
|
+
_getImageLayoutMode() {
|
|
649
|
+
if (this.options.fitImageToCanvas) return "fit";
|
|
650
|
+
if (this.options.coverImageToCanvas) return "cover";
|
|
651
|
+
if (this.options.expandCanvasToImage) return "expand";
|
|
652
|
+
return "contain";
|
|
653
|
+
}
|
|
554
654
|
/**
|
|
555
655
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
556
656
|
*
|
|
@@ -564,12 +664,16 @@
|
|
|
564
664
|
if (!this._fabricLoaded) return;
|
|
565
665
|
if (!this.canvas || this._disposed) return;
|
|
566
666
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
667
|
+
options = options || {};
|
|
567
668
|
this._assertIdleForOperation("loadImage", options);
|
|
568
|
-
|
|
569
|
-
this.
|
|
570
|
-
|
|
571
|
-
const transaction = this._captureLoadImageTransaction();
|
|
669
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
670
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
|
|
671
|
+
let transaction = null;
|
|
572
672
|
try {
|
|
673
|
+
this._isLoading = true;
|
|
674
|
+
this._updateUI();
|
|
675
|
+
this._warnOnImageLayoutOptionConflict();
|
|
676
|
+
transaction = this._captureLoadImageTransaction();
|
|
573
677
|
const imageElement = await this._createImageElement(imageBase64);
|
|
574
678
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
575
679
|
let loadSource = imageBase64;
|
|
@@ -609,7 +713,8 @@
|
|
|
609
713
|
const viewport = this._getContainerViewportSize();
|
|
610
714
|
const minWidth = viewport.width;
|
|
611
715
|
const minHeight = viewport.height;
|
|
612
|
-
|
|
716
|
+
const layoutMode = this._getImageLayoutMode();
|
|
717
|
+
if (layoutMode === "fit") {
|
|
613
718
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
614
719
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
615
720
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -617,13 +722,13 @@
|
|
|
617
722
|
fabricImage.set({ left: 0, top: 0 });
|
|
618
723
|
fabricImage.scale(fitScale);
|
|
619
724
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
620
|
-
} else if (
|
|
725
|
+
} else if (layoutMode === "cover") {
|
|
621
726
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
622
727
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
623
728
|
fabricImage.set({ left: 0, top: 0 });
|
|
624
729
|
fabricImage.scale(layout.scale);
|
|
625
730
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
626
|
-
} else if (
|
|
731
|
+
} else if (layoutMode === "expand") {
|
|
627
732
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
628
733
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
629
734
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -652,14 +757,16 @@
|
|
|
652
757
|
this._updateUI();
|
|
653
758
|
this.canvas.renderAll();
|
|
654
759
|
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
655
|
-
|
|
656
|
-
this.onImageLoaded();
|
|
657
|
-
}
|
|
760
|
+
this._notifyImageLoaded();
|
|
658
761
|
} catch (error) {
|
|
659
|
-
await this._rollbackLoadImageTransaction(
|
|
762
|
+
await this._rollbackLoadImageTransaction(
|
|
763
|
+
transaction,
|
|
764
|
+
this._withInternalOperationOptions(operationToken)
|
|
765
|
+
);
|
|
660
766
|
throw error;
|
|
661
767
|
} finally {
|
|
662
768
|
this._isLoading = false;
|
|
769
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
663
770
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
664
771
|
}
|
|
665
772
|
}
|
|
@@ -707,7 +814,7 @@
|
|
|
707
814
|
try {
|
|
708
815
|
imageElement.src = "";
|
|
709
816
|
} catch (error) {
|
|
710
|
-
|
|
817
|
+
this._reportWarning("Image timeout cleanup failed", error);
|
|
711
818
|
}
|
|
712
819
|
}, safeTimeoutMs);
|
|
713
820
|
imageElement.onload = () => settle(() => resolve(imageElement));
|
|
@@ -772,32 +879,38 @@
|
|
|
772
879
|
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
773
880
|
};
|
|
774
881
|
}
|
|
775
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
882
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
776
883
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
777
884
|
let didRestoreCanvasState = false;
|
|
885
|
+
let didFailCanvasRestore = false;
|
|
778
886
|
try {
|
|
779
887
|
if (transaction.canvasState) {
|
|
780
|
-
await this.loadFromState(transaction.canvasState);
|
|
888
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
781
889
|
didRestoreCanvasState = true;
|
|
782
890
|
}
|
|
783
891
|
} catch (error) {
|
|
784
892
|
this._lastMask = null;
|
|
893
|
+
didFailCanvasRestore = true;
|
|
785
894
|
this._reportError("loadImage rollback failed", error);
|
|
786
895
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
this.currentRotation = transaction.currentRotation;
|
|
790
|
-
this.maskCounter = transaction.maskCounter;
|
|
791
|
-
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
792
|
-
this._lastSnapshot = transaction.lastSnapshot;
|
|
793
|
-
if (didRestoreCanvasState) {
|
|
794
|
-
this._restoreLastMaskReference(transaction.lastMask);
|
|
896
|
+
if (didFailCanvasRestore) {
|
|
897
|
+
this._reconcileEditorStateFromCanvas();
|
|
795
898
|
} else {
|
|
796
|
-
this.
|
|
899
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
900
|
+
this.currentScale = transaction.currentScale;
|
|
901
|
+
this.currentRotation = transaction.currentRotation;
|
|
902
|
+
this.maskCounter = transaction.maskCounter;
|
|
903
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
904
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
905
|
+
if (didRestoreCanvasState) {
|
|
906
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
907
|
+
} else {
|
|
908
|
+
this._lastMask = null;
|
|
909
|
+
}
|
|
910
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
911
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
912
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
797
913
|
}
|
|
798
|
-
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
799
|
-
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
800
|
-
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
801
914
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
802
915
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
803
916
|
if (this.containerElement) {
|
|
@@ -810,6 +923,46 @@
|
|
|
810
923
|
this._updateUI();
|
|
811
924
|
if (this.canvas) this.canvas.renderAll();
|
|
812
925
|
}
|
|
926
|
+
_reconcileEditorStateFromCanvas() {
|
|
927
|
+
if (!this.canvas) {
|
|
928
|
+
this.originalImage = null;
|
|
929
|
+
this.baseImageScale = 1;
|
|
930
|
+
this.currentScale = 1;
|
|
931
|
+
this.currentRotation = 0;
|
|
932
|
+
this.maskCounter = 0;
|
|
933
|
+
this.isImageLoadedToCanvas = false;
|
|
934
|
+
this._lastSnapshot = null;
|
|
935
|
+
this._clearMaskPlacementMemory();
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const canvasObjects = this.canvas.getObjects();
|
|
939
|
+
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
940
|
+
if (this.originalImage) {
|
|
941
|
+
const imageScale = Number(this.originalImage.scaleX) || 1;
|
|
942
|
+
this.baseImageScale = imageScale;
|
|
943
|
+
this.currentScale = 1;
|
|
944
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
945
|
+
} else {
|
|
946
|
+
this.baseImageScale = 1;
|
|
947
|
+
this.currentScale = 1;
|
|
948
|
+
this.currentRotation = 0;
|
|
949
|
+
}
|
|
950
|
+
const masks = canvasObjects.filter((object) => object.maskId);
|
|
951
|
+
this.maskCounter = masks.reduce((max, mask) => Math.max(max, Number(mask.maskId) || 0), 0);
|
|
952
|
+
this._lastMask = masks[masks.length - 1] || null;
|
|
953
|
+
if (!this._lastMask) {
|
|
954
|
+
this._lastMaskInitialLeft = null;
|
|
955
|
+
this._lastMaskInitialTop = null;
|
|
956
|
+
this._lastMaskInitialWidth = null;
|
|
957
|
+
}
|
|
958
|
+
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
959
|
+
try {
|
|
960
|
+
this._lastSnapshot = this._serializeCanvasState();
|
|
961
|
+
} catch (error) {
|
|
962
|
+
this._lastSnapshot = null;
|
|
963
|
+
this._reportWarning("loadImage rollback: failed to reconcile canvas snapshot", error);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
813
966
|
_restoreLastMaskReference(previousLastMask) {
|
|
814
967
|
if (!this.canvas) {
|
|
815
968
|
this._lastMask = null;
|
|
@@ -880,6 +1033,7 @@
|
|
|
880
1033
|
* @private
|
|
881
1034
|
*/
|
|
882
1035
|
_setCanvasSizeInt(width, height) {
|
|
1036
|
+
if (!this.canvas) return;
|
|
883
1037
|
const integerWidth = Math.max(1, Math.round(Number(width) || 1));
|
|
884
1038
|
const integerHeight = Math.max(1, Math.round(Number(height) || 1));
|
|
885
1039
|
this.canvas.setWidth(integerWidth);
|
|
@@ -981,9 +1135,9 @@
|
|
|
981
1135
|
}
|
|
982
1136
|
_getScrollableCanvasSize(contentWidth, contentHeight, viewport = this._getContainerViewportSize()) {
|
|
983
1137
|
if (this._hasFixedContainerScrollbars()) {
|
|
984
|
-
const
|
|
985
|
-
const safeWidth = Math.max(1, viewport.width -
|
|
986
|
-
const safeHeight = Math.max(1, viewport.height -
|
|
1138
|
+
const safetyMargin2 = this._getScrollSafetyMargin();
|
|
1139
|
+
const safeWidth = Math.max(1, viewport.width - safetyMargin2);
|
|
1140
|
+
const safeHeight = Math.max(1, viewport.height - safetyMargin2);
|
|
987
1141
|
return {
|
|
988
1142
|
width: contentWidth > viewport.width + 0.5 ? this._ceilCanvasDimension(contentWidth) : safeWidth,
|
|
989
1143
|
height: contentHeight > viewport.height + 0.5 ? this._ceilCanvasDimension(contentHeight) : safeHeight,
|
|
@@ -1009,9 +1163,17 @@
|
|
|
1009
1163
|
}
|
|
1010
1164
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1011
1165
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1166
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1167
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1168
|
+
const shouldReserveNoScrollbarMargin = layoutMode === "fit" || layoutMode === "cover";
|
|
1169
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1170
|
+
const margin = hasOppositeScrollbar ? safetyMargin : shouldReserveNoScrollbarMargin ? 1 : 0;
|
|
1171
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1172
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1173
|
+
};
|
|
1012
1174
|
return {
|
|
1013
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1014
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1175
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1176
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1015
1177
|
viewportWidth: effectiveWidth,
|
|
1016
1178
|
viewportHeight: effectiveHeight,
|
|
1017
1179
|
hasHorizontal,
|
|
@@ -1133,6 +1295,45 @@
|
|
|
1133
1295
|
});
|
|
1134
1296
|
}
|
|
1135
1297
|
}
|
|
1298
|
+
_getSerializableStateObjects() {
|
|
1299
|
+
if (!this.canvas) return [];
|
|
1300
|
+
return this.canvas.getObjects().filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1301
|
+
}
|
|
1302
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1303
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1304
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1305
|
+
const numericProperties = [
|
|
1306
|
+
"left",
|
|
1307
|
+
"top",
|
|
1308
|
+
"width",
|
|
1309
|
+
"height",
|
|
1310
|
+
"scaleX",
|
|
1311
|
+
"scaleY",
|
|
1312
|
+
"angle",
|
|
1313
|
+
"skewX",
|
|
1314
|
+
"skewY",
|
|
1315
|
+
"cropX",
|
|
1316
|
+
"cropY",
|
|
1317
|
+
"radius",
|
|
1318
|
+
"rx",
|
|
1319
|
+
"ry",
|
|
1320
|
+
"strokeWidth"
|
|
1321
|
+
];
|
|
1322
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1323
|
+
const fabricObject = fabricObjects[index];
|
|
1324
|
+
if (!serializedObject || !fabricObject) return;
|
|
1325
|
+
numericProperties.forEach((property) => {
|
|
1326
|
+
const numericValue = Number(fabricObject[property]);
|
|
1327
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1328
|
+
});
|
|
1329
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1330
|
+
serializedObject.points = fabricObject.points.map((point) => ({
|
|
1331
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1332
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1333
|
+
}));
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1136
1337
|
_restoreMaskControls(mask) {
|
|
1137
1338
|
if (!mask) return;
|
|
1138
1339
|
const cornerSize = Number(mask.cornerSize);
|
|
@@ -1152,7 +1353,7 @@
|
|
|
1152
1353
|
/**
|
|
1153
1354
|
* Captures editor-owned runtime state that Fabric does not include in canvas JSON.
|
|
1154
1355
|
*
|
|
1155
|
-
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number}} Serializable editor metadata.
|
|
1356
|
+
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number, canvasWidth:number, canvasHeight:number}} Serializable editor metadata.
|
|
1156
1357
|
* @private
|
|
1157
1358
|
*/
|
|
1158
1359
|
_serializeEditorMetadata() {
|
|
@@ -1160,12 +1361,16 @@
|
|
|
1160
1361
|
const currentScale = Number(this.currentScale);
|
|
1161
1362
|
const currentRotation = Number(this.currentRotation);
|
|
1162
1363
|
const maskCounter = Number(this.maskCounter);
|
|
1364
|
+
const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
|
|
1365
|
+
const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
|
|
1163
1366
|
return {
|
|
1164
1367
|
version: 1,
|
|
1165
1368
|
baseImageScale: Number.isFinite(baseImageScale) && baseImageScale > 0 ? baseImageScale : 1,
|
|
1166
1369
|
currentScale: Number.isFinite(currentScale) && currentScale > 0 ? currentScale : 1,
|
|
1167
1370
|
currentRotation: Number.isFinite(currentRotation) ? currentRotation : 0,
|
|
1168
|
-
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0
|
|
1371
|
+
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0,
|
|
1372
|
+
canvasWidth: Number.isFinite(canvasWidth) && canvasWidth > 0 ? Math.round(canvasWidth) : 1,
|
|
1373
|
+
canvasHeight: Number.isFinite(canvasHeight) && canvasHeight > 0 ? Math.round(canvasHeight) : 1
|
|
1169
1374
|
};
|
|
1170
1375
|
}
|
|
1171
1376
|
_serializeCanvasState() {
|
|
@@ -1174,6 +1379,7 @@
|
|
|
1174
1379
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1175
1380
|
if (Array.isArray(jsonObject.objects)) {
|
|
1176
1381
|
jsonObject.objects = jsonObject.objects.filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1382
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1177
1383
|
}
|
|
1178
1384
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1179
1385
|
return JSON.stringify(jsonObject);
|
|
@@ -1248,6 +1454,12 @@
|
|
|
1248
1454
|
if (!Number.isFinite(numericValue)) return false;
|
|
1249
1455
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1250
1456
|
}
|
|
1457
|
+
_hasScaledImageEdge(axis) {
|
|
1458
|
+
if (!this.originalImage) return false;
|
|
1459
|
+
const scale = Number(axis === "y" ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1460
|
+
if (!Number.isFinite(scale)) return false;
|
|
1461
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1462
|
+
}
|
|
1251
1463
|
_getPartialExportEdges(bounds) {
|
|
1252
1464
|
if (!bounds) return null;
|
|
1253
1465
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1256,8 +1468,8 @@
|
|
|
1256
1468
|
return {
|
|
1257
1469
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1258
1470
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1259
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1260
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1471
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge("x"),
|
|
1472
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge("y")
|
|
1261
1473
|
};
|
|
1262
1474
|
}
|
|
1263
1475
|
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
@@ -1317,7 +1529,8 @@
|
|
|
1317
1529
|
* @private
|
|
1318
1530
|
*/
|
|
1319
1531
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1320
|
-
const safeMultiplier =
|
|
1532
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1533
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1321
1534
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1322
1535
|
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1323
1536
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1333,6 +1546,25 @@
|
|
|
1333
1546
|
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1334
1547
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1335
1548
|
}
|
|
1549
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1550
|
+
const numericMultiplier = Number(multiplier);
|
|
1551
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1552
|
+
throw new Error("Export multiplier must be a finite positive number");
|
|
1553
|
+
}
|
|
1554
|
+
return Math.max(1, numericMultiplier);
|
|
1555
|
+
}
|
|
1556
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1557
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1558
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1559
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1560
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1561
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1562
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1563
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0 ? Math.floor(configuredMaxPixels) : 5e7;
|
|
1564
|
+
if (outputPixels > maxPixels) {
|
|
1565
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1336
1568
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1337
1569
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1338
1570
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1377,6 +1609,7 @@
|
|
|
1377
1609
|
}
|
|
1378
1610
|
_decodeBase64Payload(base64Payload) {
|
|
1379
1611
|
const payload = String(base64Payload || "");
|
|
1612
|
+
if (!payload) throw new Error("Data URL base64 payload is empty");
|
|
1380
1613
|
if (typeof atob === "function") {
|
|
1381
1614
|
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1382
1615
|
}
|
|
@@ -1385,6 +1618,13 @@
|
|
|
1385
1618
|
}
|
|
1386
1619
|
throw new Error("Base64 decoding is unavailable");
|
|
1387
1620
|
}
|
|
1621
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
1622
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
1623
|
+
if (!match || !match[2]) {
|
|
1624
|
+
throw new Error("Export produced an invalid or empty base64 data URL");
|
|
1625
|
+
}
|
|
1626
|
+
return this._decodeBase64Payload(match[2]);
|
|
1627
|
+
}
|
|
1388
1628
|
/**
|
|
1389
1629
|
* Gets the top-left corner coordinates of the given object.
|
|
1390
1630
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1494,24 +1734,49 @@
|
|
|
1494
1734
|
const currentHeight = this.canvas.getHeight();
|
|
1495
1735
|
let requiredWidth = currentWidth;
|
|
1496
1736
|
let requiredHeight = currentHeight;
|
|
1497
|
-
|
|
1737
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1738
|
+
const usesScrollableFitBounds = layoutMode === "fit" || layoutMode === "cover";
|
|
1739
|
+
let contentWidth = 0;
|
|
1740
|
+
let contentHeight = 0;
|
|
1741
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1498
1742
|
if (!fabricObject) return;
|
|
1499
1743
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
1500
1744
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1501
|
-
|
|
1502
|
-
|
|
1745
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
1746
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
1747
|
+
contentWidth = Math.max(contentWidth, right);
|
|
1748
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
1749
|
+
return { right, bottom };
|
|
1750
|
+
};
|
|
1751
|
+
fabricObjects.forEach((fabricObject) => {
|
|
1752
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
1753
|
+
if (!bounds) return;
|
|
1754
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
1755
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1503
1756
|
});
|
|
1504
|
-
|
|
1757
|
+
if (usesScrollableFitBounds) {
|
|
1758
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
1759
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1760
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
1761
|
+
});
|
|
1762
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
1763
|
+
Math.max(1, contentWidth),
|
|
1764
|
+
Math.max(1, contentHeight)
|
|
1765
|
+
);
|
|
1766
|
+
const newWidth2 = contentSize.hasHorizontal ? Math.max(currentWidth, contentSize.width) : contentSize.width;
|
|
1767
|
+
const newHeight2 = contentSize.hasVertical ? Math.max(currentHeight, contentSize.height) : contentSize.height;
|
|
1768
|
+
if (newWidth2 !== currentWidth || newHeight2 !== currentHeight) {
|
|
1769
|
+
this._setCanvasSizeInt(newWidth2, newHeight2);
|
|
1770
|
+
}
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1505
1773
|
let minWidth = 0;
|
|
1506
1774
|
let minHeight = 0;
|
|
1507
|
-
if (
|
|
1775
|
+
if (this.containerElement) {
|
|
1508
1776
|
const viewport = this._getContainerViewportSize();
|
|
1509
1777
|
const safetyMargin = this._getScrollSafetyMargin();
|
|
1510
1778
|
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1511
1779
|
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1512
|
-
} else if (this.containerElement) {
|
|
1513
|
-
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1514
|
-
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1515
1780
|
}
|
|
1516
1781
|
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1517
1782
|
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
@@ -1522,16 +1787,60 @@
|
|
|
1522
1787
|
this._reportWarning("expandCanvasToFitObjects: failed to expand canvas", error);
|
|
1523
1788
|
}
|
|
1524
1789
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1790
|
+
_captureImageDisplayBounds() {
|
|
1791
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
1792
|
+
this.originalImage.setCoords();
|
|
1793
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
1794
|
+
const width = Number(bounds && bounds.width);
|
|
1795
|
+
const height = Number(bounds && bounds.height);
|
|
1796
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
1797
|
+
return {
|
|
1798
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
1799
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
1800
|
+
width,
|
|
1801
|
+
height
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
1805
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
1806
|
+
const imageWidth = Number(this.originalImage.width);
|
|
1807
|
+
const imageHeight = Number(this.originalImage.height);
|
|
1808
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
1809
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
1810
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
1811
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
1812
|
+
const left = Number(displayBounds.left) || 0;
|
|
1813
|
+
const top = Number(displayBounds.top) || 0;
|
|
1814
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
1815
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
1816
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
1817
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
1818
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1819
|
+
if (layoutMode === "fit" || layoutMode === "cover") {
|
|
1820
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
1821
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
1822
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
1823
|
+
}
|
|
1824
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
1825
|
+
this._setCanvasSizeInt(
|
|
1826
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
1827
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
this.originalImage.set({
|
|
1831
|
+
originX: "left",
|
|
1832
|
+
originY: "top",
|
|
1833
|
+
left,
|
|
1834
|
+
top,
|
|
1835
|
+
scaleX,
|
|
1836
|
+
scaleY
|
|
1837
|
+
});
|
|
1838
|
+
this.originalImage.setCoords();
|
|
1839
|
+
this.baseImageScale = scaleX;
|
|
1840
|
+
this.currentScale = 1;
|
|
1841
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1842
|
+
this._updateInputs();
|
|
1843
|
+
this.canvas.renderAll();
|
|
1535
1844
|
}
|
|
1536
1845
|
/**
|
|
1537
1846
|
* Scales the original image by a given factor, with animation.
|
|
@@ -1546,7 +1855,14 @@
|
|
|
1546
1855
|
} catch (error) {
|
|
1547
1856
|
return Promise.reject(error);
|
|
1548
1857
|
}
|
|
1549
|
-
return this.animationQueue.add(
|
|
1858
|
+
return this.animationQueue.add(async () => {
|
|
1859
|
+
const operationToken = this._beginBusyOperation("scaleImage");
|
|
1860
|
+
try {
|
|
1861
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
1862
|
+
} finally {
|
|
1863
|
+
this._endBusyOperation(operationToken);
|
|
1864
|
+
}
|
|
1865
|
+
}).finally(() => {
|
|
1550
1866
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1551
1867
|
});
|
|
1552
1868
|
}
|
|
@@ -1580,10 +1896,16 @@
|
|
|
1580
1896
|
_assertEditorAvailable(operationName) {
|
|
1581
1897
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1582
1898
|
}
|
|
1899
|
+
_isCropModeAllowedOperation(operationName) {
|
|
1900
|
+
return operationName === "applyCrop" || operationName === "cancelCrop";
|
|
1901
|
+
}
|
|
1583
1902
|
_assertIdleForOperation(operationName, options = {}) {
|
|
1584
1903
|
this._assertEditorAvailable(operationName);
|
|
1585
1904
|
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1586
|
-
if (this.
|
|
1905
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1906
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1907
|
+
}
|
|
1908
|
+
if ((this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) && !isOwnInternalOperation) {
|
|
1587
1909
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1588
1910
|
}
|
|
1589
1911
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -1595,10 +1917,14 @@
|
|
|
1595
1917
|
}
|
|
1596
1918
|
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1597
1919
|
this._assertEditorAvailable(operationName);
|
|
1598
|
-
|
|
1920
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1921
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1922
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1923
|
+
}
|
|
1924
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1599
1925
|
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1600
1926
|
}
|
|
1601
|
-
if (this._activeOperationToken && !
|
|
1927
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1602
1928
|
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1603
1929
|
}
|
|
1604
1930
|
}
|
|
@@ -1692,7 +2018,7 @@
|
|
|
1692
2018
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1693
2019
|
});
|
|
1694
2020
|
this._updateInputs();
|
|
1695
|
-
if (saveHistory) this.saveState();
|
|
2021
|
+
if (saveHistory) this.saveState(options);
|
|
1696
2022
|
} finally {
|
|
1697
2023
|
if (didStartAnimation) {
|
|
1698
2024
|
this.isAnimating = false;
|
|
@@ -1714,7 +2040,14 @@
|
|
|
1714
2040
|
} catch (error) {
|
|
1715
2041
|
return Promise.reject(error);
|
|
1716
2042
|
}
|
|
1717
|
-
return this.animationQueue.add(
|
|
2043
|
+
return this.animationQueue.add(async () => {
|
|
2044
|
+
const operationToken = this._beginBusyOperation("rotateImage");
|
|
2045
|
+
try {
|
|
2046
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2047
|
+
} finally {
|
|
2048
|
+
this._endBusyOperation(operationToken);
|
|
2049
|
+
}
|
|
2050
|
+
}).finally(() => {
|
|
1718
2051
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1719
2052
|
});
|
|
1720
2053
|
}
|
|
@@ -1757,7 +2090,7 @@
|
|
|
1757
2090
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1758
2091
|
});
|
|
1759
2092
|
this._updateInputs();
|
|
1760
|
-
if (saveHistory) this.saveState();
|
|
2093
|
+
if (saveHistory) this.saveState(options);
|
|
1761
2094
|
didCompleteRotation = true;
|
|
1762
2095
|
} finally {
|
|
1763
2096
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -1784,11 +2117,23 @@
|
|
|
1784
2117
|
return Promise.reject(error);
|
|
1785
2118
|
}
|
|
1786
2119
|
return this.animationQueue.add(async () => {
|
|
2120
|
+
const operationToken = this._beginBusyOperation("resetImageTransform");
|
|
1787
2121
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2122
|
+
try {
|
|
2123
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2124
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2125
|
+
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
2126
|
+
this._pushStateTransition(before, after);
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
try {
|
|
2129
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
2130
|
+
} catch (restoreError) {
|
|
2131
|
+
this._reportError("resetImageTransform rollback failed", restoreError);
|
|
2132
|
+
}
|
|
2133
|
+
throw error;
|
|
2134
|
+
} finally {
|
|
2135
|
+
this._endBusyOperation(operationToken);
|
|
2136
|
+
}
|
|
1792
2137
|
}).finally(() => {
|
|
1793
2138
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1794
2139
|
}).catch((error) => {
|
|
@@ -1812,8 +2157,13 @@
|
|
|
1812
2157
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
1813
2158
|
* @public
|
|
1814
2159
|
*/
|
|
1815
|
-
loadFromState(serializedState) {
|
|
2160
|
+
loadFromState(serializedState, options = {}) {
|
|
1816
2161
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2162
|
+
try {
|
|
2163
|
+
this._assertIdleForOperation("loadFromState", options);
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
return Promise.reject(error);
|
|
2166
|
+
}
|
|
1817
2167
|
if (this._cropMode || this._cropRect) {
|
|
1818
2168
|
this._removeCropRect();
|
|
1819
2169
|
this._restoreCropObjectState();
|
|
@@ -1827,10 +2177,13 @@
|
|
|
1827
2177
|
try {
|
|
1828
2178
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1829
2179
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
2180
|
+
const restoredCanvasWidth = Number(editorMetadata && editorMetadata.canvasWidth);
|
|
2181
|
+
const restoredCanvasHeight = Number(editorMetadata && editorMetadata.canvasHeight);
|
|
2182
|
+
const hasRestoredCanvasSize = Number.isFinite(restoredCanvasWidth) && restoredCanvasWidth > 0 && Number.isFinite(restoredCanvasHeight) && restoredCanvasHeight > 0;
|
|
1830
2183
|
if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
|
|
1831
2184
|
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
1832
2185
|
}
|
|
1833
|
-
|
|
2186
|
+
const finishLoad = async () => {
|
|
1834
2187
|
try {
|
|
1835
2188
|
if (this._disposed || !this.canvas) {
|
|
1836
2189
|
reject(new Error("Editor was disposed while loading state"));
|
|
@@ -1866,6 +2219,11 @@
|
|
|
1866
2219
|
this.currentScale = 1;
|
|
1867
2220
|
this.currentRotation = 0;
|
|
1868
2221
|
}
|
|
2222
|
+
if (hasRestoredCanvasSize) {
|
|
2223
|
+
this._setCanvasSizeInt(restoredCanvasWidth, restoredCanvasHeight);
|
|
2224
|
+
} else if (this.originalImage && this._shouldResizeCanvasToContentBounds()) {
|
|
2225
|
+
this._updateCanvasSizeToImageBounds();
|
|
2226
|
+
}
|
|
1869
2227
|
const masks = canvasObjects.filter((object) => object.maskId);
|
|
1870
2228
|
masks.forEach((mask) => {
|
|
1871
2229
|
this._restoreMaskControls(mask);
|
|
@@ -1893,6 +2251,9 @@
|
|
|
1893
2251
|
this._reportError("loadFromState() failed", callbackError);
|
|
1894
2252
|
reject(callbackError);
|
|
1895
2253
|
}
|
|
2254
|
+
};
|
|
2255
|
+
this.canvas.loadFromJSON(state, () => {
|
|
2256
|
+
void finishLoad();
|
|
1896
2257
|
});
|
|
1897
2258
|
} catch (error) {
|
|
1898
2259
|
this._reportError("loadFromState() failed", error);
|
|
@@ -1959,22 +2320,29 @@
|
|
|
1959
2320
|
* @returns {void}
|
|
1960
2321
|
* @public
|
|
1961
2322
|
*/
|
|
1962
|
-
saveState() {
|
|
2323
|
+
saveState(options = {}) {
|
|
1963
2324
|
if (!this.canvas) return;
|
|
2325
|
+
try {
|
|
2326
|
+
this._assertIdleForOperation("saveState", options);
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
this._reportError("saveState blocked", error);
|
|
2329
|
+
this._updateUI();
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
1964
2332
|
try {
|
|
1965
2333
|
const after = this._captureCanvasStateOrThrow("saveState");
|
|
1966
2334
|
const before = this._lastSnapshot || after;
|
|
1967
2335
|
if (after === before) return;
|
|
1968
2336
|
let executedOnce = false;
|
|
1969
2337
|
const command = new Command(
|
|
1970
|
-
() => {
|
|
2338
|
+
(commandOptions = {}) => {
|
|
1971
2339
|
if (executedOnce) {
|
|
1972
|
-
return this.loadFromState(after);
|
|
2340
|
+
return this.loadFromState(after, commandOptions);
|
|
1973
2341
|
}
|
|
1974
2342
|
executedOnce = true;
|
|
1975
2343
|
return void 0;
|
|
1976
2344
|
},
|
|
1977
|
-
() => this.loadFromState(before)
|
|
2345
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
1978
2346
|
);
|
|
1979
2347
|
this.historyManager.execute(command);
|
|
1980
2348
|
this._lastSnapshot = after;
|
|
@@ -2003,8 +2371,8 @@
|
|
|
2003
2371
|
if (before === after) return;
|
|
2004
2372
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2005
2373
|
const command = new Command(
|
|
2006
|
-
() => this.loadFromState(after),
|
|
2007
|
-
() => this.loadFromState(before)
|
|
2374
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2375
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2008
2376
|
);
|
|
2009
2377
|
this.historyManager.push(command);
|
|
2010
2378
|
this._lastSnapshot = after;
|
|
@@ -2017,8 +2385,16 @@
|
|
|
2017
2385
|
* @public
|
|
2018
2386
|
*/
|
|
2019
2387
|
undo() {
|
|
2020
|
-
|
|
2388
|
+
try {
|
|
2389
|
+
this._assertIdleForOperation("undo");
|
|
2390
|
+
} catch (error) {
|
|
2391
|
+
return Promise.reject(error);
|
|
2392
|
+
}
|
|
2393
|
+
const operationToken = this._beginBusyOperation("undo");
|
|
2394
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2021
2395
|
this._updateUI();
|
|
2396
|
+
}).finally(() => {
|
|
2397
|
+
this._endBusyOperation(operationToken);
|
|
2022
2398
|
}).catch((error) => {
|
|
2023
2399
|
this._reportError("undo failed", error);
|
|
2024
2400
|
throw error;
|
|
@@ -2031,8 +2407,16 @@
|
|
|
2031
2407
|
* @public
|
|
2032
2408
|
*/
|
|
2033
2409
|
redo() {
|
|
2034
|
-
|
|
2410
|
+
try {
|
|
2411
|
+
this._assertIdleForOperation("redo");
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
return Promise.reject(error);
|
|
2414
|
+
}
|
|
2415
|
+
const operationToken = this._beginBusyOperation("redo");
|
|
2416
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2035
2417
|
this._updateUI();
|
|
2418
|
+
}).finally(() => {
|
|
2419
|
+
this._endBusyOperation(operationToken);
|
|
2036
2420
|
}).catch((error) => {
|
|
2037
2421
|
this._reportError("redo failed", error);
|
|
2038
2422
|
throw error;
|
|
@@ -2040,14 +2424,7 @@
|
|
|
2040
2424
|
}
|
|
2041
2425
|
_rebindMaskEvents(mask) {
|
|
2042
2426
|
if (!mask) return;
|
|
2043
|
-
|
|
2044
|
-
try {
|
|
2045
|
-
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
2046
|
-
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
2047
|
-
} catch (error) {
|
|
2048
|
-
void error;
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2427
|
+
this._cleanupMaskEvents(mask);
|
|
2051
2428
|
const metadata = {};
|
|
2052
2429
|
if (!Number.isFinite(Number(mask.originalAlpha))) {
|
|
2053
2430
|
metadata.originalAlpha = Number.isFinite(Number(mask.opacity)) ? Number(mask.opacity) : 0.5;
|
|
@@ -2074,6 +2451,22 @@
|
|
|
2074
2451
|
mask.on("mouseout", mouseout);
|
|
2075
2452
|
mask.__imageEditorMaskHandlers = { mouseover, mouseout };
|
|
2076
2453
|
}
|
|
2454
|
+
_cleanupMaskEvents(mask) {
|
|
2455
|
+
if (!mask || !mask.__imageEditorMaskHandlers) return;
|
|
2456
|
+
try {
|
|
2457
|
+
if (typeof mask.off === "function") {
|
|
2458
|
+
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
2459
|
+
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
2460
|
+
}
|
|
2461
|
+
} catch (error) {
|
|
2462
|
+
this._reportWarning("Mask event cleanup failed", error);
|
|
2463
|
+
}
|
|
2464
|
+
try {
|
|
2465
|
+
delete mask.__imageEditorMaskHandlers;
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
this._reportWarning("Mask event metadata cleanup failed", error);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2077
2470
|
/**
|
|
2078
2471
|
* Creates a mask and adds it to the canvas.
|
|
2079
2472
|
*
|
|
@@ -2139,18 +2532,43 @@
|
|
|
2139
2532
|
}
|
|
2140
2533
|
return value != null ? value : fallback;
|
|
2141
2534
|
};
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2535
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
2536
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
2537
|
+
return null;
|
|
2538
|
+
};
|
|
2539
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
2540
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
2541
|
+
const numericValue = Number(resolvedValue);
|
|
2542
|
+
if (!Number.isFinite(numericValue)) {
|
|
2543
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
2544
|
+
}
|
|
2545
|
+
if (constraints.positive && numericValue <= 0) {
|
|
2546
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
2547
|
+
}
|
|
2548
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
2549
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
2550
|
+
}
|
|
2551
|
+
return numericValue;
|
|
2552
|
+
};
|
|
2553
|
+
try {
|
|
2554
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, "width", "gap", { nonNegative: true });
|
|
2555
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, "width", "width", { positive: true });
|
|
2556
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, "height", "height", { positive: true });
|
|
2557
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, "width", "angle");
|
|
2558
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, "width", "alpha")));
|
|
2559
|
+
if (maskConfig.left === void 0 && this._lastMask) {
|
|
2560
|
+
const previousMask = this._lastMask;
|
|
2561
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
2562
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2563
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2564
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
2565
|
+
} else {
|
|
2566
|
+
left = resolveNumber(maskConfig.left, firstOffset, "width", "left");
|
|
2567
|
+
top = resolveNumber(maskConfig.top, firstOffset, "height", "top");
|
|
2568
|
+
}
|
|
2569
|
+
} catch (error) {
|
|
2570
|
+
return rejectInvalidMask("invalid numeric configuration", error);
|
|
2151
2571
|
}
|
|
2152
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
2153
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height");
|
|
2154
2572
|
maskConfig.left = left;
|
|
2155
2573
|
maskConfig.top = top;
|
|
2156
2574
|
let mask;
|
|
@@ -2159,10 +2577,15 @@
|
|
|
2159
2577
|
} else {
|
|
2160
2578
|
switch (shapeType) {
|
|
2161
2579
|
case "circle":
|
|
2580
|
+
try {
|
|
2581
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, "min", "radius", { positive: true });
|
|
2582
|
+
} catch (error) {
|
|
2583
|
+
return rejectInvalidMask("invalid circle radius", error);
|
|
2584
|
+
}
|
|
2162
2585
|
mask = new fabric.Circle({
|
|
2163
2586
|
left,
|
|
2164
2587
|
top,
|
|
2165
|
-
radius:
|
|
2588
|
+
radius: maskConfig.radius,
|
|
2166
2589
|
fill: maskConfig.color,
|
|
2167
2590
|
opacity: maskConfig.alpha,
|
|
2168
2591
|
angle: maskConfig.angle,
|
|
@@ -2170,11 +2593,17 @@
|
|
|
2170
2593
|
});
|
|
2171
2594
|
break;
|
|
2172
2595
|
case "ellipse":
|
|
2596
|
+
try {
|
|
2597
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, "width", "rx", { positive: true });
|
|
2598
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, "height", "ry", { positive: true });
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
return rejectInvalidMask("invalid ellipse radius", error);
|
|
2601
|
+
}
|
|
2173
2602
|
mask = new fabric.Ellipse({
|
|
2174
2603
|
left,
|
|
2175
2604
|
top,
|
|
2176
|
-
rx:
|
|
2177
|
-
ry:
|
|
2605
|
+
rx: maskConfig.rx,
|
|
2606
|
+
ry: maskConfig.ry,
|
|
2178
2607
|
fill: maskConfig.color,
|
|
2179
2608
|
opacity: maskConfig.alpha,
|
|
2180
2609
|
angle: maskConfig.angle,
|
|
@@ -2183,8 +2612,20 @@
|
|
|
2183
2612
|
break;
|
|
2184
2613
|
case "polygon": {
|
|
2185
2614
|
let polygonPoints = maskConfig.points || [];
|
|
2186
|
-
if (Array.isArray(polygonPoints)
|
|
2187
|
-
|
|
2615
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
2616
|
+
return rejectInvalidMask("polygon masks require at least three points");
|
|
2617
|
+
}
|
|
2618
|
+
try {
|
|
2619
|
+
polygonPoints = polygonPoints.map((point) => {
|
|
2620
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
2621
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
2622
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
2623
|
+
throw new Error("polygon point coordinates must be finite numbers");
|
|
2624
|
+
}
|
|
2625
|
+
return { x, y };
|
|
2626
|
+
});
|
|
2627
|
+
} catch (error) {
|
|
2628
|
+
return rejectInvalidMask("invalid polygon points", error);
|
|
2188
2629
|
}
|
|
2189
2630
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2190
2631
|
left,
|
|
@@ -2198,11 +2639,17 @@
|
|
|
2198
2639
|
}
|
|
2199
2640
|
case "rect":
|
|
2200
2641
|
default:
|
|
2642
|
+
try {
|
|
2643
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, "width", "rx", { nonNegative: true });
|
|
2644
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, "height", "ry", { nonNegative: true });
|
|
2645
|
+
} catch (error) {
|
|
2646
|
+
return rejectInvalidMask("invalid rectangle corner radius", error);
|
|
2647
|
+
}
|
|
2201
2648
|
mask = new fabric.Rect({
|
|
2202
2649
|
left,
|
|
2203
2650
|
top,
|
|
2204
|
-
width:
|
|
2205
|
-
height:
|
|
2651
|
+
width: maskConfig.width,
|
|
2652
|
+
height: maskConfig.height,
|
|
2206
2653
|
fill: maskConfig.color,
|
|
2207
2654
|
opacity: maskConfig.alpha,
|
|
2208
2655
|
angle: maskConfig.angle,
|
|
@@ -2240,10 +2687,10 @@
|
|
|
2240
2687
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2241
2688
|
});
|
|
2242
2689
|
this._rebindMaskEvents(mask);
|
|
2243
|
-
this.
|
|
2690
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2244
2691
|
this._lastMaskInitialLeft = left;
|
|
2245
2692
|
this._lastMaskInitialTop = top;
|
|
2246
|
-
this._lastMaskInitialWidth =
|
|
2693
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2247
2694
|
const maskId = ++this.maskCounter;
|
|
2248
2695
|
mask.set({
|
|
2249
2696
|
maskId,
|
|
@@ -2284,6 +2731,7 @@
|
|
|
2284
2731
|
this.canvas.discardActiveObject();
|
|
2285
2732
|
selectedMasks.forEach((mask) => {
|
|
2286
2733
|
this._removeLabelForMask(mask);
|
|
2734
|
+
this._cleanupMaskEvents(mask);
|
|
2287
2735
|
this.canvas.remove(mask);
|
|
2288
2736
|
});
|
|
2289
2737
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
@@ -2308,7 +2756,10 @@
|
|
|
2308
2756
|
const saveHistory = options.saveHistory !== false;
|
|
2309
2757
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2310
2758
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2311
|
-
masks.forEach((mask) =>
|
|
2759
|
+
masks.forEach((mask) => {
|
|
2760
|
+
this._cleanupMaskEvents(mask);
|
|
2761
|
+
this.canvas.remove(mask);
|
|
2762
|
+
});
|
|
2312
2763
|
this.canvas.discardActiveObject();
|
|
2313
2764
|
this._lastMask = null;
|
|
2314
2765
|
this._lastMaskInitialLeft = null;
|
|
@@ -2377,7 +2828,7 @@
|
|
|
2377
2828
|
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2378
2829
|
this._syncMaskLabel(backup.mask);
|
|
2379
2830
|
} catch (error) {
|
|
2380
|
-
|
|
2831
|
+
this._reportWarning("restoreMaskLabelBackups: failed to restore mask label", error);
|
|
2381
2832
|
}
|
|
2382
2833
|
});
|
|
2383
2834
|
}
|
|
@@ -2508,7 +2959,6 @@
|
|
|
2508
2959
|
try {
|
|
2509
2960
|
if (canvasObjectSet.has(label)) {
|
|
2510
2961
|
this.canvas.remove(label);
|
|
2511
|
-
canvasObjectSet.delete(label);
|
|
2512
2962
|
}
|
|
2513
2963
|
} catch (error) {
|
|
2514
2964
|
void error;
|
|
@@ -2668,6 +3118,7 @@
|
|
|
2668
3118
|
this._assertIdleForOperation("mergeMasks");
|
|
2669
3119
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2670
3120
|
if (!masks.length) return;
|
|
3121
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
2671
3122
|
const beforeJson = this._serializeCanvasState();
|
|
2672
3123
|
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2673
3124
|
this.canvas.discardActiveObject();
|
|
@@ -2686,12 +3137,13 @@
|
|
|
2686
3137
|
preserveScroll: true,
|
|
2687
3138
|
resetMaskCounter: false
|
|
2688
3139
|
}));
|
|
3140
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
2689
3141
|
const afterJson = this._serializeCanvasState();
|
|
2690
3142
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2691
3143
|
} catch (error) {
|
|
2692
3144
|
this._reportError("merge error", error);
|
|
2693
3145
|
try {
|
|
2694
|
-
await this.loadFromState(beforeJson);
|
|
3146
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
2695
3147
|
} catch (restoreError) {
|
|
2696
3148
|
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2697
3149
|
}
|
|
@@ -2748,24 +3200,65 @@
|
|
|
2748
3200
|
*/
|
|
2749
3201
|
async exportImageBase64(options = {}) {
|
|
2750
3202
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3203
|
+
options = options || {};
|
|
2751
3204
|
this._assertIdleForOperation("exportImageBase64", options);
|
|
3205
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3206
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageBase64");
|
|
2752
3207
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2753
3208
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2754
3209
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2755
3210
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
3211
|
+
try {
|
|
3212
|
+
if (!exportImageArea) {
|
|
3213
|
+
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
3214
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3215
|
+
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
3216
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
3217
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
3218
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
3219
|
+
try {
|
|
3220
|
+
masks2.forEach((mask) => {
|
|
3221
|
+
mask.set({ visible: false });
|
|
3222
|
+
});
|
|
3223
|
+
this.canvas.discardActiveObject();
|
|
3224
|
+
this.canvas.renderAll();
|
|
3225
|
+
this.originalImage.setCoords();
|
|
3226
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3227
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3228
|
+
return await this._exportCanvasRegionToDataURL({
|
|
3229
|
+
...exportRegion,
|
|
3230
|
+
multiplier,
|
|
3231
|
+
quality,
|
|
3232
|
+
format,
|
|
3233
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3234
|
+
});
|
|
3235
|
+
} finally {
|
|
3236
|
+
maskVisibilityBackups.forEach((backup) => {
|
|
3237
|
+
try {
|
|
3238
|
+
backup.object.set({ visible: backup.visible });
|
|
3239
|
+
} catch (error) {
|
|
3240
|
+
void error;
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
3244
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
3245
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3246
|
+
this.canvas.renderAll();
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3250
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3251
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3252
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2763
3253
|
try {
|
|
2764
|
-
|
|
2765
|
-
mask.set({ visible: false });
|
|
2766
|
-
});
|
|
3254
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2767
3255
|
this.canvas.discardActiveObject();
|
|
2768
3256
|
this.canvas.renderAll();
|
|
3257
|
+
masks.forEach((mask) => {
|
|
3258
|
+
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
3259
|
+
mask.setCoords();
|
|
3260
|
+
});
|
|
3261
|
+
this.canvas.renderAll();
|
|
2769
3262
|
this.originalImage.setCoords();
|
|
2770
3263
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2771
3264
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
@@ -2777,50 +3270,14 @@
|
|
|
2777
3270
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2778
3271
|
});
|
|
2779
3272
|
} finally {
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
} catch (error) {
|
|
2784
|
-
void error;
|
|
2785
|
-
}
|
|
2786
|
-
});
|
|
2787
|
-
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2788
|
-
this._restoreMaskLabelBackups(labelBackups2);
|
|
2789
|
-
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3273
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3274
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3275
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2790
3276
|
this.canvas.renderAll();
|
|
2791
3277
|
}
|
|
2792
|
-
}
|
|
2793
|
-
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2794
|
-
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2795
|
-
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2796
|
-
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2797
|
-
let finalBase64;
|
|
2798
|
-
try {
|
|
2799
|
-
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2800
|
-
this.canvas.discardActiveObject();
|
|
2801
|
-
this.canvas.renderAll();
|
|
2802
|
-
masks.forEach((mask) => {
|
|
2803
|
-
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
2804
|
-
mask.setCoords();
|
|
2805
|
-
});
|
|
2806
|
-
this.canvas.renderAll();
|
|
2807
|
-
this.originalImage.setCoords();
|
|
2808
|
-
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2809
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2810
|
-
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2811
|
-
...exportRegion,
|
|
2812
|
-
multiplier,
|
|
2813
|
-
quality,
|
|
2814
|
-
format,
|
|
2815
|
-
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2816
|
-
});
|
|
2817
3278
|
} finally {
|
|
2818
|
-
this.
|
|
2819
|
-
this._restoreMaskLabelBackups(labelBackups);
|
|
2820
|
-
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2821
|
-
this.canvas.renderAll();
|
|
3279
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2822
3280
|
}
|
|
2823
|
-
return finalBase64;
|
|
2824
3281
|
}
|
|
2825
3282
|
/**
|
|
2826
3283
|
* Backward-compatible alias for {@link ImageEditor#exportImageBase64}.
|
|
@@ -2852,7 +3309,10 @@
|
|
|
2852
3309
|
*/
|
|
2853
3310
|
async exportImageFile(options = {}) {
|
|
2854
3311
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2855
|
-
|
|
3312
|
+
options = options || {};
|
|
3313
|
+
this._assertIdleForOperation("exportImageFile", options);
|
|
3314
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3315
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageFile");
|
|
2856
3316
|
const {
|
|
2857
3317
|
mergeMask = true,
|
|
2858
3318
|
fileType = "jpeg",
|
|
@@ -2862,48 +3322,52 @@
|
|
|
2862
3322
|
} = options;
|
|
2863
3323
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
2864
3324
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3325
|
+
try {
|
|
3326
|
+
let imageBase64;
|
|
3327
|
+
if (mergeMask) {
|
|
3328
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3329
|
+
exportImageArea: true,
|
|
3330
|
+
multiplier,
|
|
3331
|
+
quality: normalizedQuality,
|
|
3332
|
+
fileType: safeFileType
|
|
3333
|
+
}));
|
|
3334
|
+
} else {
|
|
3335
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3336
|
+
exportImageArea: false,
|
|
3337
|
+
multiplier,
|
|
3338
|
+
quality: normalizedQuality,
|
|
3339
|
+
fileType: safeFileType
|
|
3340
|
+
}));
|
|
3341
|
+
}
|
|
3342
|
+
let imageDataUrl = imageBase64;
|
|
3343
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3344
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3345
|
+
const imageElement = new window.Image();
|
|
3346
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3347
|
+
imageElement.onload = () => {
|
|
3348
|
+
try {
|
|
3349
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
3350
|
+
offscreenCanvas.width = imageElement.width;
|
|
3351
|
+
offscreenCanvas.height = imageElement.height;
|
|
3352
|
+
const context = offscreenCanvas.getContext("2d");
|
|
3353
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
3354
|
+
context.drawImage(imageElement, 0, 0);
|
|
3355
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3356
|
+
resolve(convertedDataUrl);
|
|
3357
|
+
} catch (error) {
|
|
3358
|
+
reject(error);
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
imageElement.onerror = reject;
|
|
3362
|
+
imageElement.src = imageBase64;
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3366
|
+
const mime = `image/${safeFileType}`;
|
|
3367
|
+
return new File([bytes], fileName, { type: mime });
|
|
3368
|
+
} finally {
|
|
3369
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2903
3370
|
}
|
|
2904
|
-
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
2905
|
-
const mime = `image/${safeFileType}`;
|
|
2906
|
-
return new File([bytes], fileName, { type: mime });
|
|
2907
3371
|
}
|
|
2908
3372
|
_clearMaskPlacementMemory() {
|
|
2909
3373
|
this._lastMask = null;
|
|
@@ -2911,7 +3375,7 @@
|
|
|
2911
3375
|
this._lastMaskInitialTop = null;
|
|
2912
3376
|
this._lastMaskInitialWidth = null;
|
|
2913
3377
|
}
|
|
2914
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
3378
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
2915
3379
|
this._reportError(message, error);
|
|
2916
3380
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
2917
3381
|
this._cropRect = null;
|
|
@@ -2922,7 +3386,7 @@
|
|
|
2922
3386
|
this._prevSelectionSetting = void 0;
|
|
2923
3387
|
if (beforeJson) {
|
|
2924
3388
|
try {
|
|
2925
|
-
await this.loadFromState(beforeJson);
|
|
3389
|
+
await this.loadFromState(beforeJson, options);
|
|
2926
3390
|
} catch (restoreError) {
|
|
2927
3391
|
this._reportError("applyCrop: rollback failed", restoreError);
|
|
2928
3392
|
}
|
|
@@ -2947,28 +3411,38 @@
|
|
|
2947
3411
|
this._cropPrevEvented = null;
|
|
2948
3412
|
}
|
|
2949
3413
|
_removeCropRect() {
|
|
2950
|
-
if (
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
3414
|
+
if (this._cropHandlers && this._cropHandlers.length) {
|
|
3415
|
+
this._cropHandlers.forEach((targetHandlers) => {
|
|
3416
|
+
(targetHandlers.handlers || []).forEach((handlerRecord) => {
|
|
3417
|
+
try {
|
|
2955
3418
|
if (targetHandlers.target && typeof targetHandlers.target.off === "function") {
|
|
2956
3419
|
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2957
3420
|
}
|
|
2958
|
-
})
|
|
3421
|
+
} catch (error) {
|
|
3422
|
+
this._reportWarning("Crop handler cleanup failed", error);
|
|
3423
|
+
}
|
|
2959
3424
|
});
|
|
2960
|
-
}
|
|
2961
|
-
} catch (error) {
|
|
2962
|
-
void error;
|
|
3425
|
+
});
|
|
2963
3426
|
}
|
|
2964
3427
|
try {
|
|
2965
|
-
if (this.canvas) this.canvas.remove(this._cropRect);
|
|
3428
|
+
if (this.canvas && this._cropRect) this.canvas.remove(this._cropRect);
|
|
2966
3429
|
} catch (error) {
|
|
2967
3430
|
void error;
|
|
2968
3431
|
}
|
|
2969
3432
|
this._cropRect = null;
|
|
2970
3433
|
this._cropHandlers = [];
|
|
2971
3434
|
}
|
|
3435
|
+
_getCropRectContentBounds(cropRect) {
|
|
3436
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
3437
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
3438
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
3439
|
+
return {
|
|
3440
|
+
left: Number(cropRect.left) || 0,
|
|
3441
|
+
top: Number(cropRect.top) || 0,
|
|
3442
|
+
width,
|
|
3443
|
+
height
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
2972
3446
|
/**
|
|
2973
3447
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
2974
3448
|
*
|
|
@@ -2992,14 +3466,19 @@
|
|
|
2992
3466
|
const padding = this.options.crop && this.options.crop.padding ? this.options.crop.padding : 10;
|
|
2993
3467
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
2994
3468
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
2995
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
2996
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
3469
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
3470
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
2997
3471
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
2998
3472
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
2999
3473
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3000
3474
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3001
3475
|
const width = minCropWidth;
|
|
3002
3476
|
const height = minCropHeight;
|
|
3477
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
3478
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
3479
|
+
this._cropRotationWarningEmitted = true;
|
|
3480
|
+
this._reportWarning("crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported");
|
|
3481
|
+
}
|
|
3003
3482
|
const cropRect = new fabric.Rect({
|
|
3004
3483
|
left,
|
|
3005
3484
|
top,
|
|
@@ -3011,8 +3490,8 @@
|
|
|
3011
3490
|
strokeWidth: 1,
|
|
3012
3491
|
strokeUniform: true,
|
|
3013
3492
|
selectable: true,
|
|
3014
|
-
hasRotatingPoint:
|
|
3015
|
-
lockRotation:
|
|
3493
|
+
hasRotatingPoint: false,
|
|
3494
|
+
lockRotation: true,
|
|
3016
3495
|
cornerSize: 8,
|
|
3017
3496
|
objectCaching: false,
|
|
3018
3497
|
originX: "left",
|
|
@@ -3049,7 +3528,7 @@
|
|
|
3049
3528
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3050
3529
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3051
3530
|
cropRect.setCoords();
|
|
3052
|
-
const cropBounds =
|
|
3531
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3053
3532
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3054
3533
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3055
3534
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3123,89 +3602,100 @@
|
|
|
3123
3602
|
async applyCrop() {
|
|
3124
3603
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3125
3604
|
this._assertIdleForOperation("applyCrop");
|
|
3126
|
-
this.
|
|
3127
|
-
const
|
|
3128
|
-
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3129
|
-
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3130
|
-
this._restoreCropObjectState();
|
|
3131
|
-
let beforeJson;
|
|
3132
|
-
try {
|
|
3133
|
-
beforeJson = this._serializeCanvasState();
|
|
3134
|
-
} catch (error) {
|
|
3135
|
-
this._reportWarning("applyCrop: could not serialize before state", error);
|
|
3136
|
-
beforeJson = null;
|
|
3137
|
-
}
|
|
3138
|
-
const preservedMasks = [];
|
|
3605
|
+
const operationToken = this._beginBusyOperation("applyCrop");
|
|
3606
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3139
3607
|
try {
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
preservedMasks.push(mask);
|
|
3152
|
-
}
|
|
3153
|
-
});
|
|
3154
|
-
this._clearMaskPlacementMemory();
|
|
3155
|
-
this.canvas.discardActiveObject();
|
|
3156
|
-
this.canvas.renderAll();
|
|
3608
|
+
this._cropRect.setCoords();
|
|
3609
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3610
|
+
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3611
|
+
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3612
|
+
this._restoreCropObjectState();
|
|
3613
|
+
let beforeJson;
|
|
3614
|
+
try {
|
|
3615
|
+
beforeJson = this._serializeCanvasState();
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3618
|
+
beforeJson = null;
|
|
3157
3619
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3620
|
+
if (!beforeJson) {
|
|
3621
|
+
this.cancelCrop();
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
const preservedMasks = [];
|
|
3625
|
+
try {
|
|
3626
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3627
|
+
if (masks && masks.length) {
|
|
3628
|
+
masks.forEach((mask) => {
|
|
3629
|
+
mask.setCoords();
|
|
3630
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3631
|
+
const intersectsCrop = maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth && maskBounds.left + maskBounds.width > cropRegion.sourceX && maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight && maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3632
|
+
this._removeLabelForMask(mask);
|
|
3633
|
+
this._cleanupMaskEvents(mask);
|
|
3634
|
+
this.canvas.remove(mask);
|
|
3635
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3636
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3637
|
+
mask.set({ visible: true });
|
|
3638
|
+
preservedMasks.push(mask);
|
|
3639
|
+
}
|
|
3640
|
+
});
|
|
3641
|
+
this._clearMaskPlacementMemory();
|
|
3642
|
+
this.canvas.discardActiveObject();
|
|
3643
|
+
this.canvas.renderAll();
|
|
3644
|
+
}
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error, internalOptions);
|
|
3647
|
+
return;
|
|
3648
|
+
}
|
|
3649
|
+
this._removeCropRect();
|
|
3650
|
+
this._cropMode = false;
|
|
3651
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3652
|
+
this._prevSelectionSetting = void 0;
|
|
3653
|
+
let croppedBase64;
|
|
3654
|
+
try {
|
|
3655
|
+
croppedBase64 = await this._exportCanvasRegionToDataURL({
|
|
3656
|
+
...cropRegion,
|
|
3657
|
+
multiplier: 1,
|
|
3658
|
+
quality: this._normalizeQuality(this.options.downsampleQuality),
|
|
3659
|
+
format: "jpeg"
|
|
3185
3660
|
});
|
|
3186
|
-
|
|
3187
|
-
this.
|
|
3188
|
-
|
|
3189
|
-
this.canvas.renderAll();
|
|
3661
|
+
} catch (error) {
|
|
3662
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to create cropped image", error, internalOptions);
|
|
3663
|
+
return;
|
|
3190
3664
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3665
|
+
try {
|
|
3666
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3667
|
+
if (preservedMasks.length) {
|
|
3668
|
+
preservedMasks.forEach((mask) => {
|
|
3669
|
+
this._rebindMaskEvents(mask);
|
|
3670
|
+
this.canvas.add(mask);
|
|
3671
|
+
this.canvas.bringToFront(mask);
|
|
3672
|
+
});
|
|
3673
|
+
this._lastMask = preservedMasks[preservedMasks.length - 1];
|
|
3674
|
+
this.maskCounter = preservedMasks.reduce((max, mask) => Math.max(max, mask.maskId || 0), this.maskCounter);
|
|
3675
|
+
this._updateMaskList();
|
|
3676
|
+
this.canvas.renderAll();
|
|
3677
|
+
}
|
|
3678
|
+
} catch (error) {
|
|
3679
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", error, internalOptions);
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3682
|
+
let afterJson;
|
|
3683
|
+
try {
|
|
3684
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
3685
|
+
} catch (error) {
|
|
3686
|
+
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
3687
|
+
afterJson = null;
|
|
3688
|
+
}
|
|
3689
|
+
try {
|
|
3690
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
3691
|
+
} catch (error) {
|
|
3692
|
+
this._reportWarning("applyCrop: failed to push history command", error);
|
|
3693
|
+
}
|
|
3694
|
+
this._updateUI();
|
|
3695
|
+
this.canvas.renderAll();
|
|
3696
|
+
} finally {
|
|
3697
|
+
this._endBusyOperation(operationToken);
|
|
3206
3698
|
}
|
|
3207
|
-
this._updateUI();
|
|
3208
|
-
this.canvas.renderAll();
|
|
3209
3699
|
}
|
|
3210
3700
|
/* ---------- Misc / UI ---------- */
|
|
3211
3701
|
/**
|
|
@@ -3214,7 +3704,7 @@
|
|
|
3214
3704
|
* @private
|
|
3215
3705
|
*/
|
|
3216
3706
|
_updateInputs() {
|
|
3217
|
-
const scaleInputElement = this._getElement("
|
|
3707
|
+
const scaleInputElement = this._getElement("scalePercentageInput");
|
|
3218
3708
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
3219
3709
|
}
|
|
3220
3710
|
/**
|
|
@@ -3238,7 +3728,7 @@
|
|
|
3238
3728
|
for (const key of Object.keys(this.elements || {})) {
|
|
3239
3729
|
const element = this._getElement(key);
|
|
3240
3730
|
if (!element) continue;
|
|
3241
|
-
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3731
|
+
if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3242
3732
|
this._setDisabled(key, false);
|
|
3243
3733
|
} else {
|
|
3244
3734
|
this._setDisabled(key, true);
|
|
@@ -3246,24 +3736,24 @@
|
|
|
3246
3736
|
}
|
|
3247
3737
|
return;
|
|
3248
3738
|
}
|
|
3249
|
-
this._setDisabled("
|
|
3250
|
-
this._setDisabled("
|
|
3251
|
-
this._setDisabled("
|
|
3252
|
-
this._setDisabled("
|
|
3253
|
-
this._setDisabled("
|
|
3254
|
-
this._setDisabled("
|
|
3255
|
-
this._setDisabled("
|
|
3256
|
-
this._setDisabled("
|
|
3257
|
-
this._setDisabled("
|
|
3258
|
-
this._setDisabled("
|
|
3259
|
-
this._setDisabled("
|
|
3260
|
-
this._setDisabled("
|
|
3261
|
-
this._setDisabled("
|
|
3262
|
-
this._setDisabled("
|
|
3263
|
-
this._setDisabled("
|
|
3264
|
-
this._setDisabled("
|
|
3265
|
-
this._setDisabled("
|
|
3266
|
-
this._setDisabled("
|
|
3739
|
+
this._setDisabled("zoomInButton", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3740
|
+
this._setDisabled("zoomOutButton", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3741
|
+
this._setDisabled("rotateLeftButton", !hasImage || isBusy);
|
|
3742
|
+
this._setDisabled("rotateRightButton", !hasImage || isBusy);
|
|
3743
|
+
this._setDisabled("createMaskButton", !hasImage || isBusy);
|
|
3744
|
+
this._setDisabled("removeSelectedMaskButton", !hasSelectedMask || isBusy);
|
|
3745
|
+
this._setDisabled("removeAllMasksButton", !hasMasks || isBusy);
|
|
3746
|
+
this._setDisabled("mergeMasksButton", !hasImage || !hasMasks || isBusy);
|
|
3747
|
+
this._setDisabled("downloadImageButton", !hasImage || isBusy);
|
|
3748
|
+
this._setDisabled("resetImageTransformButton", !hasImage || isDefaultTransform || isBusy);
|
|
3749
|
+
this._setDisabled("undoButton", !hasImage || isBusy || !canUndo);
|
|
3750
|
+
this._setDisabled("redoButton", !hasImage || isBusy || !canRedo);
|
|
3751
|
+
this._setDisabled("enterCropModeButton", !hasImage || isBusy);
|
|
3752
|
+
this._setDisabled("applyCropButton", true);
|
|
3753
|
+
this._setDisabled("cancelCropButton", true);
|
|
3754
|
+
this._setDisabled("scalePercentageInput", !hasImage || isBusy);
|
|
3755
|
+
this._setDisabled("rotateLeftDegreesInput", !hasImage || isBusy);
|
|
3756
|
+
this._setDisabled("rotateRightDegreesInput", !hasImage || isBusy);
|
|
3267
3757
|
this._setDisabled("maskList", !hasImage || isBusy);
|
|
3268
3758
|
this._setDisabled("imageInput", isBusy);
|
|
3269
3759
|
this._setDisabled("uploadArea", isBusy);
|
|
@@ -3271,7 +3761,7 @@
|
|
|
3271
3761
|
/**
|
|
3272
3762
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
3273
3763
|
*
|
|
3274
|
-
* @param {string} key - Key of the element in this.elements (e.g. '
|
|
3764
|
+
* @param {string} key - Key of the element in this.elements (e.g. 'zoomInButton').
|
|
3275
3765
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3276
3766
|
* @private
|
|
3277
3767
|
*/
|
|
@@ -3395,14 +3885,7 @@
|
|
|
3395
3885
|
} catch (error) {
|
|
3396
3886
|
void error;
|
|
3397
3887
|
}
|
|
3398
|
-
if (this._cropRect)
|
|
3399
|
-
try {
|
|
3400
|
-
this.canvas.remove(this._cropRect);
|
|
3401
|
-
} catch (error) {
|
|
3402
|
-
void error;
|
|
3403
|
-
}
|
|
3404
|
-
this._cropRect = null;
|
|
3405
|
-
}
|
|
3888
|
+
if (this._cropRect) this._removeCropRect();
|
|
3406
3889
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3407
3890
|
try {
|
|
3408
3891
|
this._restoreContainerOverflowState();
|
|
@@ -3425,11 +3908,19 @@
|
|
|
3425
3908
|
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3426
3909
|
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3427
3910
|
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
3911
|
+
this.canvasElement.style.maxWidth = this._canvasElementOriginalStyle.maxWidth;
|
|
3428
3912
|
} catch (error) {
|
|
3429
3913
|
void error;
|
|
3430
3914
|
}
|
|
3431
3915
|
}
|
|
3432
3916
|
if (this.canvas) {
|
|
3917
|
+
try {
|
|
3918
|
+
this.canvas.getObjects().forEach((object) => {
|
|
3919
|
+
if (object && object.maskId) this._cleanupMaskEvents(object);
|
|
3920
|
+
});
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
void error;
|
|
3923
|
+
}
|
|
3433
3924
|
try {
|
|
3434
3925
|
this.canvas.dispose();
|
|
3435
3926
|
} catch (error) {
|
|
@@ -3526,7 +4017,7 @@
|
|
|
3526
4017
|
task.reject(error);
|
|
3527
4018
|
}
|
|
3528
4019
|
} finally {
|
|
3529
|
-
if (
|
|
4020
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3530
4021
|
}
|
|
3531
4022
|
}
|
|
3532
4023
|
} finally {
|
|
@@ -3624,11 +4115,11 @@
|
|
|
3624
4115
|
*
|
|
3625
4116
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
3626
4117
|
*/
|
|
3627
|
-
undo() {
|
|
4118
|
+
undo(options = {}) {
|
|
3628
4119
|
return this.enqueue(async () => {
|
|
3629
4120
|
if (this.currentIndex >= 0) {
|
|
3630
4121
|
const index = this.currentIndex;
|
|
3631
|
-
await this.history[index].undo();
|
|
4122
|
+
await this.history[index].undo(options);
|
|
3632
4123
|
this.currentIndex = index - 1;
|
|
3633
4124
|
}
|
|
3634
4125
|
});
|
|
@@ -3638,11 +4129,11 @@
|
|
|
3638
4129
|
*
|
|
3639
4130
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
3640
4131
|
*/
|
|
3641
|
-
redo() {
|
|
4132
|
+
redo(options = {}) {
|
|
3642
4133
|
return this.enqueue(async () => {
|
|
3643
4134
|
if (this.currentIndex < this.history.length - 1) {
|
|
3644
4135
|
const index = this.currentIndex + 1;
|
|
3645
|
-
await this.history[index].execute();
|
|
4136
|
+
await this.history[index].execute(options);
|
|
3646
4137
|
this.currentIndex = index;
|
|
3647
4138
|
}
|
|
3648
4139
|
});
|