@bensitu/image-editor 1.4.1 → 1.4.2

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.
@@ -3,13 +3,13 @@
3
3
  /**
4
4
  * @file image-editor.js
5
5
  * @module image-editor
6
- * @version 1.4.1
6
+ * @version 1.4.2
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;
@@ -573,12 +573,14 @@
573
573
  const imageElement = await this._createImageElement(imageBase64);
574
574
  if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
575
575
  let loadSource = imageBase64;
576
- if (this.options.downsampleOnLoad) {
577
- const shouldResize = imageElement.naturalWidth > this.options.downsampleMaxWidth || imageElement.naturalHeight > this.options.downsampleMaxHeight;
576
+ const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
577
+ const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
578
+ if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
579
+ const shouldResize = imageElement.naturalWidth > downsampleMaxWidth || imageElement.naturalHeight > downsampleMaxHeight;
578
580
  if (shouldResize) {
579
581
  const ratio = Math.min(
580
- this.options.downsampleMaxWidth / imageElement.naturalWidth,
581
- this.options.downsampleMaxHeight / imageElement.naturalHeight
582
+ downsampleMaxWidth / imageElement.naturalWidth,
583
+ downsampleMaxHeight / imageElement.naturalHeight
582
584
  );
583
585
  const targetWidth = Math.round(imageElement.naturalWidth * ratio);
584
586
  const targetHeight = Math.round(imageElement.naturalHeight * ratio);
@@ -590,6 +592,8 @@
590
592
  imageBase64
591
593
  );
592
594
  }
595
+ } else if (this.options.downsampleOnLoad) {
596
+ this._reportWarning("loadImage: downsample limits must be positive numbers; using the original image");
593
597
  }
594
598
  const fabricImage = await this._createFabricImageFromURL(loadSource);
595
599
  if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
@@ -667,6 +671,15 @@
667
671
  const fabricInstance = ensureFabric();
668
672
  return !!(this.originalImage && fabricInstance && this.originalImage instanceof fabricInstance.Image && this.originalImage.width > 0 && this.originalImage.height > 0);
669
673
  }
674
+ /**
675
+ * Checks whether the editor is in a temporary non-mutating state.
676
+ *
677
+ * @returns {boolean} True while loading, animating, cropping, or running a compound operation.
678
+ * @public
679
+ */
680
+ isBusy() {
681
+ return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
682
+ }
670
683
  /**
671
684
  * Creates an HTMLImageElement from a given data URL.
672
685
  *
@@ -738,7 +751,6 @@
738
751
  _captureLoadImageTransaction() {
739
752
  return {
740
753
  canvasState: this._serializeCanvasState(),
741
- originalImage: this.originalImage,
742
754
  baseImageScale: this.baseImageScale,
743
755
  currentScale: this.currentScale,
744
756
  currentRotation: this.currentRotation,
@@ -824,12 +836,19 @@
824
836
  * @private
825
837
  */
826
838
  _resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
839
+ const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
840
+ const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
841
+ const safeTargetWidth = Math.round(Number(targetWidth));
842
+ const safeTargetHeight = Math.round(Number(targetHeight));
843
+ if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
844
+ throw new Error("Invalid image resample target dimensions");
845
+ }
827
846
  const offscreenCanvas = document.createElement("canvas");
828
- offscreenCanvas.width = targetWidth;
829
- offscreenCanvas.height = targetHeight;
847
+ offscreenCanvas.width = safeTargetWidth;
848
+ offscreenCanvas.height = safeTargetHeight;
830
849
  const context = offscreenCanvas.getContext("2d");
831
850
  if (!context) throw new Error("2D canvas context is unavailable");
832
- context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
851
+ context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
833
852
  return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
834
853
  }
835
854
  _getDataUrlMimeType(dataUrl) {
@@ -1330,10 +1349,42 @@
1330
1349
  }
1331
1350
  _getJpegBackgroundColor() {
1332
1351
  const backgroundColor = String(this.options.backgroundColor || "").trim();
1333
- if (!backgroundColor || backgroundColor === "transparent") return "#ffffff";
1334
- if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return "#ffffff";
1352
+ if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
1335
1353
  return backgroundColor;
1336
1354
  }
1355
+ _isTransparentCssColor(color) {
1356
+ const normalizedColor = String(color || "").trim().toLowerCase();
1357
+ if (!normalizedColor || normalizedColor === "transparent") return true;
1358
+ const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
1359
+ if (hexAlphaMatch) {
1360
+ const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
1361
+ return alpha === "0" || alpha === "00";
1362
+ }
1363
+ const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
1364
+ if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
1365
+ const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
1366
+ if (commaAlphaMatch) {
1367
+ const parts = commaAlphaMatch[1].split(",");
1368
+ if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
1369
+ }
1370
+ return false;
1371
+ }
1372
+ _isZeroCssAlpha(alphaValue) {
1373
+ const normalizedAlpha = String(alphaValue || "").trim();
1374
+ if (!normalizedAlpha) return false;
1375
+ if (normalizedAlpha.endsWith("%")) return Number.parseFloat(normalizedAlpha) === 0;
1376
+ return Number(normalizedAlpha) === 0;
1377
+ }
1378
+ _decodeBase64Payload(base64Payload) {
1379
+ const payload = String(base64Payload || "");
1380
+ if (typeof atob === "function") {
1381
+ return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
1382
+ }
1383
+ if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
1384
+ return new Uint8Array(Buffer.from(payload, "base64"));
1385
+ }
1386
+ throw new Error("Base64 decoding is unavailable");
1387
+ }
1337
1388
  /**
1338
1389
  * Gets the top-left corner coordinates of the given object.
1339
1390
  * Used for geometry calculations (e.g., scale, rotate).
@@ -1776,6 +1827,9 @@
1776
1827
  try {
1777
1828
  const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
1778
1829
  const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
1830
+ if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
1831
+ this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
1832
+ }
1779
1833
  this.canvas.loadFromJSON(state, async () => {
1780
1834
  try {
1781
1835
  if (this._disposed || !this.canvas) {
@@ -1854,12 +1908,12 @@
1854
1908
  }
1855
1909
  _waitForImageElementReady(imageElement) {
1856
1910
  if (!imageElement) return Promise.resolve();
1857
- if (imageElement.complete || imageElement.naturalWidth > 0 || imageElement.width > 0) return Promise.resolve();
1911
+ const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
1912
+ if (hasLoadedDimensions) return Promise.resolve();
1913
+ if (imageElement.complete) return Promise.reject(new Error("Image could not be loaded while restoring state"));
1858
1914
  return new Promise((resolve, reject) => {
1859
1915
  let isSettled = false;
1860
- const timerId = setTimeout(() => {
1861
- settle(() => reject(new Error("Image load timed out while restoring state")));
1862
- }, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
1916
+ let timerId;
1863
1917
  const settle = (callback) => {
1864
1918
  if (isSettled) return;
1865
1919
  isSettled = true;
@@ -1873,8 +1927,20 @@
1873
1927
  }
1874
1928
  callback();
1875
1929
  };
1876
- const handleLoad = () => settle(resolve);
1877
- const handleError = (error) => settle(() => reject(error));
1930
+ const handleLoad = () => {
1931
+ const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
1932
+ settle(() => {
1933
+ if (didLoad) {
1934
+ resolve();
1935
+ } else {
1936
+ reject(new Error("Image could not be loaded while restoring state"));
1937
+ }
1938
+ });
1939
+ };
1940
+ const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error("Image could not be loaded while restoring state")));
1941
+ timerId = setTimeout(() => {
1942
+ settle(() => reject(new Error("Image load timed out while restoring state")));
1943
+ }, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
1878
1944
  if (typeof imageElement.addEventListener === "function") {
1879
1945
  imageElement.addEventListener("load", handleLoad, { once: true });
1880
1946
  imageElement.addEventListener("error", handleError, { once: true });
@@ -2146,6 +2212,10 @@
2146
2212
  });
2147
2213
  }
2148
2214
  }
2215
+ if (!mask || typeof mask.set !== "function" || typeof mask.setCoords !== "function") {
2216
+ this._reportWarning("fabricGenerator returned an invalid Fabric object");
2217
+ return null;
2218
+ }
2149
2219
  const styles = maskConfig.styles || {};
2150
2220
  const hasStyle = (property) => Object.prototype.hasOwnProperty.call(styles, property);
2151
2221
  const maskSettings = {
@@ -2273,6 +2343,93 @@
2273
2343
  }
2274
2344
  }
2275
2345
  }
2346
+ _captureMaskLabelBackups(masks) {
2347
+ if (!this.canvas) return [];
2348
+ const canvasObjects = new Set(this.canvas.getObjects());
2349
+ return (masks || []).map((mask) => {
2350
+ const label = mask && mask.__label ? mask.__label : null;
2351
+ return {
2352
+ mask,
2353
+ label,
2354
+ hadLabel: !!label,
2355
+ labelInCanvas: !!label && canvasObjects.has(label),
2356
+ visible: label ? label.visible : void 0
2357
+ };
2358
+ });
2359
+ }
2360
+ _restoreMaskLabelBackups(labelBackups) {
2361
+ if (!this.canvas || !Array.isArray(labelBackups)) return;
2362
+ const canvasObjects = new Set(this.canvas.getObjects());
2363
+ labelBackups.forEach((backup) => {
2364
+ if (!backup || !backup.mask) return;
2365
+ try {
2366
+ if (!backup.hadLabel) {
2367
+ if (backup.mask.__label) this._removeLabelForMask(backup.mask);
2368
+ return;
2369
+ }
2370
+ backup.mask.__label = backup.label;
2371
+ if (!backup.label) return;
2372
+ if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
2373
+ this.canvas.add(backup.label);
2374
+ canvasObjects.add(backup.label);
2375
+ }
2376
+ if (backup.visible !== void 0) backup.label.set({ visible: backup.visible });
2377
+ if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
2378
+ this._syncMaskLabel(backup.mask);
2379
+ } catch (error) {
2380
+ void error;
2381
+ }
2382
+ });
2383
+ }
2384
+ _captureActiveObjectBackup() {
2385
+ if (!this.canvas) return null;
2386
+ const activeObject = this.canvas.getActiveObject();
2387
+ if (!activeObject) return null;
2388
+ const selectedObjects = typeof activeObject.getObjects === "function" ? activeObject.getObjects() : [activeObject];
2389
+ return { activeObject, selectedObjects };
2390
+ }
2391
+ _restoreActiveObjectBackup(activeObjectBackup) {
2392
+ if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
2393
+ const canvasObjects = this.canvas.getObjects();
2394
+ const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects) ? activeObjectBackup.selectedObjects : [];
2395
+ const canRestore = selectedObjects.length ? selectedObjects.every((object) => canvasObjects.includes(object)) : canvasObjects.includes(activeObjectBackup.activeObject);
2396
+ if (!canRestore) return;
2397
+ try {
2398
+ this.canvas.setActiveObject(activeObjectBackup.activeObject);
2399
+ } catch (error) {
2400
+ void error;
2401
+ }
2402
+ }
2403
+ _captureMaskExportBackups(masks) {
2404
+ return (masks || []).map((mask) => ({
2405
+ object: mask,
2406
+ visible: mask.visible,
2407
+ opacity: mask.opacity,
2408
+ fill: mask.fill,
2409
+ strokeWidth: mask.strokeWidth,
2410
+ stroke: mask.stroke,
2411
+ selectable: mask.selectable,
2412
+ lockRotation: mask.lockRotation
2413
+ }));
2414
+ }
2415
+ _restoreMaskExportBackups(maskBackups) {
2416
+ (maskBackups || []).forEach((backup) => {
2417
+ try {
2418
+ backup.object.set({
2419
+ visible: backup.visible,
2420
+ opacity: backup.opacity,
2421
+ fill: backup.fill,
2422
+ strokeWidth: backup.strokeWidth,
2423
+ stroke: backup.stroke,
2424
+ selectable: backup.selectable,
2425
+ lockRotation: backup.lockRotation
2426
+ });
2427
+ backup.object.setCoords();
2428
+ } catch (error) {
2429
+ void error;
2430
+ }
2431
+ });
2432
+ }
2276
2433
  /**
2277
2434
  * Returns a stable zero-based creation index for label callbacks.
2278
2435
  *
@@ -2345,10 +2502,14 @@
2345
2502
  _hideAllMaskLabels() {
2346
2503
  if (!this.canvas) return;
2347
2504
  const canvasObjects = this.canvas.getObjects();
2505
+ const canvasObjectSet = new Set(canvasObjects);
2348
2506
  const labels = canvasObjects.filter((object) => object.maskLabel);
2349
2507
  labels.forEach((label) => {
2350
2508
  try {
2351
- if (canvasObjects.includes(label)) this.canvas.remove(label);
2509
+ if (canvasObjectSet.has(label)) {
2510
+ this.canvas.remove(label);
2511
+ canvasObjectSet.delete(label);
2512
+ }
2352
2513
  } catch (error) {
2353
2514
  void error;
2354
2515
  }
@@ -2518,6 +2679,9 @@
2518
2679
  fileType: "png"
2519
2680
  }));
2520
2681
  this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
2682
+ if (this.canvas.getObjects().some((object) => object.maskId)) {
2683
+ throw new Error("Masks could not be removed during merge");
2684
+ }
2521
2685
  await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
2522
2686
  preserveScroll: true,
2523
2687
  resetMaskCounter: false
@@ -2591,7 +2755,11 @@
2591
2755
  const format = this._normalizeImageFormat(options.fileType || options.format);
2592
2756
  if (!exportImageArea) {
2593
2757
  const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
2758
+ const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
2594
2759
  const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
2760
+ const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
2761
+ const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
2762
+ const activeObjectBackup2 = this._captureActiveObjectBackup();
2595
2763
  try {
2596
2764
  masks2.forEach((mask) => {
2597
2765
  mask.set({ visible: false });
@@ -2616,19 +2784,16 @@
2616
2784
  void error;
2617
2785
  }
2618
2786
  });
2787
+ this._restoreMaskExportBackups(maskStyleBackups2);
2788
+ this._restoreMaskLabelBackups(labelBackups2);
2789
+ this._restoreActiveObjectBackup(activeObjectBackup2);
2619
2790
  this.canvas.renderAll();
2620
2791
  }
2621
2792
  }
2622
2793
  const masks = this.canvas.getObjects().filter((object) => object.maskId);
2623
- const maskStyleBackups = masks.map((mask) => ({
2624
- object: mask,
2625
- opacity: mask.opacity,
2626
- fill: mask.fill,
2627
- strokeWidth: mask.strokeWidth,
2628
- stroke: mask.stroke,
2629
- selectable: mask.selectable,
2630
- lockRotation: mask.lockRotation
2631
- }));
2794
+ const maskStyleBackups = this._captureMaskExportBackups(masks);
2795
+ const labelBackups = this._captureMaskLabelBackups(masks);
2796
+ const activeObjectBackup = this._captureActiveObjectBackup();
2632
2797
  let finalBase64;
2633
2798
  try {
2634
2799
  masks.forEach((mask) => this._removeLabelForMask(mask));
@@ -2650,21 +2815,9 @@
2650
2815
  sealPartialEdges: this._getPartialExportEdges(imageBounds)
2651
2816
  });
2652
2817
  } finally {
2653
- maskStyleBackups.forEach((backup) => {
2654
- try {
2655
- backup.object.set({
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
- });
2818
+ this._restoreMaskExportBackups(maskStyleBackups);
2819
+ this._restoreMaskLabelBackups(labelBackups);
2820
+ this._restoreActiveObjectBackup(activeObjectBackup);
2668
2821
  this.canvas.renderAll();
2669
2822
  }
2670
2823
  return finalBase64;
@@ -2748,13 +2901,8 @@
2748
2901
  imageElement.src = imageBase64;
2749
2902
  });
2750
2903
  }
2751
- const binaryString = atob(imageDataUrl.split(",")[1]);
2904
+ const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
2752
2905
  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
2906
  return new File([bytes], fileName, { type: mime });
2759
2907
  }
2760
2908
  _clearMaskPlacementMemory() {
@@ -2901,6 +3049,30 @@
2901
3049
  const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
2902
3050
  cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
2903
3051
  cropRect.setCoords();
3052
+ const cropBounds = cropRect.getBoundingRect(true, true);
3053
+ const imageLeft = Number(imageBounds.left) || 0;
3054
+ const imageTop = Number(imageBounds.top) || 0;
3055
+ const imageRight = imageLeft + (Number(imageBounds.width) || 0);
3056
+ const imageBottom = imageTop + (Number(imageBounds.height) || 0);
3057
+ let deltaX = 0;
3058
+ let deltaY = 0;
3059
+ if (cropBounds.left < imageLeft) {
3060
+ deltaX = imageLeft - cropBounds.left;
3061
+ } else if (cropBounds.left + cropBounds.width > imageRight) {
3062
+ deltaX = imageRight - (cropBounds.left + cropBounds.width);
3063
+ }
3064
+ if (cropBounds.top < imageTop) {
3065
+ deltaY = imageTop - cropBounds.top;
3066
+ } else if (cropBounds.top + cropBounds.height > imageBottom) {
3067
+ deltaY = imageBottom - (cropBounds.top + cropBounds.height);
3068
+ }
3069
+ if (deltaX || deltaY) {
3070
+ cropRect.set({
3071
+ left: (Number(cropRect.left) || 0) + deltaX,
3072
+ top: (Number(cropRect.top) || 0) + deltaY
3073
+ });
3074
+ cropRect.setCoords();
3075
+ }
2904
3076
  this.canvas.requestRenderAll();
2905
3077
  } catch (error) {
2906
3078
  void error;
@@ -2968,19 +3140,15 @@
2968
3140
  const masks = this.canvas.getObjects().filter((object) => object.maskId);
2969
3141
  if (masks && masks.length) {
2970
3142
  masks.forEach((mask) => {
2971
- try {
2972
- mask.setCoords();
2973
- const maskBounds = mask.getBoundingRect(true, true);
2974
- 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;
2975
- this._removeLabelForMask(mask);
2976
- this.canvas.remove(mask);
2977
- if (shouldPreserveMasks && intersectsCrop) {
2978
- this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
2979
- mask.set({ visible: true });
2980
- preservedMasks.push(mask);
2981
- }
2982
- } catch (error) {
2983
- this._reportWarning("applyCrop: failed to remove mask", error);
3143
+ mask.setCoords();
3144
+ const maskBounds = mask.getBoundingRect(true, true);
3145
+ 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;
3146
+ this._removeLabelForMask(mask);
3147
+ this.canvas.remove(mask);
3148
+ if (shouldPreserveMasks && intersectsCrop) {
3149
+ this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
3150
+ mask.set({ visible: true });
3151
+ preservedMasks.push(mask);
2984
3152
  }
2985
3153
  });
2986
3154
  this._clearMaskPlacementMemory();
@@ -2988,7 +3156,8 @@
2988
3156
  this.canvas.renderAll();
2989
3157
  }
2990
3158
  } catch (error) {
2991
- this._reportWarning("applyCrop: error while removing masks", error);
3159
+ await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error);
3160
+ return;
2992
3161
  }
2993
3162
  this._removeCropRect();
2994
3163
  this._cropMode = false;
@@ -3064,7 +3233,7 @@
3064
3233
  const canUndo = this.historyManager?.canUndo();
3065
3234
  const canRedo = this.historyManager?.canRedo();
3066
3235
  const isInCropMode = !!this._cropMode;
3067
- const isBusy = this.isAnimating || this._isLoading || !!this._activeOperationToken || !!(this.animationQueue && this.animationQueue.isBusy());
3236
+ const isBusy = this.isBusy();
3068
3237
  if (isInCropMode) {
3069
3238
  for (const key of Object.keys(this.elements || {})) {
3070
3239
  const element = this._getElement(key);
@@ -3092,6 +3261,10 @@
3092
3261
  this._setDisabled("cropBtn", !hasImage || isBusy);
3093
3262
  this._setDisabled("applyCropBtn", true);
3094
3263
  this._setDisabled("cancelCropBtn", true);
3264
+ this._setDisabled("scaleRate", !hasImage || isBusy);
3265
+ this._setDisabled("rotationLeftInput", !hasImage || isBusy);
3266
+ this._setDisabled("rotationRightInput", !hasImage || isBusy);
3267
+ this._setDisabled("maskList", !hasImage || isBusy);
3095
3268
  this._setDisabled("imageInput", isBusy);
3096
3269
  this._setDisabled("uploadArea", isBusy);
3097
3270
  }
@@ -3406,9 +3579,9 @@
3406
3579
  execute(command) {
3407
3580
  const result = command.execute();
3408
3581
  if (result && typeof result.then === "function") {
3409
- return Promise.resolve(result).then(() => {
3582
+ return this.enqueue(() => Promise.resolve(result).then(() => {
3410
3583
  this.push(command);
3411
- });
3584
+ }));
3412
3585
  }
3413
3586
  this.push(command);
3414
3587
  return result;