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