@bensitu/image-editor 1.5.1 → 1.5.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,7 +3,7 @@
3
3
  /**
4
4
  * @file image-editor.js
5
5
  * @module image-editor
6
- * @version 1.5.1
6
+ * @version 1.5.2
7
7
  * @author Ben Situ
8
8
  * @license MIT
9
9
  * @description Lightweight canvas-based image editor with masking/transform/export support.
@@ -76,6 +76,7 @@
76
76
  imageLoadTimeoutMs: 3e4,
77
77
  exportMultiplier: 1,
78
78
  maxExportPixels: 5e7,
79
+ maxHistorySize: 50,
79
80
  exportImageAreaByDefault: true,
80
81
  defaultMaskWidth: 50,
81
82
  defaultMaskHeight: 80,
@@ -104,6 +105,7 @@
104
105
  ...userCrop
105
106
  }
106
107
  };
108
+ this._normalizeOptions();
107
109
  this._fabricLoaded = !!ensureFabric();
108
110
  if (!this._fabricLoaded) {
109
111
  this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
@@ -123,16 +125,18 @@
123
125
  this._activeOperationToken = null;
124
126
  this.elements = {};
125
127
  this.isImageLoadedToCanvas = false;
126
- this.maxHistorySize = 50;
128
+ this.maxHistorySize = this.options.maxHistorySize;
127
129
  this._handlersByElementKey = {};
128
130
  this._elementCache = {};
129
131
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
132
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
130
133
  this._lastMask = null;
131
134
  this._lastMaskInitialLeft = null;
132
135
  this._lastMaskInitialTop = null;
133
136
  this._lastMaskInitialWidth = null;
134
137
  this._lastSnapshot = null;
135
138
  this._cropMode = false;
139
+ this._isApplyingCrop = false;
136
140
  this._cropRect = null;
137
141
  this._cropHandlers = [];
138
142
  this._cropPrevEvented = null;
@@ -229,6 +233,8 @@
229
233
  this._activeOperationName = null;
230
234
  this._activeOperationToken = null;
231
235
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
236
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
237
+ this._isApplyingCrop = false;
232
238
  this._containerOriginalOverflow = null;
233
239
  this._lastContainerViewportSize = null;
234
240
  this._canvasElementOriginalStyle = null;
@@ -348,6 +354,54 @@
348
354
  `ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
349
355
  );
350
356
  }
357
+ _normalizeFiniteNumber(value, fallback) {
358
+ const numericValue = Number(value);
359
+ return Number.isFinite(numericValue) ? numericValue : fallback;
360
+ }
361
+ _normalizePositiveNumber(value, fallback) {
362
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
363
+ return numericValue > 0 ? numericValue : fallback;
364
+ }
365
+ _normalizeNonNegativeNumber(value, fallback) {
366
+ const numericValue = this._normalizeFiniteNumber(value, fallback);
367
+ return numericValue >= 0 ? numericValue : fallback;
368
+ }
369
+ _normalizePositiveInteger(value, fallback) {
370
+ const numericValue = this._normalizePositiveNumber(value, fallback);
371
+ return Math.max(1, Math.floor(numericValue));
372
+ }
373
+ _normalizeOptions() {
374
+ const options = this.options || {};
375
+ options.canvasWidth = this._normalizePositiveNumber(options.canvasWidth, 800);
376
+ options.canvasHeight = this._normalizePositiveNumber(options.canvasHeight, 600);
377
+ options.animationDuration = this._normalizeNonNegativeNumber(options.animationDuration, 300);
378
+ const minScale = this._normalizePositiveNumber(options.minScale, 0.1);
379
+ const maxScale = this._normalizePositiveNumber(options.maxScale, 5);
380
+ if (minScale > maxScale) {
381
+ options.minScale = 0.1;
382
+ options.maxScale = 5;
383
+ } else {
384
+ options.minScale = minScale;
385
+ options.maxScale = maxScale;
386
+ }
387
+ options.scaleStep = this._normalizePositiveNumber(options.scaleStep, 0.05);
388
+ options.rotationStep = this._normalizeFiniteNumber(options.rotationStep, 90);
389
+ options.downsampleMaxWidth = this._normalizePositiveNumber(options.downsampleMaxWidth, 4e3);
390
+ options.downsampleMaxHeight = this._normalizePositiveNumber(options.downsampleMaxHeight, 3e3);
391
+ options.downsampleQuality = options.downsampleQuality == null ? 0.92 : Math.max(0, Math.min(1, this._normalizeFiniteNumber(options.downsampleQuality, 0.92)));
392
+ options.imageLoadTimeoutMs = this._normalizePositiveNumber(options.imageLoadTimeoutMs, 3e4);
393
+ options.exportMultiplier = this._normalizePositiveNumber(options.exportMultiplier, 1);
394
+ options.maxExportPixels = this._normalizePositiveInteger(options.maxExportPixels, 5e7);
395
+ options.maxHistorySize = this._normalizePositiveInteger(options.maxHistorySize, 50);
396
+ options.defaultMaskWidth = this._normalizePositiveNumber(options.defaultMaskWidth, 50);
397
+ options.defaultMaskHeight = this._normalizePositiveNumber(options.defaultMaskHeight, 80);
398
+ options.maskLabelOffset = this._normalizeNonNegativeNumber(options.maskLabelOffset, 3);
399
+ if (options.crop) {
400
+ options.crop.minWidth = this._normalizePositiveNumber(options.crop.minWidth, 100);
401
+ options.crop.minHeight = this._normalizePositiveNumber(options.crop.minHeight, 100);
402
+ options.crop.padding = this._normalizeNonNegativeNumber(options.crop.padding, 10);
403
+ }
404
+ }
351
405
  _reportError(message, error = null) {
352
406
  const handler = this.options && this.options.onError;
353
407
  if (typeof handler !== "function") return;
@@ -364,10 +418,18 @@
364
418
  } catch {
365
419
  }
366
420
  }
421
+ _emitSafeCallback(callback, message) {
422
+ if (typeof callback !== "function") return;
423
+ try {
424
+ callback();
425
+ } catch (error) {
426
+ this._reportWarning(message, error);
427
+ }
428
+ }
367
429
  _notifyImageLoaded() {
368
430
  const optionsCallback = this.options && this.options.onImageLoaded;
369
431
  const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
370
- if (typeof callback === "function") callback();
432
+ this._emitSafeCallback(callback, "onImageLoaded callback failed");
371
433
  }
372
434
  /**
373
435
  * Initializes the Fabric canvas, viewport elements, and selection event handlers.
@@ -606,7 +668,6 @@
606
668
  _loadImageFile(file) {
607
669
  if (!this._isSupportedImageFile(file)) {
608
670
  const error = new Error("Selected file is not a supported image");
609
- this._reportError("Selected file is not a supported image", error);
610
671
  return Promise.reject(error);
611
672
  }
612
673
  return new Promise((resolve, reject) => {
@@ -669,6 +730,7 @@
669
730
  const isNestedOperation = this._isOwnInternalOperation(options);
670
731
  const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
671
732
  let transaction = null;
733
+ let shouldNotifyImageLoaded;
672
734
  try {
673
735
  this._isLoading = true;
674
736
  this._updateUI();
@@ -757,7 +819,7 @@
757
819
  this._updateUI();
758
820
  this.canvas.renderAll();
759
821
  this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
760
- this._notifyImageLoaded();
822
+ shouldNotifyImageLoaded = true;
761
823
  } catch (error) {
762
824
  await this._rollbackLoadImageTransaction(
763
825
  transaction,
@@ -769,6 +831,9 @@
769
831
  if (!isNestedOperation) this._endBusyOperation(operationToken);
770
832
  if (!this._disposed && this.canvas) this._updateUI();
771
833
  }
834
+ if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
835
+ this._notifyImageLoaded();
836
+ }
772
837
  }
773
838
  /**
774
839
  * Checks whether there is a loaded image on the current canvas.
@@ -785,7 +850,7 @@
785
850
  * @public
786
851
  */
787
852
  isBusy() {
788
- return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
853
+ return !!(this.isAnimating || this._cropMode || this._isApplyingCrop || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
789
854
  }
790
855
  /**
791
856
  * Creates an HTMLImageElement from a given data URL.
@@ -1582,7 +1647,23 @@
1582
1647
  _getJpegBackgroundColor() {
1583
1648
  const backgroundColor = String(this.options.backgroundColor || "").trim();
1584
1649
  if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
1585
- return backgroundColor;
1650
+ return this._isValidCanvasFillStyle(backgroundColor) ? backgroundColor : "#ffffff";
1651
+ }
1652
+ _isValidCanvasFillStyle(color) {
1653
+ try {
1654
+ if (typeof document === "undefined" || !document.createElement) return false;
1655
+ const validationCanvas = document.createElement("canvas");
1656
+ const context = validationCanvas.getContext && validationCanvas.getContext("2d");
1657
+ if (!context) return false;
1658
+ context.fillStyle = "#010203";
1659
+ context.fillStyle = color;
1660
+ if (context.fillStyle !== "#010203") return true;
1661
+ context.fillStyle = "#040506";
1662
+ context.fillStyle = color;
1663
+ return context.fillStyle !== "#040506";
1664
+ } catch {
1665
+ return false;
1666
+ }
1586
1667
  }
1587
1668
  _isTransparentCssColor(color) {
1588
1669
  const normalizedColor = String(color || "").trim().toLowerCase();
@@ -1992,10 +2073,12 @@
1992
2073
  async _scaleImageImpl(factor, options = {}) {
1993
2074
  if (!this.originalImage || this._disposed) return;
1994
2075
  if (this.isAnimating) return;
2076
+ const numericFactor = Number(factor);
2077
+ if (!Number.isFinite(numericFactor)) return;
1995
2078
  const saveHistory = options.saveHistory !== false;
1996
2079
  let didStartAnimation = false;
1997
2080
  try {
1998
- factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
2081
+ factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
1999
2082
  this.currentScale = factor;
2000
2083
  this.isAnimating = true;
2001
2084
  didStartAnimation = true;
@@ -2061,7 +2144,8 @@
2061
2144
  async _rotateImageImpl(degrees, options = {}) {
2062
2145
  if (!this.originalImage || this._disposed) return;
2063
2146
  if (this.isAnimating) return;
2064
- if (isNaN(degrees)) return;
2147
+ const numericDegrees = Number(degrees);
2148
+ if (!Number.isFinite(numericDegrees)) return;
2065
2149
  const saveHistory = options.saveHistory !== false;
2066
2150
  const image = this.originalImage;
2067
2151
  const previousOriginX = image.originX || "left";
@@ -2070,6 +2154,7 @@
2070
2154
  let didStartAnimation = false;
2071
2155
  let didCompleteRotation = false;
2072
2156
  try {
2157
+ degrees = numericDegrees;
2073
2158
  this.currentRotation = degrees;
2074
2159
  this.isAnimating = true;
2075
2160
  didStartAnimation = true;
@@ -2573,7 +2658,11 @@
2573
2658
  maskConfig.top = top;
2574
2659
  let mask;
2575
2660
  if (typeof maskConfig.fabricGenerator === "function") {
2576
- mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
2661
+ try {
2662
+ mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
2663
+ } catch (error) {
2664
+ return rejectInvalidMask("fabricGenerator failed", error);
2665
+ }
2577
2666
  } else {
2578
2667
  switch (shapeType) {
2579
2668
  case "circle":
@@ -2627,6 +2716,17 @@
2627
2716
  } catch (error) {
2628
2717
  return rejectInvalidMask("invalid polygon points", error);
2629
2718
  }
2719
+ const uniquePointKeys = new Set(polygonPoints.map((point) => `${point.x}:${point.y}`));
2720
+ if (uniquePointKeys.size !== polygonPoints.length) {
2721
+ return rejectInvalidMask("polygon points must not contain duplicates");
2722
+ }
2723
+ const doubleArea = polygonPoints.reduce((area, point, index) => {
2724
+ const nextPoint = polygonPoints[(index + 1) % polygonPoints.length];
2725
+ return area + point.x * nextPoint.y - nextPoint.x * point.y;
2726
+ }, 0);
2727
+ if (Math.abs(doubleArea) < 1e-6) {
2728
+ return rejectInvalidMask("polygon masks must have a non-zero area");
2729
+ }
2630
2730
  mask = new fabric.Polygon(polygonPoints, {
2631
2731
  left,
2632
2732
  top,
@@ -2705,7 +2805,12 @@
2705
2805
  this._updateUI();
2706
2806
  this.canvas.renderAll();
2707
2807
  this.saveState();
2708
- if (typeof maskConfig.onCreate === "function") maskConfig.onCreate(mask, this.canvas);
2808
+ if (typeof maskConfig.onCreate === "function") {
2809
+ this._emitSafeCallback(
2810
+ () => maskConfig.onCreate(mask, this.canvas),
2811
+ "createMask onCreate callback failed"
2812
+ );
2813
+ }
2709
2814
  return mask;
2710
2815
  }
2711
2816
  /**
@@ -2909,8 +3014,15 @@
2909
3014
  this._removeLabelForMask(mask);
2910
3015
  let textObject = null;
2911
3016
  if (this.options.label && typeof this.options.label.create === "function") {
2912
- textObject = this.options.label.create(mask, fabric);
2913
- if (!textObject || typeof textObject.set !== "function") {
3017
+ let didLabelCreateThrow = false;
3018
+ try {
3019
+ textObject = this.options.label.create(mask, fabric);
3020
+ } catch (error) {
3021
+ didLabelCreateThrow = true;
3022
+ this._reportWarning("label.create() failed; using the default label", error);
3023
+ textObject = null;
3024
+ }
3025
+ if (!didLabelCreateThrow && (!textObject || typeof textObject.set !== "function")) {
2914
3026
  this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
2915
3027
  textObject = null;
2916
3028
  }
@@ -2931,7 +3043,12 @@
2931
3043
  };
2932
3044
  if (this.options.label) {
2933
3045
  if (typeof this.options.label.getText === "function") {
2934
- labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3046
+ try {
3047
+ labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
3048
+ } catch (error) {
3049
+ this._reportWarning("label.getText() failed; using the mask name", error);
3050
+ labelText = mask.maskName;
3051
+ }
2935
3052
  }
2936
3053
  if (this.options.label.textOptions) {
2937
3054
  Object.assign(textOptions, this.options.label.textOptions);
@@ -3443,6 +3560,38 @@
3443
3560
  height
3444
3561
  };
3445
3562
  }
3563
+ _getCropRectRawBounds(cropRect) {
3564
+ if (!cropRect) return { left: NaN, top: NaN, width: NaN, height: NaN };
3565
+ return {
3566
+ left: Number(cropRect.left),
3567
+ top: Number(cropRect.top),
3568
+ width: Number(cropRect.width) * Math.abs(Number(cropRect.scaleX)),
3569
+ height: Number(cropRect.height) * Math.abs(Number(cropRect.scaleY))
3570
+ };
3571
+ }
3572
+ _isValidCropRegion(cropBounds, imageBounds) {
3573
+ if (!cropBounds || !imageBounds) return false;
3574
+ const left = Number(cropBounds.left);
3575
+ const top = Number(cropBounds.top);
3576
+ const width = Number(cropBounds.width);
3577
+ const height = Number(cropBounds.height);
3578
+ const imageLeft = Number(imageBounds.left);
3579
+ const imageTop = Number(imageBounds.top);
3580
+ const imageWidth = Number(imageBounds.width);
3581
+ const imageHeight = Number(imageBounds.height);
3582
+ if (![left, top, width, height, imageLeft, imageTop, imageWidth, imageHeight].every(Number.isFinite)) return false;
3583
+ if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return false;
3584
+ const right = left + width;
3585
+ const bottom = top + height;
3586
+ const imageRight = imageLeft + imageWidth;
3587
+ const imageBottom = imageTop + imageHeight;
3588
+ const overlapsImage = left < imageRight && right > imageLeft && top < imageBottom && bottom > imageTop;
3589
+ if (!overlapsImage) return false;
3590
+ const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
3591
+ const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
3592
+ if (!Number.isFinite(canvasWidth) || !Number.isFinite(canvasHeight) || canvasWidth <= 0 || canvasHeight <= 0) return false;
3593
+ return left < canvasWidth && right > 0 && top < canvasHeight && bottom > 0;
3594
+ }
3446
3595
  /**
3447
3596
  * Enters crop mode by creating a resizable crop rectangle above the base image.
3448
3597
  *
@@ -3454,6 +3603,10 @@
3454
3603
  */
3455
3604
  enterCropMode() {
3456
3605
  if (!this.canvas || !this.originalImage || this._cropMode) return;
3606
+ if (this._isApplyingCrop) {
3607
+ this._reportWarning("enterCropMode ignored because a crop is already being applied");
3608
+ return;
3609
+ }
3457
3610
  if (!this._canMutateNow("enterCropMode")) return;
3458
3611
  if (!this.isImageLoaded()) return;
3459
3612
  this._removeCropRect();
@@ -3578,6 +3731,10 @@
3578
3731
  * @public
3579
3732
  */
3580
3733
  cancelCrop() {
3734
+ if (this._isApplyingCrop) {
3735
+ this._reportWarning("cancelCrop ignored because a crop is already being applied");
3736
+ return;
3737
+ }
3581
3738
  if (!this.canvas || !this._cropMode) return;
3582
3739
  this._removeCropRect();
3583
3740
  this._restoreCropObjectState();
@@ -3601,11 +3758,23 @@
3601
3758
  */
3602
3759
  async applyCrop() {
3603
3760
  if (!this.canvas || !this._cropMode || !this._cropRect) return;
3761
+ if (this._isApplyingCrop) {
3762
+ this._reportWarning("applyCrop ignored because a crop is already being applied");
3763
+ return;
3764
+ }
3604
3765
  this._assertIdleForOperation("applyCrop");
3766
+ this._isApplyingCrop = true;
3605
3767
  const operationToken = this._beginBusyOperation("applyCrop");
3606
3768
  const internalOptions = this._withInternalOperationOptions(operationToken);
3607
3769
  try {
3608
3770
  this._cropRect.setCoords();
3771
+ this.originalImage.setCoords();
3772
+ const imageBounds = this.originalImage.getBoundingRect(true, true);
3773
+ const rawCropBounds = this._getCropRectRawBounds(this._cropRect);
3774
+ if (!this._isValidCropRegion(rawCropBounds, imageBounds)) {
3775
+ this._reportWarning("applyCrop: crop region is invalid");
3776
+ return;
3777
+ }
3609
3778
  const rectBounds = this._getCropRectContentBounds(this._cropRect);
3610
3779
  const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
3611
3780
  const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
@@ -3618,7 +3787,13 @@
3618
3787
  beforeJson = null;
3619
3788
  }
3620
3789
  if (!beforeJson) {
3621
- this.cancelCrop();
3790
+ this._removeCropRect();
3791
+ this._cropMode = false;
3792
+ this.canvas.selection = !!this._prevSelectionSetting;
3793
+ this._prevSelectionSetting = void 0;
3794
+ this.canvas.discardActiveObject();
3795
+ this._updateUI();
3796
+ this.canvas.renderAll();
3622
3797
  return;
3623
3798
  }
3624
3799
  const preservedMasks = [];
@@ -3694,6 +3869,7 @@
3694
3869
  this._updateUI();
3695
3870
  this.canvas.renderAll();
3696
3871
  } finally {
3872
+ this._isApplyingCrop = false;
3697
3873
  this._endBusyOperation(operationToken);
3698
3874
  }
3699
3875
  }
@@ -3725,9 +3901,11 @@
3725
3901
  const isInCropMode = !!this._cropMode;
3726
3902
  const isBusy = this.isBusy();
3727
3903
  if (isInCropMode) {
3904
+ const cropInteractionKeys = /* @__PURE__ */ new Set(["canvas", "canvasContainer", "imagePlaceholder", "imgPlaceholder"]);
3728
3905
  for (const key of Object.keys(this.elements || {})) {
3729
3906
  const element = this._getElement(key);
3730
3907
  if (!element) continue;
3908
+ if (cropInteractionKeys.has(key)) continue;
3731
3909
  if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
3732
3910
  this._setDisabled(key, false);
3733
3911
  } else {
@@ -3765,9 +3943,44 @@
3765
3943
  * @param {boolean} disabled - If true, disables the element; otherwise enables.
3766
3944
  * @private
3767
3945
  */
3946
+ _rememberElementDisabledState(key, element) {
3947
+ if (!element) return;
3948
+ if (!this._elementOriginalDisabledState) this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
3949
+ if (this._elementOriginalDisabledState.has(key)) return;
3950
+ this._elementOriginalDisabledState.set(key, {
3951
+ element,
3952
+ hasDisabledProperty: "disabled" in element,
3953
+ disabled: "disabled" in element ? !!element.disabled : void 0,
3954
+ ariaDisabled: element.getAttribute ? element.getAttribute("aria-disabled") : null,
3955
+ pointerEvents: element.style ? element.style.pointerEvents || "" : ""
3956
+ });
3957
+ }
3958
+ _restoreElementDisabledStates() {
3959
+ if (!this._elementOriginalDisabledState) return;
3960
+ for (const state of this._elementOriginalDisabledState.values()) {
3961
+ const element = state && state.element;
3962
+ if (!element) continue;
3963
+ try {
3964
+ if (state.hasDisabledProperty && "disabled" in element) {
3965
+ element.disabled = !!state.disabled;
3966
+ }
3967
+ if (element.getAttribute && element.setAttribute && element.removeAttribute) {
3968
+ if (state.ariaDisabled === null) {
3969
+ element.removeAttribute("aria-disabled");
3970
+ } else {
3971
+ element.setAttribute("aria-disabled", state.ariaDisabled);
3972
+ }
3973
+ }
3974
+ if (element.style) element.style.pointerEvents = state.pointerEvents || "";
3975
+ } catch (error) {
3976
+ void error;
3977
+ }
3978
+ }
3979
+ }
3768
3980
  _setDisabled(key, disabled) {
3769
3981
  const element = this._getElement(key);
3770
3982
  if (!element) return;
3983
+ this._rememberElementDisabledState(key, element);
3771
3984
  if ("disabled" in element) {
3772
3985
  element.disabled = !!disabled;
3773
3986
  return;
@@ -3794,7 +4007,6 @@
3794
4007
  * @private
3795
4008
  */
3796
4009
  _updatePlaceholderStatus() {
3797
- if (!this.options.showPlaceholder) return;
3798
4010
  this._setPlaceholderVisible(!this.originalImage);
3799
4011
  }
3800
4012
  /**
@@ -3804,10 +4016,11 @@
3804
4016
  * @private
3805
4017
  */
3806
4018
  _setPlaceholderVisible(show) {
3807
- if (this.placeholderElement) this._setElementVisible(this.placeholderElement, show);
4019
+ const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
4020
+ if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
3808
4021
  const canvasVisibilityElement = this._getCanvasVisibilityElement();
3809
4022
  if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
3810
- this._setElementVisible(canvasVisibilityElement, !show);
4023
+ this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
3811
4024
  }
3812
4025
  }
3813
4026
  _getCanvasVisibilityElement() {
@@ -3886,6 +4099,12 @@
3886
4099
  void error;
3887
4100
  }
3888
4101
  if (this._cropRect) this._removeCropRect();
4102
+ this._isApplyingCrop = false;
4103
+ try {
4104
+ this._restoreElementDisabledStates();
4105
+ } catch (error) {
4106
+ void error;
4107
+ }
3889
4108
  if (this.containerElement && this._containerOriginalOverflow) {
3890
4109
  try {
3891
4110
  this._restoreContainerOverflowState();
@@ -3933,6 +4152,7 @@
3933
4152
  this._handlersByElementKey = {};
3934
4153
  this._elementCache = {};
3935
4154
  this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
4155
+ this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
3936
4156
  this._clearMaskPlacementMemory();
3937
4157
  this.originalImage = null;
3938
4158
  this.baseImageScale = 1;
@@ -3941,6 +4161,7 @@
3941
4161
  this.isAnimating = false;
3942
4162
  this._isLoading = false;
3943
4163
  this._cropMode = false;
4164
+ this._isApplyingCrop = false;
3944
4165
  this._cropRect = null;
3945
4166
  this._cropHandlers = [];
3946
4167
  this._cropPrevEvented = null;
@@ -4043,9 +4264,10 @@
4043
4264
  * @param {number} [maxSize=50] - Maximum number of commands to keep in history.
4044
4265
  */
4045
4266
  constructor(maxSize = 50) {
4267
+ const numericMaxSize = Number(maxSize);
4046
4268
  this.history = [];
4047
4269
  this.currentIndex = -1;
4048
- this.maxSize = maxSize;
4270
+ this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
4049
4271
  this.pending = Promise.resolve();
4050
4272
  }
4051
4273
  /**