@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.
package/image-editor.d.ts CHANGED
@@ -186,6 +186,7 @@ declare module '@bensitu/image-editor' {
186
186
  init(idMap?: ElementIdMap): void;
187
187
  loadImage(imageBase64: string, options?: LoadImageOptions): Promise<void>;
188
188
  isImageLoaded(): boolean;
189
+ isBusy(): boolean;
189
190
 
190
191
  /** Public callers should pass only `factor`; internal history control options are intentionally not exposed. */
191
192
  scaleImage(factor: number): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bensitu/image-editor",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Lightweight canvas-based image editor",
5
5
  "main": "./dist/image-editor.js",
6
6
  "module": "./dist/image-editor.esm.mjs",
@@ -45,6 +45,7 @@
45
45
  "esbuild": "^0.28.0",
46
46
  "esbuild-plugin-babel": "^0.2.3",
47
47
  "eslint": "^10.4.0",
48
+ "fabric": "^5.5.2",
48
49
  "globals": "^17.6.0"
49
50
  },
50
51
  "browserslist": [
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * @file image-editor.js
3
3
  * @module image-editor
4
- * @version 1.4.1
4
+ * @version 1.4.2
5
5
  * @author Ben Situ
6
6
  * @license MIT
7
7
  * @description Lightweight canvas-based image editor with masking/transform/export support.
8
8
  */
9
9
 
10
10
  let fabric = null;
11
- const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
11
+ const INTERNAL_OPERATION_TOKEN = Symbol.for('ImageEditorInternalOperation');
12
12
 
13
13
  /**
14
14
  * Returns the ambient global scope used to discover a globally loaded Fabric.js namespace.
@@ -768,14 +768,16 @@ function ensureFabric() {
768
768
  if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
769
769
 
770
770
  let loadSource = imageBase64;
771
- if (this.options.downsampleOnLoad) {
771
+ const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
772
+ const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
773
+ if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
772
774
  const shouldResize =
773
- imageElement.naturalWidth > this.options.downsampleMaxWidth ||
774
- imageElement.naturalHeight > this.options.downsampleMaxHeight;
775
+ imageElement.naturalWidth > downsampleMaxWidth ||
776
+ imageElement.naturalHeight > downsampleMaxHeight;
775
777
  if (shouldResize) {
776
778
  const ratio = Math.min(
777
- this.options.downsampleMaxWidth / imageElement.naturalWidth,
778
- this.options.downsampleMaxHeight / imageElement.naturalHeight
779
+ downsampleMaxWidth / imageElement.naturalWidth,
780
+ downsampleMaxHeight / imageElement.naturalHeight
779
781
  );
780
782
  const targetWidth = Math.round(imageElement.naturalWidth * ratio);
781
783
  const targetHeight = Math.round(imageElement.naturalHeight * ratio);
@@ -787,6 +789,8 @@ function ensureFabric() {
787
789
  imageBase64
788
790
  );
789
791
  }
792
+ } else if (this.options.downsampleOnLoad) {
793
+ this._reportWarning('loadImage: downsample limits must be positive numbers; using the original image');
790
794
  }
791
795
 
792
796
  const fabricImage = await this._createFabricImageFromURL(loadSource);
@@ -883,6 +887,22 @@ function ensureFabric() {
883
887
  );
884
888
  }
885
889
 
890
+ /**
891
+ * Checks whether the editor is in a temporary non-mutating state.
892
+ *
893
+ * @returns {boolean} True while loading, animating, cropping, or running a compound operation.
894
+ * @public
895
+ */
896
+ isBusy() {
897
+ return !!(
898
+ this.isAnimating ||
899
+ this._cropMode ||
900
+ this._isLoading ||
901
+ this._activeOperationToken ||
902
+ (this.animationQueue && this.animationQueue.isBusy())
903
+ );
904
+ }
905
+
886
906
  /**
887
907
  * Creates an HTMLImageElement from a given data URL.
888
908
  *
@@ -957,7 +977,6 @@ function ensureFabric() {
957
977
  _captureLoadImageTransaction() {
958
978
  return {
959
979
  canvasState: this._serializeCanvasState(),
960
- originalImage: this.originalImage,
961
980
  baseImageScale: this.baseImageScale,
962
981
  currentScale: this.currentScale,
963
982
  currentRotation: this.currentRotation,
@@ -1048,12 +1067,20 @@ function ensureFabric() {
1048
1067
  * @private
1049
1068
  */
1050
1069
  _resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
1070
+ const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
1071
+ const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
1072
+ const safeTargetWidth = Math.round(Number(targetWidth));
1073
+ const safeTargetHeight = Math.round(Number(targetHeight));
1074
+ if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
1075
+ throw new Error('Invalid image resample target dimensions');
1076
+ }
1077
+
1051
1078
  const offscreenCanvas = document.createElement('canvas');
1052
- offscreenCanvas.width = targetWidth;
1053
- offscreenCanvas.height = targetHeight;
1079
+ offscreenCanvas.width = safeTargetWidth;
1080
+ offscreenCanvas.height = safeTargetHeight;
1054
1081
  const context = offscreenCanvas.getContext('2d');
1055
1082
  if (!context) throw new Error('2D canvas context is unavailable');
1056
- context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
1083
+ context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
1057
1084
  return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
1058
1085
  }
1059
1086
 
@@ -1617,11 +1644,50 @@ function ensureFabric() {
1617
1644
 
1618
1645
  _getJpegBackgroundColor() {
1619
1646
  const backgroundColor = String(this.options.backgroundColor || '').trim();
1620
- if (!backgroundColor || backgroundColor === 'transparent') return '#ffffff';
1621
- if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return '#ffffff';
1647
+ if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
1622
1648
  return backgroundColor;
1623
1649
  }
1624
1650
 
1651
+ _isTransparentCssColor(color) {
1652
+ const normalizedColor = String(color || '').trim().toLowerCase();
1653
+ if (!normalizedColor || normalizedColor === 'transparent') return true;
1654
+
1655
+ const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
1656
+ if (hexAlphaMatch) {
1657
+ const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
1658
+ return alpha === '0' || alpha === '00';
1659
+ }
1660
+
1661
+ const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
1662
+ if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
1663
+
1664
+ const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
1665
+ if (commaAlphaMatch) {
1666
+ const parts = commaAlphaMatch[1].split(',');
1667
+ if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
1668
+ }
1669
+
1670
+ return false;
1671
+ }
1672
+
1673
+ _isZeroCssAlpha(alphaValue) {
1674
+ const normalizedAlpha = String(alphaValue || '').trim();
1675
+ if (!normalizedAlpha) return false;
1676
+ if (normalizedAlpha.endsWith('%')) return Number.parseFloat(normalizedAlpha) === 0;
1677
+ return Number(normalizedAlpha) === 0;
1678
+ }
1679
+
1680
+ _decodeBase64Payload(base64Payload) {
1681
+ const payload = String(base64Payload || '');
1682
+ if (typeof atob === 'function') {
1683
+ return Uint8Array.from(atob(payload), char => char.charCodeAt(0));
1684
+ }
1685
+ if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
1686
+ return new Uint8Array(Buffer.from(payload, 'base64'));
1687
+ }
1688
+ throw new Error('Base64 decoding is unavailable');
1689
+ }
1690
+
1625
1691
  /**
1626
1692
  * Gets the top-left corner coordinates of the given object.
1627
1693
  * Used for geometry calculations (e.g., scale, rotate).
@@ -2111,6 +2177,13 @@ function ensureFabric() {
2111
2177
  ? JSON.parse(serializedState)
2112
2178
  : serializedState;
2113
2179
  const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
2180
+ if (
2181
+ editorMetadata &&
2182
+ Object.prototype.hasOwnProperty.call(editorMetadata, 'version') &&
2183
+ Number(editorMetadata.version) !== 1
2184
+ ) {
2185
+ this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
2186
+ }
2114
2187
 
2115
2188
  this.canvas.loadFromJSON(state, async () => {
2116
2189
  try {
@@ -2204,12 +2277,13 @@ function ensureFabric() {
2204
2277
 
2205
2278
  _waitForImageElementReady(imageElement) {
2206
2279
  if (!imageElement) return Promise.resolve();
2207
- if (imageElement.complete || imageElement.naturalWidth > 0 || imageElement.width > 0) return Promise.resolve();
2280
+ const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
2281
+ (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
2282
+ if (hasLoadedDimensions) return Promise.resolve();
2283
+ if (imageElement.complete) return Promise.reject(new Error('Image could not be loaded while restoring state'));
2208
2284
  return new Promise((resolve, reject) => {
2209
2285
  let isSettled = false;
2210
- const timerId = setTimeout(() => {
2211
- settle(() => reject(new Error('Image load timed out while restoring state')));
2212
- }, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
2286
+ let timerId;
2213
2287
  const settle = (callback) => {
2214
2288
  if (isSettled) return;
2215
2289
  isSettled = true;
@@ -2223,8 +2297,21 @@ function ensureFabric() {
2223
2297
  }
2224
2298
  callback();
2225
2299
  };
2226
- const handleLoad = () => settle(resolve);
2227
- const handleError = (error) => settle(() => reject(error));
2300
+ const handleLoad = () => {
2301
+ const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
2302
+ (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
2303
+ settle(() => {
2304
+ if (didLoad) {
2305
+ resolve();
2306
+ } else {
2307
+ reject(new Error('Image could not be loaded while restoring state'));
2308
+ }
2309
+ });
2310
+ };
2311
+ const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error('Image could not be loaded while restoring state')));
2312
+ timerId = setTimeout(() => {
2313
+ settle(() => reject(new Error('Image load timed out while restoring state')));
2314
+ }, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
2228
2315
  if (typeof imageElement.addEventListener === 'function') {
2229
2316
  imageElement.addEventListener('load', handleLoad, { once: true });
2230
2317
  imageElement.addEventListener('error', handleError, { once: true });
@@ -2517,6 +2604,11 @@ function ensureFabric() {
2517
2604
  }
2518
2605
  }
2519
2606
 
2607
+ if (!mask || typeof mask.set !== 'function' || typeof mask.setCoords !== 'function') {
2608
+ this._reportWarning('fabricGenerator returned an invalid Fabric object');
2609
+ return null;
2610
+ }
2611
+
2520
2612
  const styles = maskConfig.styles || {};
2521
2613
  const hasStyle = property => Object.prototype.hasOwnProperty.call(styles, property);
2522
2614
  const maskSettings = {
@@ -2651,6 +2743,99 @@ function ensureFabric() {
2651
2743
  }
2652
2744
  }
2653
2745
 
2746
+ _captureMaskLabelBackups(masks) {
2747
+ if (!this.canvas) return [];
2748
+ const canvasObjects = new Set(this.canvas.getObjects());
2749
+ return (masks || []).map(mask => {
2750
+ const label = mask && mask.__label ? mask.__label : null;
2751
+ return {
2752
+ mask,
2753
+ label,
2754
+ hadLabel: !!label,
2755
+ labelInCanvas: !!label && canvasObjects.has(label),
2756
+ visible: label ? label.visible : undefined
2757
+ };
2758
+ });
2759
+ }
2760
+
2761
+ _restoreMaskLabelBackups(labelBackups) {
2762
+ if (!this.canvas || !Array.isArray(labelBackups)) return;
2763
+ const canvasObjects = new Set(this.canvas.getObjects());
2764
+ labelBackups.forEach(backup => {
2765
+ if (!backup || !backup.mask) return;
2766
+ try {
2767
+ if (!backup.hadLabel) {
2768
+ if (backup.mask.__label) this._removeLabelForMask(backup.mask);
2769
+ return;
2770
+ }
2771
+ backup.mask.__label = backup.label;
2772
+ if (!backup.label) return;
2773
+ if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
2774
+ this.canvas.add(backup.label);
2775
+ canvasObjects.add(backup.label);
2776
+ }
2777
+ if (backup.visible !== undefined) backup.label.set({ visible: backup.visible });
2778
+ if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
2779
+ this._syncMaskLabel(backup.mask);
2780
+ } catch (error) { void error; }
2781
+ });
2782
+ }
2783
+
2784
+ _captureActiveObjectBackup() {
2785
+ if (!this.canvas) return null;
2786
+ const activeObject = this.canvas.getActiveObject();
2787
+ if (!activeObject) return null;
2788
+ const selectedObjects = typeof activeObject.getObjects === 'function'
2789
+ ? activeObject.getObjects()
2790
+ : [activeObject];
2791
+ return { activeObject, selectedObjects };
2792
+ }
2793
+
2794
+ _restoreActiveObjectBackup(activeObjectBackup) {
2795
+ if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
2796
+ const canvasObjects = this.canvas.getObjects();
2797
+ const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects)
2798
+ ? activeObjectBackup.selectedObjects
2799
+ : [];
2800
+ const canRestore = selectedObjects.length
2801
+ ? selectedObjects.every(object => canvasObjects.includes(object))
2802
+ : canvasObjects.includes(activeObjectBackup.activeObject);
2803
+ if (!canRestore) return;
2804
+ try {
2805
+ this.canvas.setActiveObject(activeObjectBackup.activeObject);
2806
+ } catch (error) { void error; }
2807
+ }
2808
+
2809
+ _captureMaskExportBackups(masks) {
2810
+ return (masks || []).map(mask => ({
2811
+ object: mask,
2812
+ visible: mask.visible,
2813
+ opacity: mask.opacity,
2814
+ fill: mask.fill,
2815
+ strokeWidth: mask.strokeWidth,
2816
+ stroke: mask.stroke,
2817
+ selectable: mask.selectable,
2818
+ lockRotation: mask.lockRotation
2819
+ }));
2820
+ }
2821
+
2822
+ _restoreMaskExportBackups(maskBackups) {
2823
+ (maskBackups || []).forEach(backup => {
2824
+ try {
2825
+ backup.object.set({
2826
+ visible: backup.visible,
2827
+ opacity: backup.opacity,
2828
+ fill: backup.fill,
2829
+ strokeWidth: backup.strokeWidth,
2830
+ stroke: backup.stroke,
2831
+ selectable: backup.selectable,
2832
+ lockRotation: backup.lockRotation
2833
+ });
2834
+ backup.object.setCoords();
2835
+ } catch (error) { void error; }
2836
+ });
2837
+ }
2838
+
2654
2839
  /**
2655
2840
  * Returns a stable zero-based creation index for label callbacks.
2656
2841
  *
@@ -2728,10 +2913,14 @@ function ensureFabric() {
2728
2913
  _hideAllMaskLabels() {
2729
2914
  if (!this.canvas) return;
2730
2915
  const canvasObjects = this.canvas.getObjects();
2916
+ const canvasObjectSet = new Set(canvasObjects);
2731
2917
  const labels = canvasObjects.filter(object => object.maskLabel);
2732
2918
  labels.forEach(label => {
2733
2919
  try {
2734
- if (canvasObjects.includes(label)) this.canvas.remove(label);
2920
+ if (canvasObjectSet.has(label)) {
2921
+ this.canvas.remove(label);
2922
+ canvasObjectSet.delete(label);
2923
+ }
2735
2924
  } catch (error) { void error; }
2736
2925
  });
2737
2926
  canvasObjects.forEach(object => {
@@ -2908,6 +3097,9 @@ function ensureFabric() {
2908
3097
  fileType: 'png'
2909
3098
  }));
2910
3099
  this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
3100
+ if (this.canvas.getObjects().some(object => object.maskId)) {
3101
+ throw new Error('Masks could not be removed during merge');
3102
+ }
2911
3103
  await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
2912
3104
  preserveScroll: true,
2913
3105
  resetMaskCounter: false
@@ -2987,7 +3179,11 @@ function ensureFabric() {
2987
3179
 
2988
3180
  if (!exportImageArea) {
2989
3181
  const masks = this.canvas.getObjects().filter(object => object.maskId || object.maskLabel);
3182
+ const editableMasks = this.canvas.getObjects().filter(object => object.maskId);
2990
3183
  const maskVisibilityBackups = masks.map(mask => ({ object: mask, visible: mask.visible }));
3184
+ const maskStyleBackups = this._captureMaskExportBackups(editableMasks);
3185
+ const labelBackups = this._captureMaskLabelBackups(editableMasks);
3186
+ const activeObjectBackup = this._captureActiveObjectBackup();
2991
3187
 
2992
3188
  try {
2993
3189
  masks.forEach(mask => { mask.set({ visible: false }); });
@@ -3008,21 +3204,18 @@ function ensureFabric() {
3008
3204
  maskVisibilityBackups.forEach(backup => {
3009
3205
  try { backup.object.set({ visible: backup.visible }); } catch (error) { void error; }
3010
3206
  });
3207
+ this._restoreMaskExportBackups(maskStyleBackups);
3208
+ this._restoreMaskLabelBackups(labelBackups);
3209
+ this._restoreActiveObjectBackup(activeObjectBackup);
3011
3210
  this.canvas.renderAll();
3012
3211
  }
3013
3212
  }
3014
3213
 
3015
3214
  // Render masks as export shapes without mutating their editable styles.
3016
3215
  const masks = this.canvas.getObjects().filter(object => object.maskId);
3017
- const maskStyleBackups = masks.map(mask => ({
3018
- object: mask,
3019
- opacity: mask.opacity,
3020
- fill: mask.fill,
3021
- strokeWidth: mask.strokeWidth,
3022
- stroke: mask.stroke,
3023
- selectable: mask.selectable,
3024
- lockRotation: mask.lockRotation
3025
- }));
3216
+ const maskStyleBackups = this._captureMaskExportBackups(masks);
3217
+ const labelBackups = this._captureMaskLabelBackups(masks);
3218
+ const activeObjectBackup = this._captureActiveObjectBackup();
3026
3219
 
3027
3220
  let finalBase64;
3028
3221
  try {
@@ -3052,20 +3245,9 @@ function ensureFabric() {
3052
3245
  sealPartialEdges: this._getPartialExportEdges(imageBounds)
3053
3246
  });
3054
3247
  } finally {
3055
- maskStyleBackups.forEach(backup => {
3056
- try {
3057
- backup.object.set({
3058
- opacity: backup.opacity,
3059
- fill: backup.fill,
3060
- strokeWidth: backup.strokeWidth,
3061
- stroke: backup.stroke,
3062
- selectable: backup.selectable,
3063
- lockRotation: backup.lockRotation
3064
- });
3065
- backup.object.setCoords();
3066
- } catch (error) { void error; }
3067
- });
3068
-
3248
+ this._restoreMaskExportBackups(maskStyleBackups);
3249
+ this._restoreMaskLabelBackups(labelBackups);
3250
+ this._restoreActiveObjectBackup(activeObjectBackup);
3069
3251
  this.canvas.renderAll();
3070
3252
  }
3071
3253
 
@@ -3158,13 +3340,8 @@ function ensureFabric() {
3158
3340
  }
3159
3341
 
3160
3342
  // Convert the final data URL to a File with the requested MIME type.
3161
- const binaryString = atob(imageDataUrl.split(',')[1]);
3343
+ const bytes = this._decodeBase64Payload(imageDataUrl.split(',')[1]);
3162
3344
  const mime = `image/${safeFileType}`;
3163
- let byteIndex = binaryString.length;
3164
- const bytes = new Uint8Array(byteIndex);
3165
- while (byteIndex--) {
3166
- bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
3167
- }
3168
3345
  return new File([bytes], fileName, { type: mime });
3169
3346
  }
3170
3347
 
@@ -3325,6 +3502,30 @@ function ensureFabric() {
3325
3502
  const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
3326
3503
  cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
3327
3504
  cropRect.setCoords();
3505
+ const cropBounds = cropRect.getBoundingRect(true, true);
3506
+ const imageLeft = Number(imageBounds.left) || 0;
3507
+ const imageTop = Number(imageBounds.top) || 0;
3508
+ const imageRight = imageLeft + (Number(imageBounds.width) || 0);
3509
+ const imageBottom = imageTop + (Number(imageBounds.height) || 0);
3510
+ let deltaX = 0;
3511
+ let deltaY = 0;
3512
+ if (cropBounds.left < imageLeft) {
3513
+ deltaX = imageLeft - cropBounds.left;
3514
+ } else if (cropBounds.left + cropBounds.width > imageRight) {
3515
+ deltaX = imageRight - (cropBounds.left + cropBounds.width);
3516
+ }
3517
+ if (cropBounds.top < imageTop) {
3518
+ deltaY = imageTop - cropBounds.top;
3519
+ } else if (cropBounds.top + cropBounds.height > imageBottom) {
3520
+ deltaY = imageBottom - (cropBounds.top + cropBounds.height);
3521
+ }
3522
+ if (deltaX || deltaY) {
3523
+ cropRect.set({
3524
+ left: (Number(cropRect.left) || 0) + deltaX,
3525
+ top: (Number(cropRect.top) || 0) + deltaY
3526
+ });
3527
+ cropRect.setCoords();
3528
+ }
3328
3529
  this.canvas.requestRenderAll();
3329
3530
  } catch (error) { void error; }
3330
3531
  };
@@ -3404,23 +3605,19 @@ function ensureFabric() {
3404
3605
  const masks = this.canvas.getObjects().filter(object => object.maskId);
3405
3606
  if (masks && masks.length) {
3406
3607
  masks.forEach(mask => {
3407
- try {
3408
- mask.setCoords();
3409
- const maskBounds = mask.getBoundingRect(true, true);
3410
- const intersectsCrop =
3411
- maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth &&
3412
- maskBounds.left + maskBounds.width > cropRegion.sourceX &&
3413
- maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight &&
3414
- maskBounds.top + maskBounds.height > cropRegion.sourceY;
3415
- this._removeLabelForMask(mask);
3416
- this.canvas.remove(mask);
3417
- if (shouldPreserveMasks && intersectsCrop) {
3418
- this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
3419
- mask.set({ visible: true });
3420
- preservedMasks.push(mask);
3421
- }
3422
- } catch (error) {
3423
- this._reportWarning('applyCrop: failed to remove mask', error);
3608
+ mask.setCoords();
3609
+ const maskBounds = mask.getBoundingRect(true, true);
3610
+ const intersectsCrop =
3611
+ maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth &&
3612
+ maskBounds.left + maskBounds.width > cropRegion.sourceX &&
3613
+ maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight &&
3614
+ maskBounds.top + maskBounds.height > cropRegion.sourceY;
3615
+ this._removeLabelForMask(mask);
3616
+ this.canvas.remove(mask);
3617
+ if (shouldPreserveMasks && intersectsCrop) {
3618
+ this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
3619
+ mask.set({ visible: true });
3620
+ preservedMasks.push(mask);
3424
3621
  }
3425
3622
  });
3426
3623
  this._clearMaskPlacementMemory();
@@ -3428,7 +3625,8 @@ function ensureFabric() {
3428
3625
  this.canvas.renderAll();
3429
3626
  }
3430
3627
  } catch (error) {
3431
- this._reportWarning('applyCrop: error while removing masks', error);
3628
+ await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error);
3629
+ return;
3432
3630
  }
3433
3631
 
3434
3632
  this._removeCropRect();
@@ -3520,7 +3718,7 @@ function ensureFabric() {
3520
3718
  const canUndo = this.historyManager?.canUndo();
3521
3719
  const canRedo = this.historyManager?.canRedo();
3522
3720
  const isInCropMode = !!this._cropMode;
3523
- const isBusy = this.isAnimating || this._isLoading || !!this._activeOperationToken || !!(this.animationQueue && this.animationQueue.isBusy());
3721
+ const isBusy = this.isBusy();
3524
3722
 
3525
3723
  if (isInCropMode) {
3526
3724
  // Disable all controls except the crop action buttons while crop mode is active.
@@ -3551,6 +3749,10 @@ function ensureFabric() {
3551
3749
  this._setDisabled('cropBtn', !hasImage || isBusy);
3552
3750
  this._setDisabled('applyCropBtn', true);
3553
3751
  this._setDisabled('cancelCropBtn', true);
3752
+ this._setDisabled('scaleRate', !hasImage || isBusy);
3753
+ this._setDisabled('rotationLeftInput', !hasImage || isBusy);
3754
+ this._setDisabled('rotationRightInput', !hasImage || isBusy);
3755
+ this._setDisabled('maskList', !hasImage || isBusy);
3554
3756
  this._setDisabled('imageInput', isBusy);
3555
3757
  this._setDisabled('uploadArea', isBusy);
3556
3758
  }
@@ -3940,9 +4142,9 @@ function ensureFabric() {
3940
4142
  execute(command) {
3941
4143
  const result = command.execute();
3942
4144
  if (result && typeof result.then === 'function') {
3943
- return Promise.resolve(result).then(() => {
4145
+ return this.enqueue(() => Promise.resolve(result).then(() => {
3944
4146
  this.push(command);
3945
- });
4147
+ }));
3946
4148
  }
3947
4149
  this.push(command);
3948
4150
  return result;