@bensitu/image-editor 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +445 -131
- package/dist/image-editor.esm.js +537 -190
- 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 +537 -190
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +537 -190
- 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 +61 -19
- package/package.json +2 -1
- package/src/image-editor.js +588 -191
package/dist/image-editor.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file image-editor.js
|
|
5
5
|
* @module image-editor
|
|
6
|
-
* @version 1.
|
|
6
|
+
* @version 1.5.0
|
|
7
7
|
* @author Ben Situ
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
10
10
|
*/
|
|
11
11
|
var fabric = null;
|
|
12
|
-
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol("ImageEditorInternalOperation");
|
|
12
|
+
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol.for("ImageEditorInternalOperation");
|
|
13
13
|
function getGlobalScope() {
|
|
14
14
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
15
15
|
if (typeof self !== "undefined") return self;
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
145
145
|
this._disposed = false;
|
|
146
146
|
this._initialized = false;
|
|
147
|
-
this.onImageLoaded = typeof options.onImageLoaded === "function" ? options.onImageLoaded : null;
|
|
147
|
+
this.onImageLoaded = typeof this.options.onImageLoaded === "function" ? this.options.onImageLoaded : null;
|
|
148
148
|
this.animationQueue = new AnimationQueue();
|
|
149
149
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
150
150
|
}
|
|
@@ -190,10 +190,12 @@
|
|
|
190
190
|
* Use this method to set up the editor UI before interacting with it.
|
|
191
191
|
*
|
|
192
192
|
* @param {Object} [idMap={}] - Optional mapping from logical element names to actual DOM element IDs.
|
|
193
|
-
* Supported keys include: canvas, canvasContainer,
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
193
|
+
* Supported keys include: canvas, canvasContainer, imagePlaceholder, scalePercentageInput,
|
|
194
|
+
* rotateLeftDegreesInput, rotateRightDegreesInput, rotateLeftButton, rotateRightButton,
|
|
195
|
+
* createMaskButton, removeSelectedMaskButton, removeAllMasksButton, mergeMasksButton,
|
|
196
|
+
* downloadImageButton, maskList, zoomInButton, zoomOutButton, resetImageTransformButton,
|
|
197
|
+
* undoButton, redoButton, imageInput, uploadArea, enterCropModeButton, applyCropButton,
|
|
198
|
+
* and cancelCropButton. Deprecated 1.x names remain supported as aliases.
|
|
197
199
|
*
|
|
198
200
|
* @returns {void}
|
|
199
201
|
*
|
|
@@ -202,7 +204,7 @@
|
|
|
202
204
|
* @example
|
|
203
205
|
* editor.init({
|
|
204
206
|
* canvas: 'myFabricCanvasId',
|
|
205
|
-
*
|
|
207
|
+
* downloadImageButton: 'myDownloadButtonId'
|
|
206
208
|
* });
|
|
207
209
|
*/
|
|
208
210
|
init(idMap = {}) {
|
|
@@ -221,33 +223,53 @@
|
|
|
221
223
|
this._containerOriginalOverflow = null;
|
|
222
224
|
this._lastContainerViewportSize = null;
|
|
223
225
|
this._canvasElementOriginalStyle = null;
|
|
226
|
+
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
224
227
|
const defaults = {
|
|
225
228
|
canvas: "fabricCanvas",
|
|
226
229
|
canvasContainer: null,
|
|
227
230
|
// Pass an ID here if you have a scrollable viewport container
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
imagePlaceholder: "imagePlaceholder",
|
|
232
|
+
imgPlaceholder: null,
|
|
233
|
+
scalePercentageInput: "scalePercentageInput",
|
|
234
|
+
scaleRate: null,
|
|
235
|
+
rotateLeftDegreesInput: "rotateLeftDegreesInput",
|
|
236
|
+
rotationLeftInput: null,
|
|
237
|
+
rotateRightDegreesInput: "rotateRightDegreesInput",
|
|
238
|
+
rotationRightInput: null,
|
|
239
|
+
rotateLeftButton: "rotateLeftButton",
|
|
240
|
+
rotateLeftBtn: null,
|
|
241
|
+
rotateRightButton: "rotateRightButton",
|
|
242
|
+
rotateRightBtn: null,
|
|
243
|
+
createMaskButton: "createMaskButton",
|
|
244
|
+
addMaskBtn: null,
|
|
245
|
+
removeSelectedMaskButton: "removeSelectedMaskButton",
|
|
246
|
+
removeMaskBtn: null,
|
|
247
|
+
removeAllMasksButton: "removeAllMasksButton",
|
|
248
|
+
removeAllMasksBtn: null,
|
|
249
|
+
mergeMasksButton: "mergeMasksButton",
|
|
250
|
+
mergeBtn: null,
|
|
251
|
+
downloadImageButton: "downloadImageButton",
|
|
252
|
+
downloadBtn: null,
|
|
239
253
|
maskList: "maskList",
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
254
|
+
zoomInButton: "zoomInButton",
|
|
255
|
+
zoomInBtn: null,
|
|
256
|
+
zoomOutButton: "zoomOutButton",
|
|
257
|
+
zoomOutBtn: null,
|
|
258
|
+
resetImageTransformButton: "resetImageTransformButton",
|
|
259
|
+
resetBtn: null,
|
|
260
|
+
undoButton: "undoButton",
|
|
261
|
+
undoBtn: null,
|
|
262
|
+
redoButton: "redoButton",
|
|
263
|
+
redoBtn: null,
|
|
245
264
|
imageInput: "imageInput",
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
enterCropModeButton: "enterCropModeButton",
|
|
266
|
+
cropBtn: null,
|
|
267
|
+
applyCropButton: "applyCropButton",
|
|
268
|
+
applyCropBtn: null,
|
|
269
|
+
cancelCropButton: "cancelCropButton",
|
|
270
|
+
cancelCropBtn: null
|
|
249
271
|
};
|
|
250
|
-
this.elements = {
|
|
272
|
+
this.elements = this._resolveElementIdMap(idMap || {}, defaults);
|
|
251
273
|
this._elementCache = {};
|
|
252
274
|
this._initCanvas();
|
|
253
275
|
this._bindEvents();
|
|
@@ -260,6 +282,63 @@
|
|
|
260
282
|
this._updatePlaceholderStatus();
|
|
261
283
|
}
|
|
262
284
|
}
|
|
285
|
+
_resolveElementIdMap(idMap, defaults) {
|
|
286
|
+
const resolved = { ...defaults, ...idMap };
|
|
287
|
+
this._resolveElementAliases(resolved, idMap, defaults, "imagePlaceholder", ["imgPlaceholder"]);
|
|
288
|
+
this._resolveElementAliases(resolved, idMap, defaults, "scalePercentageInput", ["scaleRate"]);
|
|
289
|
+
this._resolveElementAliases(resolved, idMap, defaults, "rotateLeftDegreesInput", ["rotationLeftInput"]);
|
|
290
|
+
this._resolveElementAliases(resolved, idMap, defaults, "rotateRightDegreesInput", ["rotationRightInput"]);
|
|
291
|
+
this._resolveElementAlias(resolved, idMap, defaults, "rotateLeftButton", "rotateLeftBtn");
|
|
292
|
+
this._resolveElementAlias(resolved, idMap, defaults, "rotateRightButton", "rotateRightBtn");
|
|
293
|
+
this._resolveElementAlias(resolved, idMap, defaults, "createMaskButton", "addMaskBtn");
|
|
294
|
+
this._resolveElementAliases(resolved, idMap, defaults, "removeSelectedMaskButton", ["removeMaskBtn"]);
|
|
295
|
+
this._resolveElementAlias(resolved, idMap, defaults, "removeAllMasksButton", "removeAllMasksBtn");
|
|
296
|
+
this._resolveElementAlias(resolved, idMap, defaults, "mergeMasksButton", "mergeBtn");
|
|
297
|
+
this._resolveElementAliases(resolved, idMap, defaults, "downloadImageButton", ["downloadBtn"]);
|
|
298
|
+
this._resolveElementAlias(resolved, idMap, defaults, "zoomInButton", "zoomInBtn");
|
|
299
|
+
this._resolveElementAlias(resolved, idMap, defaults, "zoomOutButton", "zoomOutBtn");
|
|
300
|
+
this._resolveElementAlias(resolved, idMap, defaults, "resetImageTransformButton", "resetBtn");
|
|
301
|
+
this._resolveElementAlias(resolved, idMap, defaults, "undoButton", "undoBtn");
|
|
302
|
+
this._resolveElementAlias(resolved, idMap, defaults, "redoButton", "redoBtn");
|
|
303
|
+
this._resolveElementAliases(resolved, idMap, defaults, "enterCropModeButton", ["cropBtn"]);
|
|
304
|
+
this._resolveElementAlias(resolved, idMap, defaults, "applyCropButton", "applyCropBtn");
|
|
305
|
+
this._resolveElementAlias(resolved, idMap, defaults, "cancelCropButton", "cancelCropBtn");
|
|
306
|
+
return resolved;
|
|
307
|
+
}
|
|
308
|
+
_resolveElementAlias(resolved, idMap, defaults, canonicalKey, deprecatedKey) {
|
|
309
|
+
this._resolveElementAliases(resolved, idMap, defaults, canonicalKey, [deprecatedKey]);
|
|
310
|
+
}
|
|
311
|
+
_resolveElementAliases(resolved, idMap, defaults, canonicalKey, deprecatedKeys) {
|
|
312
|
+
const hasCanonicalKey = Object.prototype.hasOwnProperty.call(idMap, canonicalKey);
|
|
313
|
+
if (hasCanonicalKey) {
|
|
314
|
+
resolved[canonicalKey] = idMap[canonicalKey];
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let deprecatedValue;
|
|
318
|
+
let hasDeprecatedValue = false;
|
|
319
|
+
for (const deprecatedKey of deprecatedKeys) {
|
|
320
|
+
if (Object.prototype.hasOwnProperty.call(idMap, deprecatedKey)) {
|
|
321
|
+
if (!hasDeprecatedValue) {
|
|
322
|
+
deprecatedValue = idMap[deprecatedKey];
|
|
323
|
+
hasDeprecatedValue = true;
|
|
324
|
+
}
|
|
325
|
+
this._warnDeprecatedElementIdKey(deprecatedKey, canonicalKey);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (hasDeprecatedValue) {
|
|
329
|
+
resolved[canonicalKey] = deprecatedValue;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
resolved[canonicalKey] = defaults[canonicalKey];
|
|
333
|
+
}
|
|
334
|
+
_warnDeprecatedElementIdKey(deprecatedKey, canonicalKey) {
|
|
335
|
+
if (!this._deprecatedElementKeyWarnings) this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
336
|
+
if (this._deprecatedElementKeyWarnings.has(deprecatedKey)) return;
|
|
337
|
+
this._deprecatedElementKeyWarnings.add(deprecatedKey);
|
|
338
|
+
this._reportWarning(
|
|
339
|
+
`ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
263
342
|
_reportError(message, error = null) {
|
|
264
343
|
const handler = this.options && this.options.onError;
|
|
265
344
|
if (typeof handler !== "function") return;
|
|
@@ -276,6 +355,11 @@
|
|
|
276
355
|
} catch {
|
|
277
356
|
}
|
|
278
357
|
}
|
|
358
|
+
_notifyImageLoaded() {
|
|
359
|
+
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
360
|
+
const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
|
|
361
|
+
if (typeof callback === "function") callback();
|
|
362
|
+
}
|
|
279
363
|
/**
|
|
280
364
|
* Initializes the Fabric canvas, viewport elements, and selection event handlers.
|
|
281
365
|
*
|
|
@@ -298,7 +382,7 @@
|
|
|
298
382
|
} else {
|
|
299
383
|
this.containerElement = canvasElement.parentElement;
|
|
300
384
|
}
|
|
301
|
-
this.placeholderElement = this._getElement("
|
|
385
|
+
this.placeholderElement = this._getElement("imagePlaceholder") || null;
|
|
302
386
|
let initialWidth = this.options.canvasWidth;
|
|
303
387
|
let initialHeight = this.options.canvasHeight;
|
|
304
388
|
if (this.containerElement) {
|
|
@@ -448,20 +532,20 @@
|
|
|
448
532
|
});
|
|
449
533
|
}
|
|
450
534
|
});
|
|
451
|
-
this._bindIfExists("
|
|
452
|
-
this._bindIfExists("
|
|
453
|
-
this._bindIfExists("
|
|
535
|
+
this._bindIfExists("zoomInButton", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
536
|
+
this._bindIfExists("zoomOutButton", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
537
|
+
this._bindIfExists("resetImageTransformButton", "click", () => {
|
|
454
538
|
this.resetImageTransform().catch((error) => this._reportError("resetImageTransform failed", error));
|
|
455
539
|
});
|
|
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("
|
|
540
|
+
this._bindIfExists("createMaskButton", "click", () => this.createMask());
|
|
541
|
+
this._bindIfExists("removeSelectedMaskButton", "click", () => this.removeSelectedMask());
|
|
542
|
+
this._bindIfExists("removeAllMasksButton", "click", () => this.removeAllMasks());
|
|
543
|
+
this._bindIfExists("mergeMasksButton", "click", () => this.mergeMasks().catch((error) => this._reportError("merge error", error)));
|
|
544
|
+
this._bindIfExists("downloadImageButton", "click", () => this.downloadImage());
|
|
545
|
+
this._bindIfExists("undoButton", "click", () => this.undo().catch((error) => this._reportError("undo failed", error)));
|
|
546
|
+
this._bindIfExists("redoButton", "click", () => this.redo().catch((error) => this._reportError("redo failed", error)));
|
|
547
|
+
this._bindIfExists("rotateLeftButton", "click", () => {
|
|
548
|
+
const rotationInputElement = this._getElement("rotateLeftDegreesInput");
|
|
465
549
|
let step = this.options.rotationStep;
|
|
466
550
|
if (rotationInputElement) {
|
|
467
551
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -469,8 +553,8 @@
|
|
|
469
553
|
}
|
|
470
554
|
this.rotateImage(this.currentRotation - step).catch((error) => this._reportError("rotateImage failed", error));
|
|
471
555
|
});
|
|
472
|
-
this._bindIfExists("
|
|
473
|
-
const rotationInputElement = this._getElement("
|
|
556
|
+
this._bindIfExists("rotateRightButton", "click", () => {
|
|
557
|
+
const rotationInputElement = this._getElement("rotateRightDegreesInput");
|
|
474
558
|
let step = this.options.rotationStep;
|
|
475
559
|
if (rotationInputElement) {
|
|
476
560
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -478,11 +562,11 @@
|
|
|
478
562
|
}
|
|
479
563
|
this.rotateImage(this.currentRotation + step).catch((error) => this._reportError("rotateImage failed", error));
|
|
480
564
|
});
|
|
481
|
-
this._bindIfExists("
|
|
482
|
-
this._bindIfExists("
|
|
565
|
+
this._bindIfExists("enterCropModeButton", "click", () => this.enterCropMode());
|
|
566
|
+
this._bindIfExists("applyCropButton", "click", () => {
|
|
483
567
|
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
484
568
|
});
|
|
485
|
-
this._bindIfExists("
|
|
569
|
+
this._bindIfExists("cancelCropButton", "click", () => this.cancelCrop());
|
|
486
570
|
this._bindIfExists("maskList", "click", (event) => this._handleMaskListClick(event));
|
|
487
571
|
}
|
|
488
572
|
/**
|
|
@@ -573,12 +657,14 @@
|
|
|
573
657
|
const imageElement = await this._createImageElement(imageBase64);
|
|
574
658
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
575
659
|
let loadSource = imageBase64;
|
|
576
|
-
|
|
577
|
-
|
|
660
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
661
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
662
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
663
|
+
const shouldResize = imageElement.naturalWidth > downsampleMaxWidth || imageElement.naturalHeight > downsampleMaxHeight;
|
|
578
664
|
if (shouldResize) {
|
|
579
665
|
const ratio = Math.min(
|
|
580
|
-
|
|
581
|
-
|
|
666
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
667
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
582
668
|
);
|
|
583
669
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
584
670
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -590,6 +676,8 @@
|
|
|
590
676
|
imageBase64
|
|
591
677
|
);
|
|
592
678
|
}
|
|
679
|
+
} else if (this.options.downsampleOnLoad) {
|
|
680
|
+
this._reportWarning("loadImage: downsample limits must be positive numbers; using the original image");
|
|
593
681
|
}
|
|
594
682
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
595
683
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
@@ -648,9 +736,7 @@
|
|
|
648
736
|
this._updateUI();
|
|
649
737
|
this.canvas.renderAll();
|
|
650
738
|
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
651
|
-
|
|
652
|
-
this.onImageLoaded();
|
|
653
|
-
}
|
|
739
|
+
this._notifyImageLoaded();
|
|
654
740
|
} catch (error) {
|
|
655
741
|
await this._rollbackLoadImageTransaction(transaction);
|
|
656
742
|
throw error;
|
|
@@ -667,6 +753,15 @@
|
|
|
667
753
|
const fabricInstance = ensureFabric();
|
|
668
754
|
return !!(this.originalImage && fabricInstance && this.originalImage instanceof fabricInstance.Image && this.originalImage.width > 0 && this.originalImage.height > 0);
|
|
669
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
758
|
+
*
|
|
759
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
760
|
+
* @public
|
|
761
|
+
*/
|
|
762
|
+
isBusy() {
|
|
763
|
+
return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
|
|
764
|
+
}
|
|
670
765
|
/**
|
|
671
766
|
* Creates an HTMLImageElement from a given data URL.
|
|
672
767
|
*
|
|
@@ -694,7 +789,7 @@
|
|
|
694
789
|
try {
|
|
695
790
|
imageElement.src = "";
|
|
696
791
|
} catch (error) {
|
|
697
|
-
|
|
792
|
+
this._reportWarning("Image timeout cleanup failed", error);
|
|
698
793
|
}
|
|
699
794
|
}, safeTimeoutMs);
|
|
700
795
|
imageElement.onload = () => settle(() => resolve(imageElement));
|
|
@@ -738,7 +833,6 @@
|
|
|
738
833
|
_captureLoadImageTransaction() {
|
|
739
834
|
return {
|
|
740
835
|
canvasState: this._serializeCanvasState(),
|
|
741
|
-
originalImage: this.originalImage,
|
|
742
836
|
baseImageScale: this.baseImageScale,
|
|
743
837
|
currentScale: this.currentScale,
|
|
744
838
|
currentRotation: this.currentRotation,
|
|
@@ -763,6 +857,7 @@
|
|
|
763
857
|
async _rollbackLoadImageTransaction(transaction) {
|
|
764
858
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
765
859
|
let didRestoreCanvasState = false;
|
|
860
|
+
let didFailCanvasRestore = false;
|
|
766
861
|
try {
|
|
767
862
|
if (transaction.canvasState) {
|
|
768
863
|
await this.loadFromState(transaction.canvasState);
|
|
@@ -770,22 +865,27 @@
|
|
|
770
865
|
}
|
|
771
866
|
} catch (error) {
|
|
772
867
|
this._lastMask = null;
|
|
868
|
+
didFailCanvasRestore = true;
|
|
773
869
|
this._reportError("loadImage rollback failed", error);
|
|
774
870
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
this.currentRotation = transaction.currentRotation;
|
|
778
|
-
this.maskCounter = transaction.maskCounter;
|
|
779
|
-
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
780
|
-
this._lastSnapshot = transaction.lastSnapshot;
|
|
781
|
-
if (didRestoreCanvasState) {
|
|
782
|
-
this._restoreLastMaskReference(transaction.lastMask);
|
|
871
|
+
if (didFailCanvasRestore) {
|
|
872
|
+
this._reconcileEditorStateFromCanvas();
|
|
783
873
|
} else {
|
|
784
|
-
this.
|
|
874
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
875
|
+
this.currentScale = transaction.currentScale;
|
|
876
|
+
this.currentRotation = transaction.currentRotation;
|
|
877
|
+
this.maskCounter = transaction.maskCounter;
|
|
878
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
879
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
880
|
+
if (didRestoreCanvasState) {
|
|
881
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
882
|
+
} else {
|
|
883
|
+
this._lastMask = null;
|
|
884
|
+
}
|
|
885
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
886
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
887
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
785
888
|
}
|
|
786
|
-
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
787
|
-
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
788
|
-
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
789
889
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
790
890
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
791
891
|
if (this.containerElement) {
|
|
@@ -798,6 +898,46 @@
|
|
|
798
898
|
this._updateUI();
|
|
799
899
|
if (this.canvas) this.canvas.renderAll();
|
|
800
900
|
}
|
|
901
|
+
_reconcileEditorStateFromCanvas() {
|
|
902
|
+
if (!this.canvas) {
|
|
903
|
+
this.originalImage = null;
|
|
904
|
+
this.baseImageScale = 1;
|
|
905
|
+
this.currentScale = 1;
|
|
906
|
+
this.currentRotation = 0;
|
|
907
|
+
this.maskCounter = 0;
|
|
908
|
+
this.isImageLoadedToCanvas = false;
|
|
909
|
+
this._lastSnapshot = null;
|
|
910
|
+
this._clearMaskPlacementMemory();
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const canvasObjects = this.canvas.getObjects();
|
|
914
|
+
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
915
|
+
if (this.originalImage) {
|
|
916
|
+
const imageScale = Number(this.originalImage.scaleX) || 1;
|
|
917
|
+
this.baseImageScale = imageScale;
|
|
918
|
+
this.currentScale = 1;
|
|
919
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
920
|
+
} else {
|
|
921
|
+
this.baseImageScale = 1;
|
|
922
|
+
this.currentScale = 1;
|
|
923
|
+
this.currentRotation = 0;
|
|
924
|
+
}
|
|
925
|
+
const masks = canvasObjects.filter((object) => object.maskId);
|
|
926
|
+
this.maskCounter = masks.reduce((max, mask) => Math.max(max, Number(mask.maskId) || 0), 0);
|
|
927
|
+
this._lastMask = masks[masks.length - 1] || null;
|
|
928
|
+
if (!this._lastMask) {
|
|
929
|
+
this._lastMaskInitialLeft = null;
|
|
930
|
+
this._lastMaskInitialTop = null;
|
|
931
|
+
this._lastMaskInitialWidth = null;
|
|
932
|
+
}
|
|
933
|
+
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
934
|
+
try {
|
|
935
|
+
this._lastSnapshot = this._serializeCanvasState();
|
|
936
|
+
} catch (error) {
|
|
937
|
+
this._lastSnapshot = null;
|
|
938
|
+
this._reportWarning("loadImage rollback: failed to reconcile canvas snapshot", error);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
801
941
|
_restoreLastMaskReference(previousLastMask) {
|
|
802
942
|
if (!this.canvas) {
|
|
803
943
|
this._lastMask = null;
|
|
@@ -824,12 +964,19 @@
|
|
|
824
964
|
* @private
|
|
825
965
|
*/
|
|
826
966
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
967
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
968
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
969
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
970
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
971
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
972
|
+
throw new Error("Invalid image resample target dimensions");
|
|
973
|
+
}
|
|
827
974
|
const offscreenCanvas = document.createElement("canvas");
|
|
828
|
-
offscreenCanvas.width =
|
|
829
|
-
offscreenCanvas.height =
|
|
975
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
976
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
830
977
|
const context = offscreenCanvas.getContext("2d");
|
|
831
978
|
if (!context) throw new Error("2D canvas context is unavailable");
|
|
832
|
-
context.drawImage(imageElement, 0, 0,
|
|
979
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
833
980
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
834
981
|
}
|
|
835
982
|
_getDataUrlMimeType(dataUrl) {
|
|
@@ -861,6 +1008,7 @@
|
|
|
861
1008
|
* @private
|
|
862
1009
|
*/
|
|
863
1010
|
_setCanvasSizeInt(width, height) {
|
|
1011
|
+
if (!this.canvas) return;
|
|
864
1012
|
const integerWidth = Math.max(1, Math.round(Number(width) || 1));
|
|
865
1013
|
const integerHeight = Math.max(1, Math.round(Number(height) || 1));
|
|
866
1014
|
this.canvas.setWidth(integerWidth);
|
|
@@ -1133,7 +1281,7 @@
|
|
|
1133
1281
|
/**
|
|
1134
1282
|
* Captures editor-owned runtime state that Fabric does not include in canvas JSON.
|
|
1135
1283
|
*
|
|
1136
|
-
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number}} Serializable editor metadata.
|
|
1284
|
+
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number, canvasWidth:number, canvasHeight:number}} Serializable editor metadata.
|
|
1137
1285
|
* @private
|
|
1138
1286
|
*/
|
|
1139
1287
|
_serializeEditorMetadata() {
|
|
@@ -1141,12 +1289,16 @@
|
|
|
1141
1289
|
const currentScale = Number(this.currentScale);
|
|
1142
1290
|
const currentRotation = Number(this.currentRotation);
|
|
1143
1291
|
const maskCounter = Number(this.maskCounter);
|
|
1292
|
+
const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
|
|
1293
|
+
const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
|
|
1144
1294
|
return {
|
|
1145
1295
|
version: 1,
|
|
1146
1296
|
baseImageScale: Number.isFinite(baseImageScale) && baseImageScale > 0 ? baseImageScale : 1,
|
|
1147
1297
|
currentScale: Number.isFinite(currentScale) && currentScale > 0 ? currentScale : 1,
|
|
1148
1298
|
currentRotation: Number.isFinite(currentRotation) ? currentRotation : 0,
|
|
1149
|
-
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0
|
|
1299
|
+
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0,
|
|
1300
|
+
canvasWidth: Number.isFinite(canvasWidth) && canvasWidth > 0 ? Math.round(canvasWidth) : 1,
|
|
1301
|
+
canvasHeight: Number.isFinite(canvasHeight) && canvasHeight > 0 ? Math.round(canvasHeight) : 1
|
|
1150
1302
|
};
|
|
1151
1303
|
}
|
|
1152
1304
|
_serializeCanvasState() {
|
|
@@ -1330,10 +1482,42 @@
|
|
|
1330
1482
|
}
|
|
1331
1483
|
_getJpegBackgroundColor() {
|
|
1332
1484
|
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1333
|
-
if (!backgroundColor || backgroundColor
|
|
1334
|
-
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return "#ffffff";
|
|
1485
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
|
|
1335
1486
|
return backgroundColor;
|
|
1336
1487
|
}
|
|
1488
|
+
_isTransparentCssColor(color) {
|
|
1489
|
+
const normalizedColor = String(color || "").trim().toLowerCase();
|
|
1490
|
+
if (!normalizedColor || normalizedColor === "transparent") return true;
|
|
1491
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1492
|
+
if (hexAlphaMatch) {
|
|
1493
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1494
|
+
return alpha === "0" || alpha === "00";
|
|
1495
|
+
}
|
|
1496
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1497
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1498
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1499
|
+
if (commaAlphaMatch) {
|
|
1500
|
+
const parts = commaAlphaMatch[1].split(",");
|
|
1501
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1502
|
+
}
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1506
|
+
const normalizedAlpha = String(alphaValue || "").trim();
|
|
1507
|
+
if (!normalizedAlpha) return false;
|
|
1508
|
+
if (normalizedAlpha.endsWith("%")) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1509
|
+
return Number(normalizedAlpha) === 0;
|
|
1510
|
+
}
|
|
1511
|
+
_decodeBase64Payload(base64Payload) {
|
|
1512
|
+
const payload = String(base64Payload || "");
|
|
1513
|
+
if (typeof atob === "function") {
|
|
1514
|
+
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
|
|
1517
|
+
return new Uint8Array(Buffer.from(payload, "base64"));
|
|
1518
|
+
}
|
|
1519
|
+
throw new Error("Base64 decoding is unavailable");
|
|
1520
|
+
}
|
|
1337
1521
|
/**
|
|
1338
1522
|
* Gets the top-left corner coordinates of the given object.
|
|
1339
1523
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1450,17 +1634,13 @@
|
|
|
1450
1634
|
requiredWidth = Math.max(requiredWidth, Math.ceil(boundingRect.left + boundingRect.width + padding));
|
|
1451
1635
|
requiredHeight = Math.max(requiredHeight, Math.ceil(boundingRect.top + boundingRect.height + padding));
|
|
1452
1636
|
});
|
|
1453
|
-
const shouldUseScrollSafeViewport = this.options.fitImageToCanvas || this.options.coverImageToCanvas;
|
|
1454
1637
|
let minWidth = 0;
|
|
1455
1638
|
let minHeight = 0;
|
|
1456
|
-
if (
|
|
1639
|
+
if (this.containerElement) {
|
|
1457
1640
|
const viewport = this._getContainerViewportSize();
|
|
1458
1641
|
const safetyMargin = this._getScrollSafetyMargin();
|
|
1459
1642
|
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1460
1643
|
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1461
|
-
} else if (this.containerElement) {
|
|
1462
|
-
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1463
|
-
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1464
1644
|
}
|
|
1465
1645
|
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1466
1646
|
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
@@ -1529,9 +1709,15 @@
|
|
|
1529
1709
|
_assertEditorAvailable(operationName) {
|
|
1530
1710
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1531
1711
|
}
|
|
1712
|
+
_isCropModeAllowedOperation(operationName) {
|
|
1713
|
+
return operationName === "applyCrop" || operationName === "cancelCrop";
|
|
1714
|
+
}
|
|
1532
1715
|
_assertIdleForOperation(operationName, options = {}) {
|
|
1533
1716
|
this._assertEditorAvailable(operationName);
|
|
1534
1717
|
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1718
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1719
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1720
|
+
}
|
|
1535
1721
|
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1536
1722
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1537
1723
|
}
|
|
@@ -1544,10 +1730,14 @@
|
|
|
1544
1730
|
}
|
|
1545
1731
|
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1546
1732
|
this._assertEditorAvailable(operationName);
|
|
1547
|
-
|
|
1733
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1734
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1735
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1736
|
+
}
|
|
1737
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1548
1738
|
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1549
1739
|
}
|
|
1550
|
-
if (this._activeOperationToken && !
|
|
1740
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1551
1741
|
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1552
1742
|
}
|
|
1553
1743
|
}
|
|
@@ -1734,10 +1924,19 @@
|
|
|
1734
1924
|
}
|
|
1735
1925
|
return this.animationQueue.add(async () => {
|
|
1736
1926
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1927
|
+
try {
|
|
1928
|
+
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1929
|
+
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1930
|
+
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1931
|
+
this._pushStateTransition(before, after);
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
try {
|
|
1934
|
+
await this.loadFromState(before);
|
|
1935
|
+
} catch (restoreError) {
|
|
1936
|
+
this._reportError("resetImageTransform rollback failed", restoreError);
|
|
1937
|
+
}
|
|
1938
|
+
throw error;
|
|
1939
|
+
}
|
|
1741
1940
|
}).finally(() => {
|
|
1742
1941
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1743
1942
|
}).catch((error) => {
|
|
@@ -1776,7 +1975,13 @@
|
|
|
1776
1975
|
try {
|
|
1777
1976
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1778
1977
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1779
|
-
|
|
1978
|
+
const restoredCanvasWidth = Number(editorMetadata && editorMetadata.canvasWidth);
|
|
1979
|
+
const restoredCanvasHeight = Number(editorMetadata && editorMetadata.canvasHeight);
|
|
1980
|
+
const hasRestoredCanvasSize = Number.isFinite(restoredCanvasWidth) && restoredCanvasWidth > 0 && Number.isFinite(restoredCanvasHeight) && restoredCanvasHeight > 0;
|
|
1981
|
+
if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
|
|
1982
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
1983
|
+
}
|
|
1984
|
+
const finishLoad = async () => {
|
|
1780
1985
|
try {
|
|
1781
1986
|
if (this._disposed || !this.canvas) {
|
|
1782
1987
|
reject(new Error("Editor was disposed while loading state"));
|
|
@@ -1812,6 +2017,11 @@
|
|
|
1812
2017
|
this.currentScale = 1;
|
|
1813
2018
|
this.currentRotation = 0;
|
|
1814
2019
|
}
|
|
2020
|
+
if (hasRestoredCanvasSize) {
|
|
2021
|
+
this._setCanvasSizeInt(restoredCanvasWidth, restoredCanvasHeight);
|
|
2022
|
+
} else if (this.originalImage && this._shouldResizeCanvasToContentBounds()) {
|
|
2023
|
+
this._updateCanvasSizeToImageBounds();
|
|
2024
|
+
}
|
|
1815
2025
|
const masks = canvasObjects.filter((object) => object.maskId);
|
|
1816
2026
|
masks.forEach((mask) => {
|
|
1817
2027
|
this._restoreMaskControls(mask);
|
|
@@ -1839,6 +2049,9 @@
|
|
|
1839
2049
|
this._reportError("loadFromState() failed", callbackError);
|
|
1840
2050
|
reject(callbackError);
|
|
1841
2051
|
}
|
|
2052
|
+
};
|
|
2053
|
+
this.canvas.loadFromJSON(state, () => {
|
|
2054
|
+
void finishLoad();
|
|
1842
2055
|
});
|
|
1843
2056
|
} catch (error) {
|
|
1844
2057
|
this._reportError("loadFromState() failed", error);
|
|
@@ -1854,12 +2067,12 @@
|
|
|
1854
2067
|
}
|
|
1855
2068
|
_waitForImageElementReady(imageElement) {
|
|
1856
2069
|
if (!imageElement) return Promise.resolve();
|
|
1857
|
-
|
|
2070
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2071
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
2072
|
+
if (imageElement.complete) return Promise.reject(new Error("Image could not be loaded while restoring state"));
|
|
1858
2073
|
return new Promise((resolve, reject) => {
|
|
1859
2074
|
let isSettled = false;
|
|
1860
|
-
|
|
1861
|
-
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1862
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2075
|
+
let timerId;
|
|
1863
2076
|
const settle = (callback) => {
|
|
1864
2077
|
if (isSettled) return;
|
|
1865
2078
|
isSettled = true;
|
|
@@ -1873,8 +2086,20 @@
|
|
|
1873
2086
|
}
|
|
1874
2087
|
callback();
|
|
1875
2088
|
};
|
|
1876
|
-
const handleLoad = () =>
|
|
1877
|
-
|
|
2089
|
+
const handleLoad = () => {
|
|
2090
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2091
|
+
settle(() => {
|
|
2092
|
+
if (didLoad) {
|
|
2093
|
+
resolve();
|
|
2094
|
+
} else {
|
|
2095
|
+
reject(new Error("Image could not be loaded while restoring state"));
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
};
|
|
2099
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error("Image could not be loaded while restoring state")));
|
|
2100
|
+
timerId = setTimeout(() => {
|
|
2101
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
2102
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1878
2103
|
if (typeof imageElement.addEventListener === "function") {
|
|
1879
2104
|
imageElement.addEventListener("load", handleLoad, { once: true });
|
|
1880
2105
|
imageElement.addEventListener("error", handleError, { once: true });
|
|
@@ -1974,14 +2199,7 @@
|
|
|
1974
2199
|
}
|
|
1975
2200
|
_rebindMaskEvents(mask) {
|
|
1976
2201
|
if (!mask) return;
|
|
1977
|
-
|
|
1978
|
-
try {
|
|
1979
|
-
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
1980
|
-
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
1981
|
-
} catch (error) {
|
|
1982
|
-
void error;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
2202
|
+
this._cleanupMaskEvents(mask);
|
|
1985
2203
|
const metadata = {};
|
|
1986
2204
|
if (!Number.isFinite(Number(mask.originalAlpha))) {
|
|
1987
2205
|
metadata.originalAlpha = Number.isFinite(Number(mask.opacity)) ? Number(mask.opacity) : 0.5;
|
|
@@ -2008,6 +2226,22 @@
|
|
|
2008
2226
|
mask.on("mouseout", mouseout);
|
|
2009
2227
|
mask.__imageEditorMaskHandlers = { mouseover, mouseout };
|
|
2010
2228
|
}
|
|
2229
|
+
_cleanupMaskEvents(mask) {
|
|
2230
|
+
if (!mask || !mask.__imageEditorMaskHandlers) return;
|
|
2231
|
+
try {
|
|
2232
|
+
if (typeof mask.off === "function") {
|
|
2233
|
+
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
2234
|
+
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
2235
|
+
}
|
|
2236
|
+
} catch (error) {
|
|
2237
|
+
this._reportWarning("Mask event cleanup failed", error);
|
|
2238
|
+
}
|
|
2239
|
+
try {
|
|
2240
|
+
delete mask.__imageEditorMaskHandlers;
|
|
2241
|
+
} catch (error) {
|
|
2242
|
+
this._reportWarning("Mask event metadata cleanup failed", error);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2011
2245
|
/**
|
|
2012
2246
|
* Creates a mask and adds it to the canvas.
|
|
2013
2247
|
*
|
|
@@ -2146,6 +2380,10 @@
|
|
|
2146
2380
|
});
|
|
2147
2381
|
}
|
|
2148
2382
|
}
|
|
2383
|
+
if (!mask || typeof mask.set !== "function" || typeof mask.setCoords !== "function") {
|
|
2384
|
+
this._reportWarning("fabricGenerator returned an invalid Fabric object");
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2149
2387
|
const styles = maskConfig.styles || {};
|
|
2150
2388
|
const hasStyle = (property) => Object.prototype.hasOwnProperty.call(styles, property);
|
|
2151
2389
|
const maskSettings = {
|
|
@@ -2214,6 +2452,7 @@
|
|
|
2214
2452
|
this.canvas.discardActiveObject();
|
|
2215
2453
|
selectedMasks.forEach((mask) => {
|
|
2216
2454
|
this._removeLabelForMask(mask);
|
|
2455
|
+
this._cleanupMaskEvents(mask);
|
|
2217
2456
|
this.canvas.remove(mask);
|
|
2218
2457
|
});
|
|
2219
2458
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
@@ -2238,7 +2477,10 @@
|
|
|
2238
2477
|
const saveHistory = options.saveHistory !== false;
|
|
2239
2478
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2240
2479
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2241
|
-
masks.forEach((mask) =>
|
|
2480
|
+
masks.forEach((mask) => {
|
|
2481
|
+
this._cleanupMaskEvents(mask);
|
|
2482
|
+
this.canvas.remove(mask);
|
|
2483
|
+
});
|
|
2242
2484
|
this.canvas.discardActiveObject();
|
|
2243
2485
|
this._lastMask = null;
|
|
2244
2486
|
this._lastMaskInitialLeft = null;
|
|
@@ -2273,6 +2515,93 @@
|
|
|
2273
2515
|
}
|
|
2274
2516
|
}
|
|
2275
2517
|
}
|
|
2518
|
+
_captureMaskLabelBackups(masks) {
|
|
2519
|
+
if (!this.canvas) return [];
|
|
2520
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2521
|
+
return (masks || []).map((mask) => {
|
|
2522
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2523
|
+
return {
|
|
2524
|
+
mask,
|
|
2525
|
+
label,
|
|
2526
|
+
hadLabel: !!label,
|
|
2527
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2528
|
+
visible: label ? label.visible : void 0
|
|
2529
|
+
};
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2533
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2534
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2535
|
+
labelBackups.forEach((backup) => {
|
|
2536
|
+
if (!backup || !backup.mask) return;
|
|
2537
|
+
try {
|
|
2538
|
+
if (!backup.hadLabel) {
|
|
2539
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
backup.mask.__label = backup.label;
|
|
2543
|
+
if (!backup.label) return;
|
|
2544
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2545
|
+
this.canvas.add(backup.label);
|
|
2546
|
+
canvasObjects.add(backup.label);
|
|
2547
|
+
}
|
|
2548
|
+
if (backup.visible !== void 0) backup.label.set({ visible: backup.visible });
|
|
2549
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2550
|
+
this._syncMaskLabel(backup.mask);
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
this._reportWarning("restoreMaskLabelBackups: failed to restore mask label", error);
|
|
2553
|
+
}
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
_captureActiveObjectBackup() {
|
|
2557
|
+
if (!this.canvas) return null;
|
|
2558
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2559
|
+
if (!activeObject) return null;
|
|
2560
|
+
const selectedObjects = typeof activeObject.getObjects === "function" ? activeObject.getObjects() : [activeObject];
|
|
2561
|
+
return { activeObject, selectedObjects };
|
|
2562
|
+
}
|
|
2563
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2564
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2565
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2566
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects) ? activeObjectBackup.selectedObjects : [];
|
|
2567
|
+
const canRestore = selectedObjects.length ? selectedObjects.every((object) => canvasObjects.includes(object)) : canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2568
|
+
if (!canRestore) return;
|
|
2569
|
+
try {
|
|
2570
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
void error;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
_captureMaskExportBackups(masks) {
|
|
2576
|
+
return (masks || []).map((mask) => ({
|
|
2577
|
+
object: mask,
|
|
2578
|
+
visible: mask.visible,
|
|
2579
|
+
opacity: mask.opacity,
|
|
2580
|
+
fill: mask.fill,
|
|
2581
|
+
strokeWidth: mask.strokeWidth,
|
|
2582
|
+
stroke: mask.stroke,
|
|
2583
|
+
selectable: mask.selectable,
|
|
2584
|
+
lockRotation: mask.lockRotation
|
|
2585
|
+
}));
|
|
2586
|
+
}
|
|
2587
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
2588
|
+
(maskBackups || []).forEach((backup) => {
|
|
2589
|
+
try {
|
|
2590
|
+
backup.object.set({
|
|
2591
|
+
visible: backup.visible,
|
|
2592
|
+
opacity: backup.opacity,
|
|
2593
|
+
fill: backup.fill,
|
|
2594
|
+
strokeWidth: backup.strokeWidth,
|
|
2595
|
+
stroke: backup.stroke,
|
|
2596
|
+
selectable: backup.selectable,
|
|
2597
|
+
lockRotation: backup.lockRotation
|
|
2598
|
+
});
|
|
2599
|
+
backup.object.setCoords();
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
void error;
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2276
2605
|
/**
|
|
2277
2606
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2278
2607
|
*
|
|
@@ -2345,10 +2674,13 @@
|
|
|
2345
2674
|
_hideAllMaskLabels() {
|
|
2346
2675
|
if (!this.canvas) return;
|
|
2347
2676
|
const canvasObjects = this.canvas.getObjects();
|
|
2677
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2348
2678
|
const labels = canvasObjects.filter((object) => object.maskLabel);
|
|
2349
2679
|
labels.forEach((label) => {
|
|
2350
2680
|
try {
|
|
2351
|
-
if (
|
|
2681
|
+
if (canvasObjectSet.has(label)) {
|
|
2682
|
+
this.canvas.remove(label);
|
|
2683
|
+
}
|
|
2352
2684
|
} catch (error) {
|
|
2353
2685
|
void error;
|
|
2354
2686
|
}
|
|
@@ -2518,6 +2850,9 @@
|
|
|
2518
2850
|
fileType: "png"
|
|
2519
2851
|
}));
|
|
2520
2852
|
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2853
|
+
if (this.canvas.getObjects().some((object) => object.maskId)) {
|
|
2854
|
+
throw new Error("Masks could not be removed during merge");
|
|
2855
|
+
}
|
|
2521
2856
|
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2522
2857
|
preserveScroll: true,
|
|
2523
2858
|
resetMaskCounter: false
|
|
@@ -2591,7 +2926,11 @@
|
|
|
2591
2926
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2592
2927
|
if (!exportImageArea) {
|
|
2593
2928
|
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
2929
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2594
2930
|
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
2931
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
2932
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
2933
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
2595
2934
|
try {
|
|
2596
2935
|
masks2.forEach((mask) => {
|
|
2597
2936
|
mask.set({ visible: false });
|
|
@@ -2616,20 +2955,16 @@
|
|
|
2616
2955
|
void error;
|
|
2617
2956
|
}
|
|
2618
2957
|
});
|
|
2958
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2959
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
2960
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
2619
2961
|
this.canvas.renderAll();
|
|
2620
2962
|
}
|
|
2621
2963
|
}
|
|
2622
2964
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2623
|
-
const maskStyleBackups =
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
fill: mask.fill,
|
|
2627
|
-
strokeWidth: mask.strokeWidth,
|
|
2628
|
-
stroke: mask.stroke,
|
|
2629
|
-
selectable: mask.selectable,
|
|
2630
|
-
lockRotation: mask.lockRotation
|
|
2631
|
-
}));
|
|
2632
|
-
let finalBase64;
|
|
2965
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2966
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2967
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2633
2968
|
try {
|
|
2634
2969
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2635
2970
|
this.canvas.discardActiveObject();
|
|
@@ -2642,7 +2977,7 @@
|
|
|
2642
2977
|
this.originalImage.setCoords();
|
|
2643
2978
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2644
2979
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2645
|
-
|
|
2980
|
+
return await this._exportCanvasRegionToDataURL({
|
|
2646
2981
|
...exportRegion,
|
|
2647
2982
|
multiplier,
|
|
2648
2983
|
quality,
|
|
@@ -2650,24 +2985,11 @@
|
|
|
2650
2985
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2651
2986
|
});
|
|
2652
2987
|
} finally {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
opacity: backup.opacity,
|
|
2657
|
-
fill: backup.fill,
|
|
2658
|
-
strokeWidth: backup.strokeWidth,
|
|
2659
|
-
stroke: backup.stroke,
|
|
2660
|
-
selectable: backup.selectable,
|
|
2661
|
-
lockRotation: backup.lockRotation
|
|
2662
|
-
});
|
|
2663
|
-
backup.object.setCoords();
|
|
2664
|
-
} catch (error) {
|
|
2665
|
-
void error;
|
|
2666
|
-
}
|
|
2667
|
-
});
|
|
2988
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
2989
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
2990
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2668
2991
|
this.canvas.renderAll();
|
|
2669
2992
|
}
|
|
2670
|
-
return finalBase64;
|
|
2671
2993
|
}
|
|
2672
2994
|
/**
|
|
2673
2995
|
* Backward-compatible alias for {@link ImageEditor#exportImageBase64}.
|
|
@@ -2748,13 +3070,8 @@
|
|
|
2748
3070
|
imageElement.src = imageBase64;
|
|
2749
3071
|
});
|
|
2750
3072
|
}
|
|
2751
|
-
const
|
|
3073
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
2752
3074
|
const mime = `image/${safeFileType}`;
|
|
2753
|
-
let byteIndex = binaryString.length;
|
|
2754
|
-
const bytes = new Uint8Array(byteIndex);
|
|
2755
|
-
while (byteIndex--) {
|
|
2756
|
-
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
2757
|
-
}
|
|
2758
3075
|
return new File([bytes], fileName, { type: mime });
|
|
2759
3076
|
}
|
|
2760
3077
|
_clearMaskPlacementMemory() {
|
|
@@ -2799,22 +3116,21 @@
|
|
|
2799
3116
|
this._cropPrevEvented = null;
|
|
2800
3117
|
}
|
|
2801
3118
|
_removeCropRect() {
|
|
2802
|
-
if (
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
3119
|
+
if (this._cropHandlers && this._cropHandlers.length) {
|
|
3120
|
+
this._cropHandlers.forEach((targetHandlers) => {
|
|
3121
|
+
(targetHandlers.handlers || []).forEach((handlerRecord) => {
|
|
3122
|
+
try {
|
|
2807
3123
|
if (targetHandlers.target && typeof targetHandlers.target.off === "function") {
|
|
2808
3124
|
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2809
3125
|
}
|
|
2810
|
-
})
|
|
3126
|
+
} catch (error) {
|
|
3127
|
+
this._reportWarning("Crop handler cleanup failed", error);
|
|
3128
|
+
}
|
|
2811
3129
|
});
|
|
2812
|
-
}
|
|
2813
|
-
} catch (error) {
|
|
2814
|
-
void error;
|
|
3130
|
+
});
|
|
2815
3131
|
}
|
|
2816
3132
|
try {
|
|
2817
|
-
if (this.canvas) this.canvas.remove(this._cropRect);
|
|
3133
|
+
if (this.canvas && this._cropRect) this.canvas.remove(this._cropRect);
|
|
2818
3134
|
} catch (error) {
|
|
2819
3135
|
void error;
|
|
2820
3136
|
}
|
|
@@ -2901,6 +3217,30 @@
|
|
|
2901
3217
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
2902
3218
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
2903
3219
|
cropRect.setCoords();
|
|
3220
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3221
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3222
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3223
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3224
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3225
|
+
let deltaX = 0;
|
|
3226
|
+
let deltaY = 0;
|
|
3227
|
+
if (cropBounds.left < imageLeft) {
|
|
3228
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3229
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3230
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3231
|
+
}
|
|
3232
|
+
if (cropBounds.top < imageTop) {
|
|
3233
|
+
deltaY = imageTop - cropBounds.top;
|
|
3234
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3235
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3236
|
+
}
|
|
3237
|
+
if (deltaX || deltaY) {
|
|
3238
|
+
cropRect.set({
|
|
3239
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3240
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3241
|
+
});
|
|
3242
|
+
cropRect.setCoords();
|
|
3243
|
+
}
|
|
2904
3244
|
this.canvas.requestRenderAll();
|
|
2905
3245
|
} catch (error) {
|
|
2906
3246
|
void error;
|
|
@@ -2960,27 +3300,28 @@
|
|
|
2960
3300
|
try {
|
|
2961
3301
|
beforeJson = this._serializeCanvasState();
|
|
2962
3302
|
} catch (error) {
|
|
2963
|
-
this.
|
|
3303
|
+
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
2964
3304
|
beforeJson = null;
|
|
2965
3305
|
}
|
|
3306
|
+
if (!beforeJson) {
|
|
3307
|
+
this.cancelCrop();
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
2966
3310
|
const preservedMasks = [];
|
|
2967
3311
|
try {
|
|
2968
3312
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2969
3313
|
if (masks && masks.length) {
|
|
2970
3314
|
masks.forEach((mask) => {
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
}
|
|
2982
|
-
} catch (error) {
|
|
2983
|
-
this._reportWarning("applyCrop: failed to remove mask", error);
|
|
3315
|
+
mask.setCoords();
|
|
3316
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3317
|
+
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;
|
|
3318
|
+
this._removeLabelForMask(mask);
|
|
3319
|
+
this._cleanupMaskEvents(mask);
|
|
3320
|
+
this.canvas.remove(mask);
|
|
3321
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3322
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3323
|
+
mask.set({ visible: true });
|
|
3324
|
+
preservedMasks.push(mask);
|
|
2984
3325
|
}
|
|
2985
3326
|
});
|
|
2986
3327
|
this._clearMaskPlacementMemory();
|
|
@@ -2988,7 +3329,8 @@
|
|
|
2988
3329
|
this.canvas.renderAll();
|
|
2989
3330
|
}
|
|
2990
3331
|
} catch (error) {
|
|
2991
|
-
this.
|
|
3332
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error);
|
|
3333
|
+
return;
|
|
2992
3334
|
}
|
|
2993
3335
|
this._removeCropRect();
|
|
2994
3336
|
this._cropMode = false;
|
|
@@ -3045,7 +3387,7 @@
|
|
|
3045
3387
|
* @private
|
|
3046
3388
|
*/
|
|
3047
3389
|
_updateInputs() {
|
|
3048
|
-
const scaleInputElement = this._getElement("
|
|
3390
|
+
const scaleInputElement = this._getElement("scalePercentageInput");
|
|
3049
3391
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
3050
3392
|
}
|
|
3051
3393
|
/**
|
|
@@ -3064,12 +3406,12 @@
|
|
|
3064
3406
|
const canUndo = this.historyManager?.canUndo();
|
|
3065
3407
|
const canRedo = this.historyManager?.canRedo();
|
|
3066
3408
|
const isInCropMode = !!this._cropMode;
|
|
3067
|
-
const isBusy = this.
|
|
3409
|
+
const isBusy = this.isBusy();
|
|
3068
3410
|
if (isInCropMode) {
|
|
3069
3411
|
for (const key of Object.keys(this.elements || {})) {
|
|
3070
3412
|
const element = this._getElement(key);
|
|
3071
3413
|
if (!element) continue;
|
|
3072
|
-
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3414
|
+
if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3073
3415
|
this._setDisabled(key, false);
|
|
3074
3416
|
} else {
|
|
3075
3417
|
this._setDisabled(key, true);
|
|
@@ -3077,28 +3419,32 @@
|
|
|
3077
3419
|
}
|
|
3078
3420
|
return;
|
|
3079
3421
|
}
|
|
3080
|
-
this._setDisabled("
|
|
3081
|
-
this._setDisabled("
|
|
3082
|
-
this._setDisabled("
|
|
3083
|
-
this._setDisabled("
|
|
3084
|
-
this._setDisabled("
|
|
3085
|
-
this._setDisabled("
|
|
3086
|
-
this._setDisabled("
|
|
3087
|
-
this._setDisabled("
|
|
3088
|
-
this._setDisabled("
|
|
3089
|
-
this._setDisabled("
|
|
3090
|
-
this._setDisabled("
|
|
3091
|
-
this._setDisabled("
|
|
3092
|
-
this._setDisabled("
|
|
3093
|
-
this._setDisabled("
|
|
3094
|
-
this._setDisabled("
|
|
3422
|
+
this._setDisabled("zoomInButton", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3423
|
+
this._setDisabled("zoomOutButton", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3424
|
+
this._setDisabled("rotateLeftButton", !hasImage || isBusy);
|
|
3425
|
+
this._setDisabled("rotateRightButton", !hasImage || isBusy);
|
|
3426
|
+
this._setDisabled("createMaskButton", !hasImage || isBusy);
|
|
3427
|
+
this._setDisabled("removeSelectedMaskButton", !hasSelectedMask || isBusy);
|
|
3428
|
+
this._setDisabled("removeAllMasksButton", !hasMasks || isBusy);
|
|
3429
|
+
this._setDisabled("mergeMasksButton", !hasImage || !hasMasks || isBusy);
|
|
3430
|
+
this._setDisabled("downloadImageButton", !hasImage || isBusy);
|
|
3431
|
+
this._setDisabled("resetImageTransformButton", !hasImage || isDefaultTransform || isBusy);
|
|
3432
|
+
this._setDisabled("undoButton", !hasImage || isBusy || !canUndo);
|
|
3433
|
+
this._setDisabled("redoButton", !hasImage || isBusy || !canRedo);
|
|
3434
|
+
this._setDisabled("enterCropModeButton", !hasImage || isBusy);
|
|
3435
|
+
this._setDisabled("applyCropButton", true);
|
|
3436
|
+
this._setDisabled("cancelCropButton", true);
|
|
3437
|
+
this._setDisabled("scalePercentageInput", !hasImage || isBusy);
|
|
3438
|
+
this._setDisabled("rotateLeftDegreesInput", !hasImage || isBusy);
|
|
3439
|
+
this._setDisabled("rotateRightDegreesInput", !hasImage || isBusy);
|
|
3440
|
+
this._setDisabled("maskList", !hasImage || isBusy);
|
|
3095
3441
|
this._setDisabled("imageInput", isBusy);
|
|
3096
3442
|
this._setDisabled("uploadArea", isBusy);
|
|
3097
3443
|
}
|
|
3098
3444
|
/**
|
|
3099
3445
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
3100
3446
|
*
|
|
3101
|
-
* @param {string} key - Key of the element in this.elements (e.g. '
|
|
3447
|
+
* @param {string} key - Key of the element in this.elements (e.g. 'zoomInButton').
|
|
3102
3448
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3103
3449
|
* @private
|
|
3104
3450
|
*/
|
|
@@ -3222,14 +3568,7 @@
|
|
|
3222
3568
|
} catch (error) {
|
|
3223
3569
|
void error;
|
|
3224
3570
|
}
|
|
3225
|
-
if (this._cropRect)
|
|
3226
|
-
try {
|
|
3227
|
-
this.canvas.remove(this._cropRect);
|
|
3228
|
-
} catch (error) {
|
|
3229
|
-
void error;
|
|
3230
|
-
}
|
|
3231
|
-
this._cropRect = null;
|
|
3232
|
-
}
|
|
3571
|
+
if (this._cropRect) this._removeCropRect();
|
|
3233
3572
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3234
3573
|
try {
|
|
3235
3574
|
this._restoreContainerOverflowState();
|
|
@@ -3252,11 +3591,19 @@
|
|
|
3252
3591
|
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3253
3592
|
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3254
3593
|
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
3594
|
+
this.canvasElement.style.maxWidth = this._canvasElementOriginalStyle.maxWidth;
|
|
3255
3595
|
} catch (error) {
|
|
3256
3596
|
void error;
|
|
3257
3597
|
}
|
|
3258
3598
|
}
|
|
3259
3599
|
if (this.canvas) {
|
|
3600
|
+
try {
|
|
3601
|
+
this.canvas.getObjects().forEach((object) => {
|
|
3602
|
+
if (object && object.maskId) this._cleanupMaskEvents(object);
|
|
3603
|
+
});
|
|
3604
|
+
} catch (error) {
|
|
3605
|
+
void error;
|
|
3606
|
+
}
|
|
3260
3607
|
try {
|
|
3261
3608
|
this.canvas.dispose();
|
|
3262
3609
|
} catch (error) {
|
|
@@ -3353,7 +3700,7 @@
|
|
|
3353
3700
|
task.reject(error);
|
|
3354
3701
|
}
|
|
3355
3702
|
} finally {
|
|
3356
|
-
if (
|
|
3703
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3357
3704
|
}
|
|
3358
3705
|
}
|
|
3359
3706
|
} finally {
|
|
@@ -3406,9 +3753,9 @@
|
|
|
3406
3753
|
execute(command) {
|
|
3407
3754
|
const result = command.execute();
|
|
3408
3755
|
if (result && typeof result.then === "function") {
|
|
3409
|
-
return Promise.resolve(result).then(() => {
|
|
3756
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3410
3757
|
this.push(command);
|
|
3411
|
-
});
|
|
3758
|
+
}));
|
|
3412
3759
|
}
|
|
3413
3760
|
this.push(command);
|
|
3414
3761
|
return result;
|