@bensitu/image-editor 1.5.0 → 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.
- package/README.md +137 -47
- package/dist/image-editor.cjs +4407 -0
- package/dist/image-editor.cjs.map +7 -0
- package/dist/image-editor.esm.js +812 -273
- package/dist/image-editor.esm.js.map +3 -3
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -3
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +812 -273
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +812 -273
- package/dist/image-editor.js.map +3 -3
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +6 -3
- package/package.json +4 -3
- package/src/image-editor.js +759 -165
|
@@ -5,7 +5,7 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.5.
|
|
8
|
+
* @version 1.5.2
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -77,6 +77,8 @@ var ImageEditor = class {
|
|
|
77
77
|
downsampleMimeType: null,
|
|
78
78
|
imageLoadTimeoutMs: 3e4,
|
|
79
79
|
exportMultiplier: 1,
|
|
80
|
+
maxExportPixels: 5e7,
|
|
81
|
+
maxHistorySize: 50,
|
|
80
82
|
exportImageAreaByDefault: true,
|
|
81
83
|
defaultMaskWidth: 50,
|
|
82
84
|
defaultMaskHeight: 80,
|
|
@@ -105,6 +107,7 @@ var ImageEditor = class {
|
|
|
105
107
|
...userCrop
|
|
106
108
|
}
|
|
107
109
|
};
|
|
110
|
+
this._normalizeOptions();
|
|
108
111
|
this._fabricLoaded = !!ensureFabric();
|
|
109
112
|
if (!this._fabricLoaded) {
|
|
110
113
|
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
@@ -124,16 +127,18 @@ var ImageEditor = class {
|
|
|
124
127
|
this._activeOperationToken = null;
|
|
125
128
|
this.elements = {};
|
|
126
129
|
this.isImageLoadedToCanvas = false;
|
|
127
|
-
this.maxHistorySize =
|
|
130
|
+
this.maxHistorySize = this.options.maxHistorySize;
|
|
128
131
|
this._handlersByElementKey = {};
|
|
129
132
|
this._elementCache = {};
|
|
130
133
|
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
134
|
+
this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
131
135
|
this._lastMask = null;
|
|
132
136
|
this._lastMaskInitialLeft = null;
|
|
133
137
|
this._lastMaskInitialTop = null;
|
|
134
138
|
this._lastMaskInitialWidth = null;
|
|
135
139
|
this._lastSnapshot = null;
|
|
136
140
|
this._cropMode = false;
|
|
141
|
+
this._isApplyingCrop = false;
|
|
137
142
|
this._cropRect = null;
|
|
138
143
|
this._cropHandlers = [];
|
|
139
144
|
this._cropPrevEvented = null;
|
|
@@ -146,6 +151,8 @@ var ImageEditor = class {
|
|
|
146
151
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
147
152
|
this._disposed = false;
|
|
148
153
|
this._initialized = false;
|
|
154
|
+
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
155
|
+
this._cropRotationWarningEmitted = false;
|
|
149
156
|
this.onImageLoaded = typeof this.options.onImageLoaded === "function" ? this.options.onImageLoaded : null;
|
|
150
157
|
this.animationQueue = new AnimationQueue();
|
|
151
158
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -210,7 +217,13 @@ var ImageEditor = class {
|
|
|
210
217
|
* });
|
|
211
218
|
*/
|
|
212
219
|
init(idMap = {}) {
|
|
213
|
-
if (!this._fabricLoaded)
|
|
220
|
+
if (!this._fabricLoaded) {
|
|
221
|
+
this._fabricLoaded = !!ensureFabric();
|
|
222
|
+
if (!this._fabricLoaded) {
|
|
223
|
+
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
214
227
|
if (this._initialized || this.canvas) this.dispose();
|
|
215
228
|
this._disposed = false;
|
|
216
229
|
this._initialized = true;
|
|
@@ -222,10 +235,11 @@ var ImageEditor = class {
|
|
|
222
235
|
this._activeOperationName = null;
|
|
223
236
|
this._activeOperationToken = null;
|
|
224
237
|
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
238
|
+
this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
239
|
+
this._isApplyingCrop = false;
|
|
225
240
|
this._containerOriginalOverflow = null;
|
|
226
241
|
this._lastContainerViewportSize = null;
|
|
227
242
|
this._canvasElementOriginalStyle = null;
|
|
228
|
-
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
229
243
|
const defaults = {
|
|
230
244
|
canvas: "fabricCanvas",
|
|
231
245
|
canvasContainer: null,
|
|
@@ -264,6 +278,7 @@ var ImageEditor = class {
|
|
|
264
278
|
redoButton: "redoButton",
|
|
265
279
|
redoBtn: null,
|
|
266
280
|
imageInput: "imageInput",
|
|
281
|
+
uploadArea: null,
|
|
267
282
|
enterCropModeButton: "enterCropModeButton",
|
|
268
283
|
cropBtn: null,
|
|
269
284
|
applyCropButton: "applyCropButton",
|
|
@@ -279,7 +294,7 @@ var ImageEditor = class {
|
|
|
279
294
|
this._updateMaskList();
|
|
280
295
|
this._updateUI();
|
|
281
296
|
if (this.options.initialImageBase64) {
|
|
282
|
-
this.loadImage(this.options.initialImageBase64);
|
|
297
|
+
this.loadImage(this.options.initialImageBase64).catch((error) => this._reportError("initialImageBase64 could not be loaded", error));
|
|
283
298
|
} else {
|
|
284
299
|
this._updatePlaceholderStatus();
|
|
285
300
|
}
|
|
@@ -341,6 +356,54 @@ var ImageEditor = class {
|
|
|
341
356
|
`ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
|
|
342
357
|
);
|
|
343
358
|
}
|
|
359
|
+
_normalizeFiniteNumber(value, fallback) {
|
|
360
|
+
const numericValue = Number(value);
|
|
361
|
+
return Number.isFinite(numericValue) ? numericValue : fallback;
|
|
362
|
+
}
|
|
363
|
+
_normalizePositiveNumber(value, fallback) {
|
|
364
|
+
const numericValue = this._normalizeFiniteNumber(value, fallback);
|
|
365
|
+
return numericValue > 0 ? numericValue : fallback;
|
|
366
|
+
}
|
|
367
|
+
_normalizeNonNegativeNumber(value, fallback) {
|
|
368
|
+
const numericValue = this._normalizeFiniteNumber(value, fallback);
|
|
369
|
+
return numericValue >= 0 ? numericValue : fallback;
|
|
370
|
+
}
|
|
371
|
+
_normalizePositiveInteger(value, fallback) {
|
|
372
|
+
const numericValue = this._normalizePositiveNumber(value, fallback);
|
|
373
|
+
return Math.max(1, Math.floor(numericValue));
|
|
374
|
+
}
|
|
375
|
+
_normalizeOptions() {
|
|
376
|
+
const options = this.options || {};
|
|
377
|
+
options.canvasWidth = this._normalizePositiveNumber(options.canvasWidth, 800);
|
|
378
|
+
options.canvasHeight = this._normalizePositiveNumber(options.canvasHeight, 600);
|
|
379
|
+
options.animationDuration = this._normalizeNonNegativeNumber(options.animationDuration, 300);
|
|
380
|
+
const minScale = this._normalizePositiveNumber(options.minScale, 0.1);
|
|
381
|
+
const maxScale = this._normalizePositiveNumber(options.maxScale, 5);
|
|
382
|
+
if (minScale > maxScale) {
|
|
383
|
+
options.minScale = 0.1;
|
|
384
|
+
options.maxScale = 5;
|
|
385
|
+
} else {
|
|
386
|
+
options.minScale = minScale;
|
|
387
|
+
options.maxScale = maxScale;
|
|
388
|
+
}
|
|
389
|
+
options.scaleStep = this._normalizePositiveNumber(options.scaleStep, 0.05);
|
|
390
|
+
options.rotationStep = this._normalizeFiniteNumber(options.rotationStep, 90);
|
|
391
|
+
options.downsampleMaxWidth = this._normalizePositiveNumber(options.downsampleMaxWidth, 4e3);
|
|
392
|
+
options.downsampleMaxHeight = this._normalizePositiveNumber(options.downsampleMaxHeight, 3e3);
|
|
393
|
+
options.downsampleQuality = options.downsampleQuality == null ? 0.92 : Math.max(0, Math.min(1, this._normalizeFiniteNumber(options.downsampleQuality, 0.92)));
|
|
394
|
+
options.imageLoadTimeoutMs = this._normalizePositiveNumber(options.imageLoadTimeoutMs, 3e4);
|
|
395
|
+
options.exportMultiplier = this._normalizePositiveNumber(options.exportMultiplier, 1);
|
|
396
|
+
options.maxExportPixels = this._normalizePositiveInteger(options.maxExportPixels, 5e7);
|
|
397
|
+
options.maxHistorySize = this._normalizePositiveInteger(options.maxHistorySize, 50);
|
|
398
|
+
options.defaultMaskWidth = this._normalizePositiveNumber(options.defaultMaskWidth, 50);
|
|
399
|
+
options.defaultMaskHeight = this._normalizePositiveNumber(options.defaultMaskHeight, 80);
|
|
400
|
+
options.maskLabelOffset = this._normalizeNonNegativeNumber(options.maskLabelOffset, 3);
|
|
401
|
+
if (options.crop) {
|
|
402
|
+
options.crop.minWidth = this._normalizePositiveNumber(options.crop.minWidth, 100);
|
|
403
|
+
options.crop.minHeight = this._normalizePositiveNumber(options.crop.minHeight, 100);
|
|
404
|
+
options.crop.padding = this._normalizeNonNegativeNumber(options.crop.padding, 10);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
344
407
|
_reportError(message, error = null) {
|
|
345
408
|
const handler = this.options && this.options.onError;
|
|
346
409
|
if (typeof handler !== "function") return;
|
|
@@ -357,10 +420,18 @@ var ImageEditor = class {
|
|
|
357
420
|
} catch {
|
|
358
421
|
}
|
|
359
422
|
}
|
|
423
|
+
_emitSafeCallback(callback, message) {
|
|
424
|
+
if (typeof callback !== "function") return;
|
|
425
|
+
try {
|
|
426
|
+
callback();
|
|
427
|
+
} catch (error) {
|
|
428
|
+
this._reportWarning(message, error);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
360
431
|
_notifyImageLoaded() {
|
|
361
432
|
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
362
433
|
const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
|
|
363
|
-
|
|
434
|
+
this._emitSafeCallback(callback, "onImageLoaded callback failed");
|
|
364
435
|
}
|
|
365
436
|
/**
|
|
366
437
|
* Initializes the Fabric canvas, viewport elements, and selection event handlers.
|
|
@@ -480,13 +551,14 @@ var ImageEditor = class {
|
|
|
480
551
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
481
552
|
this._captureContainerOverflowState();
|
|
482
553
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
483
|
-
|
|
554
|
+
const layoutMode = this._getImageLayoutMode();
|
|
555
|
+
if (layoutMode === "cover") {
|
|
484
556
|
this.containerElement.style.overflow = "scroll";
|
|
485
557
|
if (!shouldPreserveScroll) {
|
|
486
558
|
this.containerElement.scrollLeft = 0;
|
|
487
559
|
this.containerElement.scrollTop = 0;
|
|
488
560
|
}
|
|
489
|
-
} else if (
|
|
561
|
+
} else if (layoutMode === "fit") {
|
|
490
562
|
this.containerElement.style.overflow = "auto";
|
|
491
563
|
if (!shouldPreserveScroll) {
|
|
492
564
|
this.containerElement.scrollLeft = 0;
|
|
@@ -598,7 +670,6 @@ var ImageEditor = class {
|
|
|
598
670
|
_loadImageFile(file) {
|
|
599
671
|
if (!this._isSupportedImageFile(file)) {
|
|
600
672
|
const error = new Error("Selected file is not a supported image");
|
|
601
|
-
this._reportError("Selected file is not a supported image", error);
|
|
602
673
|
return Promise.reject(error);
|
|
603
674
|
}
|
|
604
675
|
return new Promise((resolve, reject) => {
|
|
@@ -637,6 +708,12 @@ var ImageEditor = class {
|
|
|
637
708
|
`Only one image layout mode should be enabled. Active modes: ${activeModes.join(", ")}.`
|
|
638
709
|
);
|
|
639
710
|
}
|
|
711
|
+
_getImageLayoutMode() {
|
|
712
|
+
if (this.options.fitImageToCanvas) return "fit";
|
|
713
|
+
if (this.options.coverImageToCanvas) return "cover";
|
|
714
|
+
if (this.options.expandCanvasToImage) return "expand";
|
|
715
|
+
return "contain";
|
|
716
|
+
}
|
|
640
717
|
/**
|
|
641
718
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
642
719
|
*
|
|
@@ -650,12 +727,17 @@ var ImageEditor = class {
|
|
|
650
727
|
if (!this._fabricLoaded) return;
|
|
651
728
|
if (!this.canvas || this._disposed) return;
|
|
652
729
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
730
|
+
options = options || {};
|
|
653
731
|
this._assertIdleForOperation("loadImage", options);
|
|
654
|
-
|
|
655
|
-
this.
|
|
656
|
-
|
|
657
|
-
|
|
732
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
733
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
|
|
734
|
+
let transaction = null;
|
|
735
|
+
let shouldNotifyImageLoaded;
|
|
658
736
|
try {
|
|
737
|
+
this._isLoading = true;
|
|
738
|
+
this._updateUI();
|
|
739
|
+
this._warnOnImageLayoutOptionConflict();
|
|
740
|
+
transaction = this._captureLoadImageTransaction();
|
|
659
741
|
const imageElement = await this._createImageElement(imageBase64);
|
|
660
742
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
661
743
|
let loadSource = imageBase64;
|
|
@@ -695,7 +777,8 @@ var ImageEditor = class {
|
|
|
695
777
|
const viewport = this._getContainerViewportSize();
|
|
696
778
|
const minWidth = viewport.width;
|
|
697
779
|
const minHeight = viewport.height;
|
|
698
|
-
|
|
780
|
+
const layoutMode = this._getImageLayoutMode();
|
|
781
|
+
if (layoutMode === "fit") {
|
|
699
782
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
700
783
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
701
784
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -703,13 +786,13 @@ var ImageEditor = class {
|
|
|
703
786
|
fabricImage.set({ left: 0, top: 0 });
|
|
704
787
|
fabricImage.scale(fitScale);
|
|
705
788
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
706
|
-
} else if (
|
|
789
|
+
} else if (layoutMode === "cover") {
|
|
707
790
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
708
791
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
709
792
|
fabricImage.set({ left: 0, top: 0 });
|
|
710
793
|
fabricImage.scale(layout.scale);
|
|
711
794
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
712
|
-
} else if (
|
|
795
|
+
} else if (layoutMode === "expand") {
|
|
713
796
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
714
797
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
715
798
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -738,14 +821,21 @@ var ImageEditor = class {
|
|
|
738
821
|
this._updateUI();
|
|
739
822
|
this.canvas.renderAll();
|
|
740
823
|
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
741
|
-
|
|
824
|
+
shouldNotifyImageLoaded = true;
|
|
742
825
|
} catch (error) {
|
|
743
|
-
await this._rollbackLoadImageTransaction(
|
|
826
|
+
await this._rollbackLoadImageTransaction(
|
|
827
|
+
transaction,
|
|
828
|
+
this._withInternalOperationOptions(operationToken)
|
|
829
|
+
);
|
|
744
830
|
throw error;
|
|
745
831
|
} finally {
|
|
746
832
|
this._isLoading = false;
|
|
833
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
747
834
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
748
835
|
}
|
|
836
|
+
if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
|
|
837
|
+
this._notifyImageLoaded();
|
|
838
|
+
}
|
|
749
839
|
}
|
|
750
840
|
/**
|
|
751
841
|
* Checks whether there is a loaded image on the current canvas.
|
|
@@ -762,7 +852,7 @@ var ImageEditor = class {
|
|
|
762
852
|
* @public
|
|
763
853
|
*/
|
|
764
854
|
isBusy() {
|
|
765
|
-
return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
|
|
855
|
+
return !!(this.isAnimating || this._cropMode || this._isApplyingCrop || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
|
|
766
856
|
}
|
|
767
857
|
/**
|
|
768
858
|
* Creates an HTMLImageElement from a given data URL.
|
|
@@ -856,13 +946,13 @@ var ImageEditor = class {
|
|
|
856
946
|
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
857
947
|
};
|
|
858
948
|
}
|
|
859
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
949
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
860
950
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
861
951
|
let didRestoreCanvasState = false;
|
|
862
952
|
let didFailCanvasRestore = false;
|
|
863
953
|
try {
|
|
864
954
|
if (transaction.canvasState) {
|
|
865
|
-
await this.loadFromState(transaction.canvasState);
|
|
955
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
866
956
|
didRestoreCanvasState = true;
|
|
867
957
|
}
|
|
868
958
|
} catch (error) {
|
|
@@ -1112,9 +1202,9 @@ var ImageEditor = class {
|
|
|
1112
1202
|
}
|
|
1113
1203
|
_getScrollableCanvasSize(contentWidth, contentHeight, viewport = this._getContainerViewportSize()) {
|
|
1114
1204
|
if (this._hasFixedContainerScrollbars()) {
|
|
1115
|
-
const
|
|
1116
|
-
const safeWidth = Math.max(1, viewport.width -
|
|
1117
|
-
const safeHeight = Math.max(1, viewport.height -
|
|
1205
|
+
const safetyMargin2 = this._getScrollSafetyMargin();
|
|
1206
|
+
const safeWidth = Math.max(1, viewport.width - safetyMargin2);
|
|
1207
|
+
const safeHeight = Math.max(1, viewport.height - safetyMargin2);
|
|
1118
1208
|
return {
|
|
1119
1209
|
width: contentWidth > viewport.width + 0.5 ? this._ceilCanvasDimension(contentWidth) : safeWidth,
|
|
1120
1210
|
height: contentHeight > viewport.height + 0.5 ? this._ceilCanvasDimension(contentHeight) : safeHeight,
|
|
@@ -1140,9 +1230,17 @@ var ImageEditor = class {
|
|
|
1140
1230
|
}
|
|
1141
1231
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1142
1232
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1233
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1234
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1235
|
+
const shouldReserveNoScrollbarMargin = layoutMode === "fit" || layoutMode === "cover";
|
|
1236
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1237
|
+
const margin = hasOppositeScrollbar ? safetyMargin : shouldReserveNoScrollbarMargin ? 1 : 0;
|
|
1238
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1239
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1240
|
+
};
|
|
1143
1241
|
return {
|
|
1144
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1145
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1242
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1243
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1146
1244
|
viewportWidth: effectiveWidth,
|
|
1147
1245
|
viewportHeight: effectiveHeight,
|
|
1148
1246
|
hasHorizontal,
|
|
@@ -1264,6 +1362,45 @@ var ImageEditor = class {
|
|
|
1264
1362
|
});
|
|
1265
1363
|
}
|
|
1266
1364
|
}
|
|
1365
|
+
_getSerializableStateObjects() {
|
|
1366
|
+
if (!this.canvas) return [];
|
|
1367
|
+
return this.canvas.getObjects().filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1368
|
+
}
|
|
1369
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1370
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1371
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1372
|
+
const numericProperties = [
|
|
1373
|
+
"left",
|
|
1374
|
+
"top",
|
|
1375
|
+
"width",
|
|
1376
|
+
"height",
|
|
1377
|
+
"scaleX",
|
|
1378
|
+
"scaleY",
|
|
1379
|
+
"angle",
|
|
1380
|
+
"skewX",
|
|
1381
|
+
"skewY",
|
|
1382
|
+
"cropX",
|
|
1383
|
+
"cropY",
|
|
1384
|
+
"radius",
|
|
1385
|
+
"rx",
|
|
1386
|
+
"ry",
|
|
1387
|
+
"strokeWidth"
|
|
1388
|
+
];
|
|
1389
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1390
|
+
const fabricObject = fabricObjects[index];
|
|
1391
|
+
if (!serializedObject || !fabricObject) return;
|
|
1392
|
+
numericProperties.forEach((property) => {
|
|
1393
|
+
const numericValue = Number(fabricObject[property]);
|
|
1394
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1395
|
+
});
|
|
1396
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1397
|
+
serializedObject.points = fabricObject.points.map((point) => ({
|
|
1398
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1399
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1400
|
+
}));
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1267
1404
|
_restoreMaskControls(mask) {
|
|
1268
1405
|
if (!mask) return;
|
|
1269
1406
|
const cornerSize = Number(mask.cornerSize);
|
|
@@ -1309,6 +1446,7 @@ var ImageEditor = class {
|
|
|
1309
1446
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1310
1447
|
if (Array.isArray(jsonObject.objects)) {
|
|
1311
1448
|
jsonObject.objects = jsonObject.objects.filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1449
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1312
1450
|
}
|
|
1313
1451
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1314
1452
|
return JSON.stringify(jsonObject);
|
|
@@ -1383,6 +1521,12 @@ var ImageEditor = class {
|
|
|
1383
1521
|
if (!Number.isFinite(numericValue)) return false;
|
|
1384
1522
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1385
1523
|
}
|
|
1524
|
+
_hasScaledImageEdge(axis) {
|
|
1525
|
+
if (!this.originalImage) return false;
|
|
1526
|
+
const scale = Number(axis === "y" ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1527
|
+
if (!Number.isFinite(scale)) return false;
|
|
1528
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1529
|
+
}
|
|
1386
1530
|
_getPartialExportEdges(bounds) {
|
|
1387
1531
|
if (!bounds) return null;
|
|
1388
1532
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1391,8 +1535,8 @@ var ImageEditor = class {
|
|
|
1391
1535
|
return {
|
|
1392
1536
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1393
1537
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1394
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1395
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1538
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge("x"),
|
|
1539
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge("y")
|
|
1396
1540
|
};
|
|
1397
1541
|
}
|
|
1398
1542
|
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
@@ -1452,7 +1596,8 @@ var ImageEditor = class {
|
|
|
1452
1596
|
* @private
|
|
1453
1597
|
*/
|
|
1454
1598
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1455
|
-
const safeMultiplier =
|
|
1599
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1600
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1456
1601
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1457
1602
|
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1458
1603
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1468,6 +1613,25 @@ var ImageEditor = class {
|
|
|
1468
1613
|
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1469
1614
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1470
1615
|
}
|
|
1616
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1617
|
+
const numericMultiplier = Number(multiplier);
|
|
1618
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1619
|
+
throw new Error("Export multiplier must be a finite positive number");
|
|
1620
|
+
}
|
|
1621
|
+
return Math.max(1, numericMultiplier);
|
|
1622
|
+
}
|
|
1623
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1624
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1625
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1626
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1627
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1628
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1629
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1630
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0 ? Math.floor(configuredMaxPixels) : 5e7;
|
|
1631
|
+
if (outputPixels > maxPixels) {
|
|
1632
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1471
1635
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1472
1636
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1473
1637
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1485,7 +1649,23 @@ var ImageEditor = class {
|
|
|
1485
1649
|
_getJpegBackgroundColor() {
|
|
1486
1650
|
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1487
1651
|
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
|
|
1488
|
-
return backgroundColor;
|
|
1652
|
+
return this._isValidCanvasFillStyle(backgroundColor) ? backgroundColor : "#ffffff";
|
|
1653
|
+
}
|
|
1654
|
+
_isValidCanvasFillStyle(color) {
|
|
1655
|
+
try {
|
|
1656
|
+
if (typeof document === "undefined" || !document.createElement) return false;
|
|
1657
|
+
const validationCanvas = document.createElement("canvas");
|
|
1658
|
+
const context = validationCanvas.getContext && validationCanvas.getContext("2d");
|
|
1659
|
+
if (!context) return false;
|
|
1660
|
+
context.fillStyle = "#010203";
|
|
1661
|
+
context.fillStyle = color;
|
|
1662
|
+
if (context.fillStyle !== "#010203") return true;
|
|
1663
|
+
context.fillStyle = "#040506";
|
|
1664
|
+
context.fillStyle = color;
|
|
1665
|
+
return context.fillStyle !== "#040506";
|
|
1666
|
+
} catch {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1489
1669
|
}
|
|
1490
1670
|
_isTransparentCssColor(color) {
|
|
1491
1671
|
const normalizedColor = String(color || "").trim().toLowerCase();
|
|
@@ -1512,6 +1692,7 @@ var ImageEditor = class {
|
|
|
1512
1692
|
}
|
|
1513
1693
|
_decodeBase64Payload(base64Payload) {
|
|
1514
1694
|
const payload = String(base64Payload || "");
|
|
1695
|
+
if (!payload) throw new Error("Data URL base64 payload is empty");
|
|
1515
1696
|
if (typeof atob === "function") {
|
|
1516
1697
|
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1517
1698
|
}
|
|
@@ -1520,6 +1701,13 @@ var ImageEditor = class {
|
|
|
1520
1701
|
}
|
|
1521
1702
|
throw new Error("Base64 decoding is unavailable");
|
|
1522
1703
|
}
|
|
1704
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
1705
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
1706
|
+
if (!match || !match[2]) {
|
|
1707
|
+
throw new Error("Export produced an invalid or empty base64 data URL");
|
|
1708
|
+
}
|
|
1709
|
+
return this._decodeBase64Payload(match[2]);
|
|
1710
|
+
}
|
|
1523
1711
|
/**
|
|
1524
1712
|
* Gets the top-left corner coordinates of the given object.
|
|
1525
1713
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1629,13 +1817,42 @@ var ImageEditor = class {
|
|
|
1629
1817
|
const currentHeight = this.canvas.getHeight();
|
|
1630
1818
|
let requiredWidth = currentWidth;
|
|
1631
1819
|
let requiredHeight = currentHeight;
|
|
1632
|
-
|
|
1820
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1821
|
+
const usesScrollableFitBounds = layoutMode === "fit" || layoutMode === "cover";
|
|
1822
|
+
let contentWidth = 0;
|
|
1823
|
+
let contentHeight = 0;
|
|
1824
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1633
1825
|
if (!fabricObject) return;
|
|
1634
1826
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
1635
1827
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1636
|
-
|
|
1637
|
-
|
|
1828
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
1829
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
1830
|
+
contentWidth = Math.max(contentWidth, right);
|
|
1831
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
1832
|
+
return { right, bottom };
|
|
1833
|
+
};
|
|
1834
|
+
fabricObjects.forEach((fabricObject) => {
|
|
1835
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
1836
|
+
if (!bounds) return;
|
|
1837
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
1838
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1638
1839
|
});
|
|
1840
|
+
if (usesScrollableFitBounds) {
|
|
1841
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
1842
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1843
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
1844
|
+
});
|
|
1845
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
1846
|
+
Math.max(1, contentWidth),
|
|
1847
|
+
Math.max(1, contentHeight)
|
|
1848
|
+
);
|
|
1849
|
+
const newWidth2 = contentSize.hasHorizontal ? Math.max(currentWidth, contentSize.width) : contentSize.width;
|
|
1850
|
+
const newHeight2 = contentSize.hasVertical ? Math.max(currentHeight, contentSize.height) : contentSize.height;
|
|
1851
|
+
if (newWidth2 !== currentWidth || newHeight2 !== currentHeight) {
|
|
1852
|
+
this._setCanvasSizeInt(newWidth2, newHeight2);
|
|
1853
|
+
}
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1639
1856
|
let minWidth = 0;
|
|
1640
1857
|
let minHeight = 0;
|
|
1641
1858
|
if (this.containerElement) {
|
|
@@ -1653,16 +1870,60 @@ var ImageEditor = class {
|
|
|
1653
1870
|
this._reportWarning("expandCanvasToFitObjects: failed to expand canvas", error);
|
|
1654
1871
|
}
|
|
1655
1872
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1873
|
+
_captureImageDisplayBounds() {
|
|
1874
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
1875
|
+
this.originalImage.setCoords();
|
|
1876
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
1877
|
+
const width = Number(bounds && bounds.width);
|
|
1878
|
+
const height = Number(bounds && bounds.height);
|
|
1879
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
1880
|
+
return {
|
|
1881
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
1882
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
1883
|
+
width,
|
|
1884
|
+
height
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
1888
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
1889
|
+
const imageWidth = Number(this.originalImage.width);
|
|
1890
|
+
const imageHeight = Number(this.originalImage.height);
|
|
1891
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
1892
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
1893
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
1894
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
1895
|
+
const left = Number(displayBounds.left) || 0;
|
|
1896
|
+
const top = Number(displayBounds.top) || 0;
|
|
1897
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
1898
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
1899
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
1900
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
1901
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1902
|
+
if (layoutMode === "fit" || layoutMode === "cover") {
|
|
1903
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
1904
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
1905
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
1906
|
+
}
|
|
1907
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
1908
|
+
this._setCanvasSizeInt(
|
|
1909
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
1910
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
this.originalImage.set({
|
|
1914
|
+
originX: "left",
|
|
1915
|
+
originY: "top",
|
|
1916
|
+
left,
|
|
1917
|
+
top,
|
|
1918
|
+
scaleX,
|
|
1919
|
+
scaleY
|
|
1920
|
+
});
|
|
1921
|
+
this.originalImage.setCoords();
|
|
1922
|
+
this.baseImageScale = scaleX;
|
|
1923
|
+
this.currentScale = 1;
|
|
1924
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1925
|
+
this._updateInputs();
|
|
1926
|
+
this.canvas.renderAll();
|
|
1666
1927
|
}
|
|
1667
1928
|
/**
|
|
1668
1929
|
* Scales the original image by a given factor, with animation.
|
|
@@ -1677,7 +1938,14 @@ var ImageEditor = class {
|
|
|
1677
1938
|
} catch (error) {
|
|
1678
1939
|
return Promise.reject(error);
|
|
1679
1940
|
}
|
|
1680
|
-
return this.animationQueue.add(
|
|
1941
|
+
return this.animationQueue.add(async () => {
|
|
1942
|
+
const operationToken = this._beginBusyOperation("scaleImage");
|
|
1943
|
+
try {
|
|
1944
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
1945
|
+
} finally {
|
|
1946
|
+
this._endBusyOperation(operationToken);
|
|
1947
|
+
}
|
|
1948
|
+
}).finally(() => {
|
|
1681
1949
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1682
1950
|
});
|
|
1683
1951
|
}
|
|
@@ -1720,7 +1988,7 @@ var ImageEditor = class {
|
|
|
1720
1988
|
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1721
1989
|
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1722
1990
|
}
|
|
1723
|
-
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1991
|
+
if ((this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) && !isOwnInternalOperation) {
|
|
1724
1992
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1725
1993
|
}
|
|
1726
1994
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -1807,10 +2075,12 @@ var ImageEditor = class {
|
|
|
1807
2075
|
async _scaleImageImpl(factor, options = {}) {
|
|
1808
2076
|
if (!this.originalImage || this._disposed) return;
|
|
1809
2077
|
if (this.isAnimating) return;
|
|
2078
|
+
const numericFactor = Number(factor);
|
|
2079
|
+
if (!Number.isFinite(numericFactor)) return;
|
|
1810
2080
|
const saveHistory = options.saveHistory !== false;
|
|
1811
2081
|
let didStartAnimation = false;
|
|
1812
2082
|
try {
|
|
1813
|
-
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale,
|
|
2083
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
|
|
1814
2084
|
this.currentScale = factor;
|
|
1815
2085
|
this.isAnimating = true;
|
|
1816
2086
|
didStartAnimation = true;
|
|
@@ -1833,7 +2103,7 @@ var ImageEditor = class {
|
|
|
1833
2103
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1834
2104
|
});
|
|
1835
2105
|
this._updateInputs();
|
|
1836
|
-
if (saveHistory) this.saveState();
|
|
2106
|
+
if (saveHistory) this.saveState(options);
|
|
1837
2107
|
} finally {
|
|
1838
2108
|
if (didStartAnimation) {
|
|
1839
2109
|
this.isAnimating = false;
|
|
@@ -1855,7 +2125,14 @@ var ImageEditor = class {
|
|
|
1855
2125
|
} catch (error) {
|
|
1856
2126
|
return Promise.reject(error);
|
|
1857
2127
|
}
|
|
1858
|
-
return this.animationQueue.add(
|
|
2128
|
+
return this.animationQueue.add(async () => {
|
|
2129
|
+
const operationToken = this._beginBusyOperation("rotateImage");
|
|
2130
|
+
try {
|
|
2131
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2132
|
+
} finally {
|
|
2133
|
+
this._endBusyOperation(operationToken);
|
|
2134
|
+
}
|
|
2135
|
+
}).finally(() => {
|
|
1859
2136
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1860
2137
|
});
|
|
1861
2138
|
}
|
|
@@ -1869,7 +2146,8 @@ var ImageEditor = class {
|
|
|
1869
2146
|
async _rotateImageImpl(degrees, options = {}) {
|
|
1870
2147
|
if (!this.originalImage || this._disposed) return;
|
|
1871
2148
|
if (this.isAnimating) return;
|
|
1872
|
-
|
|
2149
|
+
const numericDegrees = Number(degrees);
|
|
2150
|
+
if (!Number.isFinite(numericDegrees)) return;
|
|
1873
2151
|
const saveHistory = options.saveHistory !== false;
|
|
1874
2152
|
const image = this.originalImage;
|
|
1875
2153
|
const previousOriginX = image.originX || "left";
|
|
@@ -1878,6 +2156,7 @@ var ImageEditor = class {
|
|
|
1878
2156
|
let didStartAnimation = false;
|
|
1879
2157
|
let didCompleteRotation = false;
|
|
1880
2158
|
try {
|
|
2159
|
+
degrees = numericDegrees;
|
|
1881
2160
|
this.currentRotation = degrees;
|
|
1882
2161
|
this.isAnimating = true;
|
|
1883
2162
|
didStartAnimation = true;
|
|
@@ -1898,7 +2177,7 @@ var ImageEditor = class {
|
|
|
1898
2177
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1899
2178
|
});
|
|
1900
2179
|
this._updateInputs();
|
|
1901
|
-
if (saveHistory) this.saveState();
|
|
2180
|
+
if (saveHistory) this.saveState(options);
|
|
1902
2181
|
didCompleteRotation = true;
|
|
1903
2182
|
} finally {
|
|
1904
2183
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -1925,19 +2204,22 @@ var ImageEditor = class {
|
|
|
1925
2204
|
return Promise.reject(error);
|
|
1926
2205
|
}
|
|
1927
2206
|
return this.animationQueue.add(async () => {
|
|
2207
|
+
const operationToken = this._beginBusyOperation("resetImageTransform");
|
|
1928
2208
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1929
2209
|
try {
|
|
1930
|
-
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1931
|
-
await this._rotateImageImpl(0, { saveHistory: false });
|
|
2210
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2211
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
1932
2212
|
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1933
2213
|
this._pushStateTransition(before, after);
|
|
1934
2214
|
} catch (error) {
|
|
1935
2215
|
try {
|
|
1936
|
-
await this.loadFromState(before);
|
|
2216
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
1937
2217
|
} catch (restoreError) {
|
|
1938
2218
|
this._reportError("resetImageTransform rollback failed", restoreError);
|
|
1939
2219
|
}
|
|
1940
2220
|
throw error;
|
|
2221
|
+
} finally {
|
|
2222
|
+
this._endBusyOperation(operationToken);
|
|
1941
2223
|
}
|
|
1942
2224
|
}).finally(() => {
|
|
1943
2225
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
@@ -1962,8 +2244,13 @@ var ImageEditor = class {
|
|
|
1962
2244
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
1963
2245
|
* @public
|
|
1964
2246
|
*/
|
|
1965
|
-
loadFromState(serializedState) {
|
|
2247
|
+
loadFromState(serializedState, options = {}) {
|
|
1966
2248
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2249
|
+
try {
|
|
2250
|
+
this._assertIdleForOperation("loadFromState", options);
|
|
2251
|
+
} catch (error) {
|
|
2252
|
+
return Promise.reject(error);
|
|
2253
|
+
}
|
|
1967
2254
|
if (this._cropMode || this._cropRect) {
|
|
1968
2255
|
this._removeCropRect();
|
|
1969
2256
|
this._restoreCropObjectState();
|
|
@@ -2120,22 +2407,29 @@ var ImageEditor = class {
|
|
|
2120
2407
|
* @returns {void}
|
|
2121
2408
|
* @public
|
|
2122
2409
|
*/
|
|
2123
|
-
saveState() {
|
|
2410
|
+
saveState(options = {}) {
|
|
2124
2411
|
if (!this.canvas) return;
|
|
2412
|
+
try {
|
|
2413
|
+
this._assertIdleForOperation("saveState", options);
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
this._reportError("saveState blocked", error);
|
|
2416
|
+
this._updateUI();
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2125
2419
|
try {
|
|
2126
2420
|
const after = this._captureCanvasStateOrThrow("saveState");
|
|
2127
2421
|
const before = this._lastSnapshot || after;
|
|
2128
2422
|
if (after === before) return;
|
|
2129
2423
|
let executedOnce = false;
|
|
2130
2424
|
const command = new Command(
|
|
2131
|
-
() => {
|
|
2425
|
+
(commandOptions = {}) => {
|
|
2132
2426
|
if (executedOnce) {
|
|
2133
|
-
return this.loadFromState(after);
|
|
2427
|
+
return this.loadFromState(after, commandOptions);
|
|
2134
2428
|
}
|
|
2135
2429
|
executedOnce = true;
|
|
2136
2430
|
return void 0;
|
|
2137
2431
|
},
|
|
2138
|
-
() => this.loadFromState(before)
|
|
2432
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2139
2433
|
);
|
|
2140
2434
|
this.historyManager.execute(command);
|
|
2141
2435
|
this._lastSnapshot = after;
|
|
@@ -2164,8 +2458,8 @@ var ImageEditor = class {
|
|
|
2164
2458
|
if (before === after) return;
|
|
2165
2459
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2166
2460
|
const command = new Command(
|
|
2167
|
-
() => this.loadFromState(after),
|
|
2168
|
-
() => this.loadFromState(before)
|
|
2461
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2462
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2169
2463
|
);
|
|
2170
2464
|
this.historyManager.push(command);
|
|
2171
2465
|
this._lastSnapshot = after;
|
|
@@ -2178,8 +2472,16 @@ var ImageEditor = class {
|
|
|
2178
2472
|
* @public
|
|
2179
2473
|
*/
|
|
2180
2474
|
undo() {
|
|
2181
|
-
|
|
2475
|
+
try {
|
|
2476
|
+
this._assertIdleForOperation("undo");
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
return Promise.reject(error);
|
|
2479
|
+
}
|
|
2480
|
+
const operationToken = this._beginBusyOperation("undo");
|
|
2481
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2182
2482
|
this._updateUI();
|
|
2483
|
+
}).finally(() => {
|
|
2484
|
+
this._endBusyOperation(operationToken);
|
|
2183
2485
|
}).catch((error) => {
|
|
2184
2486
|
this._reportError("undo failed", error);
|
|
2185
2487
|
throw error;
|
|
@@ -2192,8 +2494,16 @@ var ImageEditor = class {
|
|
|
2192
2494
|
* @public
|
|
2193
2495
|
*/
|
|
2194
2496
|
redo() {
|
|
2195
|
-
|
|
2497
|
+
try {
|
|
2498
|
+
this._assertIdleForOperation("redo");
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
return Promise.reject(error);
|
|
2501
|
+
}
|
|
2502
|
+
const operationToken = this._beginBusyOperation("redo");
|
|
2503
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2196
2504
|
this._updateUI();
|
|
2505
|
+
}).finally(() => {
|
|
2506
|
+
this._endBusyOperation(operationToken);
|
|
2197
2507
|
}).catch((error) => {
|
|
2198
2508
|
this._reportError("redo failed", error);
|
|
2199
2509
|
throw error;
|
|
@@ -2309,30 +2619,64 @@ var ImageEditor = class {
|
|
|
2309
2619
|
}
|
|
2310
2620
|
return value != null ? value : fallback;
|
|
2311
2621
|
};
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2622
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
2623
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
2624
|
+
return null;
|
|
2625
|
+
};
|
|
2626
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
2627
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
2628
|
+
const numericValue = Number(resolvedValue);
|
|
2629
|
+
if (!Number.isFinite(numericValue)) {
|
|
2630
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
2631
|
+
}
|
|
2632
|
+
if (constraints.positive && numericValue <= 0) {
|
|
2633
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
2634
|
+
}
|
|
2635
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
2636
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
2637
|
+
}
|
|
2638
|
+
return numericValue;
|
|
2639
|
+
};
|
|
2640
|
+
try {
|
|
2641
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, "width", "gap", { nonNegative: true });
|
|
2642
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, "width", "width", { positive: true });
|
|
2643
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, "height", "height", { positive: true });
|
|
2644
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, "width", "angle");
|
|
2645
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, "width", "alpha")));
|
|
2646
|
+
if (maskConfig.left === void 0 && this._lastMask) {
|
|
2647
|
+
const previousMask = this._lastMask;
|
|
2648
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
2649
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2650
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2651
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
2652
|
+
} else {
|
|
2653
|
+
left = resolveNumber(maskConfig.left, firstOffset, "width", "left");
|
|
2654
|
+
top = resolveNumber(maskConfig.top, firstOffset, "height", "top");
|
|
2655
|
+
}
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
return rejectInvalidMask("invalid numeric configuration", error);
|
|
2321
2658
|
}
|
|
2322
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
2323
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height");
|
|
2324
2659
|
maskConfig.left = left;
|
|
2325
2660
|
maskConfig.top = top;
|
|
2326
2661
|
let mask;
|
|
2327
2662
|
if (typeof maskConfig.fabricGenerator === "function") {
|
|
2328
|
-
|
|
2663
|
+
try {
|
|
2664
|
+
mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
|
|
2665
|
+
} catch (error) {
|
|
2666
|
+
return rejectInvalidMask("fabricGenerator failed", error);
|
|
2667
|
+
}
|
|
2329
2668
|
} else {
|
|
2330
2669
|
switch (shapeType) {
|
|
2331
2670
|
case "circle":
|
|
2671
|
+
try {
|
|
2672
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, "min", "radius", { positive: true });
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
return rejectInvalidMask("invalid circle radius", error);
|
|
2675
|
+
}
|
|
2332
2676
|
mask = new fabric.Circle({
|
|
2333
2677
|
left,
|
|
2334
2678
|
top,
|
|
2335
|
-
radius:
|
|
2679
|
+
radius: maskConfig.radius,
|
|
2336
2680
|
fill: maskConfig.color,
|
|
2337
2681
|
opacity: maskConfig.alpha,
|
|
2338
2682
|
angle: maskConfig.angle,
|
|
@@ -2340,11 +2684,17 @@ var ImageEditor = class {
|
|
|
2340
2684
|
});
|
|
2341
2685
|
break;
|
|
2342
2686
|
case "ellipse":
|
|
2687
|
+
try {
|
|
2688
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, "width", "rx", { positive: true });
|
|
2689
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, "height", "ry", { positive: true });
|
|
2690
|
+
} catch (error) {
|
|
2691
|
+
return rejectInvalidMask("invalid ellipse radius", error);
|
|
2692
|
+
}
|
|
2343
2693
|
mask = new fabric.Ellipse({
|
|
2344
2694
|
left,
|
|
2345
2695
|
top,
|
|
2346
|
-
rx:
|
|
2347
|
-
ry:
|
|
2696
|
+
rx: maskConfig.rx,
|
|
2697
|
+
ry: maskConfig.ry,
|
|
2348
2698
|
fill: maskConfig.color,
|
|
2349
2699
|
opacity: maskConfig.alpha,
|
|
2350
2700
|
angle: maskConfig.angle,
|
|
@@ -2353,8 +2703,31 @@ var ImageEditor = class {
|
|
|
2353
2703
|
break;
|
|
2354
2704
|
case "polygon": {
|
|
2355
2705
|
let polygonPoints = maskConfig.points || [];
|
|
2356
|
-
if (Array.isArray(polygonPoints)
|
|
2357
|
-
|
|
2706
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
2707
|
+
return rejectInvalidMask("polygon masks require at least three points");
|
|
2708
|
+
}
|
|
2709
|
+
try {
|
|
2710
|
+
polygonPoints = polygonPoints.map((point) => {
|
|
2711
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
2712
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
2713
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
2714
|
+
throw new Error("polygon point coordinates must be finite numbers");
|
|
2715
|
+
}
|
|
2716
|
+
return { x, y };
|
|
2717
|
+
});
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
return rejectInvalidMask("invalid polygon points", error);
|
|
2720
|
+
}
|
|
2721
|
+
const uniquePointKeys = new Set(polygonPoints.map((point) => `${point.x}:${point.y}`));
|
|
2722
|
+
if (uniquePointKeys.size !== polygonPoints.length) {
|
|
2723
|
+
return rejectInvalidMask("polygon points must not contain duplicates");
|
|
2724
|
+
}
|
|
2725
|
+
const doubleArea = polygonPoints.reduce((area, point, index) => {
|
|
2726
|
+
const nextPoint = polygonPoints[(index + 1) % polygonPoints.length];
|
|
2727
|
+
return area + point.x * nextPoint.y - nextPoint.x * point.y;
|
|
2728
|
+
}, 0);
|
|
2729
|
+
if (Math.abs(doubleArea) < 1e-6) {
|
|
2730
|
+
return rejectInvalidMask("polygon masks must have a non-zero area");
|
|
2358
2731
|
}
|
|
2359
2732
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2360
2733
|
left,
|
|
@@ -2368,11 +2741,17 @@ var ImageEditor = class {
|
|
|
2368
2741
|
}
|
|
2369
2742
|
case "rect":
|
|
2370
2743
|
default:
|
|
2744
|
+
try {
|
|
2745
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, "width", "rx", { nonNegative: true });
|
|
2746
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, "height", "ry", { nonNegative: true });
|
|
2747
|
+
} catch (error) {
|
|
2748
|
+
return rejectInvalidMask("invalid rectangle corner radius", error);
|
|
2749
|
+
}
|
|
2371
2750
|
mask = new fabric.Rect({
|
|
2372
2751
|
left,
|
|
2373
2752
|
top,
|
|
2374
|
-
width:
|
|
2375
|
-
height:
|
|
2753
|
+
width: maskConfig.width,
|
|
2754
|
+
height: maskConfig.height,
|
|
2376
2755
|
fill: maskConfig.color,
|
|
2377
2756
|
opacity: maskConfig.alpha,
|
|
2378
2757
|
angle: maskConfig.angle,
|
|
@@ -2410,10 +2789,10 @@ var ImageEditor = class {
|
|
|
2410
2789
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2411
2790
|
});
|
|
2412
2791
|
this._rebindMaskEvents(mask);
|
|
2413
|
-
this.
|
|
2792
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2414
2793
|
this._lastMaskInitialLeft = left;
|
|
2415
2794
|
this._lastMaskInitialTop = top;
|
|
2416
|
-
this._lastMaskInitialWidth =
|
|
2795
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2417
2796
|
const maskId = ++this.maskCounter;
|
|
2418
2797
|
mask.set({
|
|
2419
2798
|
maskId,
|
|
@@ -2428,7 +2807,12 @@ var ImageEditor = class {
|
|
|
2428
2807
|
this._updateUI();
|
|
2429
2808
|
this.canvas.renderAll();
|
|
2430
2809
|
this.saveState();
|
|
2431
|
-
if (typeof maskConfig.onCreate === "function")
|
|
2810
|
+
if (typeof maskConfig.onCreate === "function") {
|
|
2811
|
+
this._emitSafeCallback(
|
|
2812
|
+
() => maskConfig.onCreate(mask, this.canvas),
|
|
2813
|
+
"createMask onCreate callback failed"
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2432
2816
|
return mask;
|
|
2433
2817
|
}
|
|
2434
2818
|
/**
|
|
@@ -2632,8 +3016,15 @@ var ImageEditor = class {
|
|
|
2632
3016
|
this._removeLabelForMask(mask);
|
|
2633
3017
|
let textObject = null;
|
|
2634
3018
|
if (this.options.label && typeof this.options.label.create === "function") {
|
|
2635
|
-
|
|
2636
|
-
|
|
3019
|
+
let didLabelCreateThrow = false;
|
|
3020
|
+
try {
|
|
3021
|
+
textObject = this.options.label.create(mask, fabric);
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
didLabelCreateThrow = true;
|
|
3024
|
+
this._reportWarning("label.create() failed; using the default label", error);
|
|
3025
|
+
textObject = null;
|
|
3026
|
+
}
|
|
3027
|
+
if (!didLabelCreateThrow && (!textObject || typeof textObject.set !== "function")) {
|
|
2637
3028
|
this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
|
|
2638
3029
|
textObject = null;
|
|
2639
3030
|
}
|
|
@@ -2654,7 +3045,12 @@ var ImageEditor = class {
|
|
|
2654
3045
|
};
|
|
2655
3046
|
if (this.options.label) {
|
|
2656
3047
|
if (typeof this.options.label.getText === "function") {
|
|
2657
|
-
|
|
3048
|
+
try {
|
|
3049
|
+
labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
this._reportWarning("label.getText() failed; using the mask name", error);
|
|
3052
|
+
labelText = mask.maskName;
|
|
3053
|
+
}
|
|
2658
3054
|
}
|
|
2659
3055
|
if (this.options.label.textOptions) {
|
|
2660
3056
|
Object.assign(textOptions, this.options.label.textOptions);
|
|
@@ -2841,6 +3237,7 @@ var ImageEditor = class {
|
|
|
2841
3237
|
this._assertIdleForOperation("mergeMasks");
|
|
2842
3238
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2843
3239
|
if (!masks.length) return;
|
|
3240
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
2844
3241
|
const beforeJson = this._serializeCanvasState();
|
|
2845
3242
|
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2846
3243
|
this.canvas.discardActiveObject();
|
|
@@ -2859,12 +3256,13 @@ var ImageEditor = class {
|
|
|
2859
3256
|
preserveScroll: true,
|
|
2860
3257
|
resetMaskCounter: false
|
|
2861
3258
|
}));
|
|
3259
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
2862
3260
|
const afterJson = this._serializeCanvasState();
|
|
2863
3261
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2864
3262
|
} catch (error) {
|
|
2865
3263
|
this._reportError("merge error", error);
|
|
2866
3264
|
try {
|
|
2867
|
-
await this.loadFromState(beforeJson);
|
|
3265
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
2868
3266
|
} catch (restoreError) {
|
|
2869
3267
|
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2870
3268
|
}
|
|
@@ -2921,24 +3319,65 @@ var ImageEditor = class {
|
|
|
2921
3319
|
*/
|
|
2922
3320
|
async exportImageBase64(options = {}) {
|
|
2923
3321
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3322
|
+
options = options || {};
|
|
2924
3323
|
this._assertIdleForOperation("exportImageBase64", options);
|
|
3324
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3325
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageBase64");
|
|
2925
3326
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2926
3327
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2927
3328
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2928
3329
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3330
|
+
try {
|
|
3331
|
+
if (!exportImageArea) {
|
|
3332
|
+
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
3333
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3334
|
+
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
3335
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
3336
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
3337
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
3338
|
+
try {
|
|
3339
|
+
masks2.forEach((mask) => {
|
|
3340
|
+
mask.set({ visible: false });
|
|
3341
|
+
});
|
|
3342
|
+
this.canvas.discardActiveObject();
|
|
3343
|
+
this.canvas.renderAll();
|
|
3344
|
+
this.originalImage.setCoords();
|
|
3345
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3346
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3347
|
+
return await this._exportCanvasRegionToDataURL({
|
|
3348
|
+
...exportRegion,
|
|
3349
|
+
multiplier,
|
|
3350
|
+
quality,
|
|
3351
|
+
format,
|
|
3352
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3353
|
+
});
|
|
3354
|
+
} finally {
|
|
3355
|
+
maskVisibilityBackups.forEach((backup) => {
|
|
3356
|
+
try {
|
|
3357
|
+
backup.object.set({ visible: backup.visible });
|
|
3358
|
+
} catch (error) {
|
|
3359
|
+
void error;
|
|
3360
|
+
}
|
|
3361
|
+
});
|
|
3362
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
3363
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
3364
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3365
|
+
this.canvas.renderAll();
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3369
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3370
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3371
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2936
3372
|
try {
|
|
2937
|
-
|
|
2938
|
-
mask.set({ visible: false });
|
|
2939
|
-
});
|
|
3373
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2940
3374
|
this.canvas.discardActiveObject();
|
|
2941
3375
|
this.canvas.renderAll();
|
|
3376
|
+
masks.forEach((mask) => {
|
|
3377
|
+
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
3378
|
+
mask.setCoords();
|
|
3379
|
+
});
|
|
3380
|
+
this.canvas.renderAll();
|
|
2942
3381
|
this.originalImage.setCoords();
|
|
2943
3382
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2944
3383
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
@@ -2950,47 +3389,13 @@ var ImageEditor = class {
|
|
|
2950
3389
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2951
3390
|
});
|
|
2952
3391
|
} finally {
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
} catch (error) {
|
|
2957
|
-
void error;
|
|
2958
|
-
}
|
|
2959
|
-
});
|
|
2960
|
-
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2961
|
-
this._restoreMaskLabelBackups(labelBackups2);
|
|
2962
|
-
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3392
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3393
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3394
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2963
3395
|
this.canvas.renderAll();
|
|
2964
3396
|
}
|
|
2965
|
-
}
|
|
2966
|
-
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2967
|
-
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2968
|
-
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2969
|
-
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2970
|
-
try {
|
|
2971
|
-
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2972
|
-
this.canvas.discardActiveObject();
|
|
2973
|
-
this.canvas.renderAll();
|
|
2974
|
-
masks.forEach((mask) => {
|
|
2975
|
-
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
2976
|
-
mask.setCoords();
|
|
2977
|
-
});
|
|
2978
|
-
this.canvas.renderAll();
|
|
2979
|
-
this.originalImage.setCoords();
|
|
2980
|
-
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2981
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2982
|
-
return await this._exportCanvasRegionToDataURL({
|
|
2983
|
-
...exportRegion,
|
|
2984
|
-
multiplier,
|
|
2985
|
-
quality,
|
|
2986
|
-
format,
|
|
2987
|
-
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2988
|
-
});
|
|
2989
3397
|
} finally {
|
|
2990
|
-
this.
|
|
2991
|
-
this._restoreMaskLabelBackups(labelBackups);
|
|
2992
|
-
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2993
|
-
this.canvas.renderAll();
|
|
3398
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2994
3399
|
}
|
|
2995
3400
|
}
|
|
2996
3401
|
/**
|
|
@@ -3023,7 +3428,10 @@ var ImageEditor = class {
|
|
|
3023
3428
|
*/
|
|
3024
3429
|
async exportImageFile(options = {}) {
|
|
3025
3430
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3026
|
-
|
|
3431
|
+
options = options || {};
|
|
3432
|
+
this._assertIdleForOperation("exportImageFile", options);
|
|
3433
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3434
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageFile");
|
|
3027
3435
|
const {
|
|
3028
3436
|
mergeMask = true,
|
|
3029
3437
|
fileType = "jpeg",
|
|
@@ -3033,48 +3441,52 @@ var ImageEditor = class {
|
|
|
3033
3441
|
} = options;
|
|
3034
3442
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3035
3443
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3444
|
+
try {
|
|
3445
|
+
let imageBase64;
|
|
3446
|
+
if (mergeMask) {
|
|
3447
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3448
|
+
exportImageArea: true,
|
|
3449
|
+
multiplier,
|
|
3450
|
+
quality: normalizedQuality,
|
|
3451
|
+
fileType: safeFileType
|
|
3452
|
+
}));
|
|
3453
|
+
} else {
|
|
3454
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3455
|
+
exportImageArea: false,
|
|
3456
|
+
multiplier,
|
|
3457
|
+
quality: normalizedQuality,
|
|
3458
|
+
fileType: safeFileType
|
|
3459
|
+
}));
|
|
3460
|
+
}
|
|
3461
|
+
let imageDataUrl = imageBase64;
|
|
3462
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3463
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3464
|
+
const imageElement = new window.Image();
|
|
3465
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3466
|
+
imageElement.onload = () => {
|
|
3467
|
+
try {
|
|
3468
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
3469
|
+
offscreenCanvas.width = imageElement.width;
|
|
3470
|
+
offscreenCanvas.height = imageElement.height;
|
|
3471
|
+
const context = offscreenCanvas.getContext("2d");
|
|
3472
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
3473
|
+
context.drawImage(imageElement, 0, 0);
|
|
3474
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3475
|
+
resolve(convertedDataUrl);
|
|
3476
|
+
} catch (error) {
|
|
3477
|
+
reject(error);
|
|
3478
|
+
}
|
|
3479
|
+
};
|
|
3480
|
+
imageElement.onerror = reject;
|
|
3481
|
+
imageElement.src = imageBase64;
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3485
|
+
const mime = `image/${safeFileType}`;
|
|
3486
|
+
return new File([bytes], fileName, { type: mime });
|
|
3487
|
+
} finally {
|
|
3488
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3074
3489
|
}
|
|
3075
|
-
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
3076
|
-
const mime = `image/${safeFileType}`;
|
|
3077
|
-
return new File([bytes], fileName, { type: mime });
|
|
3078
3490
|
}
|
|
3079
3491
|
_clearMaskPlacementMemory() {
|
|
3080
3492
|
this._lastMask = null;
|
|
@@ -3082,7 +3494,7 @@ var ImageEditor = class {
|
|
|
3082
3494
|
this._lastMaskInitialTop = null;
|
|
3083
3495
|
this._lastMaskInitialWidth = null;
|
|
3084
3496
|
}
|
|
3085
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
3497
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
3086
3498
|
this._reportError(message, error);
|
|
3087
3499
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
3088
3500
|
this._cropRect = null;
|
|
@@ -3093,7 +3505,7 @@ var ImageEditor = class {
|
|
|
3093
3505
|
this._prevSelectionSetting = void 0;
|
|
3094
3506
|
if (beforeJson) {
|
|
3095
3507
|
try {
|
|
3096
|
-
await this.loadFromState(beforeJson);
|
|
3508
|
+
await this.loadFromState(beforeJson, options);
|
|
3097
3509
|
} catch (restoreError) {
|
|
3098
3510
|
this._reportError("applyCrop: rollback failed", restoreError);
|
|
3099
3511
|
}
|
|
@@ -3139,6 +3551,49 @@ var ImageEditor = class {
|
|
|
3139
3551
|
this._cropRect = null;
|
|
3140
3552
|
this._cropHandlers = [];
|
|
3141
3553
|
}
|
|
3554
|
+
_getCropRectContentBounds(cropRect) {
|
|
3555
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
3556
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
3557
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
3558
|
+
return {
|
|
3559
|
+
left: Number(cropRect.left) || 0,
|
|
3560
|
+
top: Number(cropRect.top) || 0,
|
|
3561
|
+
width,
|
|
3562
|
+
height
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
_getCropRectRawBounds(cropRect) {
|
|
3566
|
+
if (!cropRect) return { left: NaN, top: NaN, width: NaN, height: NaN };
|
|
3567
|
+
return {
|
|
3568
|
+
left: Number(cropRect.left),
|
|
3569
|
+
top: Number(cropRect.top),
|
|
3570
|
+
width: Number(cropRect.width) * Math.abs(Number(cropRect.scaleX)),
|
|
3571
|
+
height: Number(cropRect.height) * Math.abs(Number(cropRect.scaleY))
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
_isValidCropRegion(cropBounds, imageBounds) {
|
|
3575
|
+
if (!cropBounds || !imageBounds) return false;
|
|
3576
|
+
const left = Number(cropBounds.left);
|
|
3577
|
+
const top = Number(cropBounds.top);
|
|
3578
|
+
const width = Number(cropBounds.width);
|
|
3579
|
+
const height = Number(cropBounds.height);
|
|
3580
|
+
const imageLeft = Number(imageBounds.left);
|
|
3581
|
+
const imageTop = Number(imageBounds.top);
|
|
3582
|
+
const imageWidth = Number(imageBounds.width);
|
|
3583
|
+
const imageHeight = Number(imageBounds.height);
|
|
3584
|
+
if (![left, top, width, height, imageLeft, imageTop, imageWidth, imageHeight].every(Number.isFinite)) return false;
|
|
3585
|
+
if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return false;
|
|
3586
|
+
const right = left + width;
|
|
3587
|
+
const bottom = top + height;
|
|
3588
|
+
const imageRight = imageLeft + imageWidth;
|
|
3589
|
+
const imageBottom = imageTop + imageHeight;
|
|
3590
|
+
const overlapsImage = left < imageRight && right > imageLeft && top < imageBottom && bottom > imageTop;
|
|
3591
|
+
if (!overlapsImage) return false;
|
|
3592
|
+
const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
|
|
3593
|
+
const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
|
|
3594
|
+
if (!Number.isFinite(canvasWidth) || !Number.isFinite(canvasHeight) || canvasWidth <= 0 || canvasHeight <= 0) return false;
|
|
3595
|
+
return left < canvasWidth && right > 0 && top < canvasHeight && bottom > 0;
|
|
3596
|
+
}
|
|
3142
3597
|
/**
|
|
3143
3598
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
3144
3599
|
*
|
|
@@ -3150,6 +3605,10 @@ var ImageEditor = class {
|
|
|
3150
3605
|
*/
|
|
3151
3606
|
enterCropMode() {
|
|
3152
3607
|
if (!this.canvas || !this.originalImage || this._cropMode) return;
|
|
3608
|
+
if (this._isApplyingCrop) {
|
|
3609
|
+
this._reportWarning("enterCropMode ignored because a crop is already being applied");
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3153
3612
|
if (!this._canMutateNow("enterCropMode")) return;
|
|
3154
3613
|
if (!this.isImageLoaded()) return;
|
|
3155
3614
|
this._removeCropRect();
|
|
@@ -3162,14 +3621,19 @@ var ImageEditor = class {
|
|
|
3162
3621
|
const padding = this.options.crop && this.options.crop.padding ? this.options.crop.padding : 10;
|
|
3163
3622
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
3164
3623
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
3165
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
3166
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
3624
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
3625
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
3167
3626
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
3168
3627
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
3169
3628
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3170
3629
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3171
3630
|
const width = minCropWidth;
|
|
3172
3631
|
const height = minCropHeight;
|
|
3632
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
3633
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
3634
|
+
this._cropRotationWarningEmitted = true;
|
|
3635
|
+
this._reportWarning("crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported");
|
|
3636
|
+
}
|
|
3173
3637
|
const cropRect = new fabric.Rect({
|
|
3174
3638
|
left,
|
|
3175
3639
|
top,
|
|
@@ -3181,8 +3645,8 @@ var ImageEditor = class {
|
|
|
3181
3645
|
strokeWidth: 1,
|
|
3182
3646
|
strokeUniform: true,
|
|
3183
3647
|
selectable: true,
|
|
3184
|
-
hasRotatingPoint:
|
|
3185
|
-
lockRotation:
|
|
3648
|
+
hasRotatingPoint: false,
|
|
3649
|
+
lockRotation: true,
|
|
3186
3650
|
cornerSize: 8,
|
|
3187
3651
|
objectCaching: false,
|
|
3188
3652
|
originX: "left",
|
|
@@ -3219,7 +3683,7 @@ var ImageEditor = class {
|
|
|
3219
3683
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3220
3684
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3221
3685
|
cropRect.setCoords();
|
|
3222
|
-
const cropBounds =
|
|
3686
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3223
3687
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3224
3688
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3225
3689
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3269,6 +3733,10 @@ var ImageEditor = class {
|
|
|
3269
3733
|
* @public
|
|
3270
3734
|
*/
|
|
3271
3735
|
cancelCrop() {
|
|
3736
|
+
if (this._isApplyingCrop) {
|
|
3737
|
+
this._reportWarning("cancelCrop ignored because a crop is already being applied");
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3272
3740
|
if (!this.canvas || !this._cropMode) return;
|
|
3273
3741
|
this._removeCropRect();
|
|
3274
3742
|
this._restoreCropObjectState();
|
|
@@ -3292,95 +3760,120 @@ var ImageEditor = class {
|
|
|
3292
3760
|
*/
|
|
3293
3761
|
async applyCrop() {
|
|
3294
3762
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3295
|
-
this.
|
|
3296
|
-
|
|
3297
|
-
const rectBounds = this._cropRect.getBoundingRect(true, true);
|
|
3298
|
-
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3299
|
-
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3300
|
-
this._restoreCropObjectState();
|
|
3301
|
-
let beforeJson;
|
|
3302
|
-
try {
|
|
3303
|
-
beforeJson = this._serializeCanvasState();
|
|
3304
|
-
} catch (error) {
|
|
3305
|
-
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3306
|
-
beforeJson = null;
|
|
3307
|
-
}
|
|
3308
|
-
if (!beforeJson) {
|
|
3309
|
-
this.cancelCrop();
|
|
3763
|
+
if (this._isApplyingCrop) {
|
|
3764
|
+
this._reportWarning("applyCrop ignored because a crop is already being applied");
|
|
3310
3765
|
return;
|
|
3311
3766
|
}
|
|
3312
|
-
|
|
3767
|
+
this._assertIdleForOperation("applyCrop");
|
|
3768
|
+
this._isApplyingCrop = true;
|
|
3769
|
+
const operationToken = this._beginBusyOperation("applyCrop");
|
|
3770
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3313
3771
|
try {
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3772
|
+
this._cropRect.setCoords();
|
|
3773
|
+
this.originalImage.setCoords();
|
|
3774
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3775
|
+
const rawCropBounds = this._getCropRectRawBounds(this._cropRect);
|
|
3776
|
+
if (!this._isValidCropRegion(rawCropBounds, imageBounds)) {
|
|
3777
|
+
this._reportWarning("applyCrop: crop region is invalid");
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3781
|
+
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3782
|
+
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3783
|
+
this._restoreCropObjectState();
|
|
3784
|
+
let beforeJson;
|
|
3785
|
+
try {
|
|
3786
|
+
beforeJson = this._serializeCanvasState();
|
|
3787
|
+
} catch (error) {
|
|
3788
|
+
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3789
|
+
beforeJson = null;
|
|
3790
|
+
}
|
|
3791
|
+
if (!beforeJson) {
|
|
3792
|
+
this._removeCropRect();
|
|
3793
|
+
this._cropMode = false;
|
|
3794
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3795
|
+
this._prevSelectionSetting = void 0;
|
|
3330
3796
|
this.canvas.discardActiveObject();
|
|
3797
|
+
this._updateUI();
|
|
3331
3798
|
this.canvas.renderAll();
|
|
3799
|
+
return;
|
|
3332
3800
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3801
|
+
const preservedMasks = [];
|
|
3802
|
+
try {
|
|
3803
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3804
|
+
if (masks && masks.length) {
|
|
3805
|
+
masks.forEach((mask) => {
|
|
3806
|
+
mask.setCoords();
|
|
3807
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3808
|
+
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;
|
|
3809
|
+
this._removeLabelForMask(mask);
|
|
3810
|
+
this._cleanupMaskEvents(mask);
|
|
3811
|
+
this.canvas.remove(mask);
|
|
3812
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3813
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3814
|
+
mask.set({ visible: true });
|
|
3815
|
+
preservedMasks.push(mask);
|
|
3816
|
+
}
|
|
3817
|
+
});
|
|
3818
|
+
this._clearMaskPlacementMemory();
|
|
3819
|
+
this.canvas.discardActiveObject();
|
|
3820
|
+
this.canvas.renderAll();
|
|
3821
|
+
}
|
|
3822
|
+
} catch (error) {
|
|
3823
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error, internalOptions);
|
|
3824
|
+
return;
|
|
3825
|
+
}
|
|
3826
|
+
this._removeCropRect();
|
|
3827
|
+
this._cropMode = false;
|
|
3828
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3829
|
+
this._prevSelectionSetting = void 0;
|
|
3830
|
+
let croppedBase64;
|
|
3831
|
+
try {
|
|
3832
|
+
croppedBase64 = await this._exportCanvasRegionToDataURL({
|
|
3833
|
+
...cropRegion,
|
|
3834
|
+
multiplier: 1,
|
|
3835
|
+
quality: this._normalizeQuality(this.options.downsampleQuality),
|
|
3836
|
+
format: "jpeg"
|
|
3360
3837
|
});
|
|
3361
|
-
|
|
3362
|
-
this.
|
|
3363
|
-
|
|
3364
|
-
this.canvas.renderAll();
|
|
3838
|
+
} catch (error) {
|
|
3839
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to create cropped image", error, internalOptions);
|
|
3840
|
+
return;
|
|
3365
3841
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3842
|
+
try {
|
|
3843
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3844
|
+
if (preservedMasks.length) {
|
|
3845
|
+
preservedMasks.forEach((mask) => {
|
|
3846
|
+
this._rebindMaskEvents(mask);
|
|
3847
|
+
this.canvas.add(mask);
|
|
3848
|
+
this.canvas.bringToFront(mask);
|
|
3849
|
+
});
|
|
3850
|
+
this._lastMask = preservedMasks[preservedMasks.length - 1];
|
|
3851
|
+
this.maskCounter = preservedMasks.reduce((max, mask) => Math.max(max, mask.maskId || 0), this.maskCounter);
|
|
3852
|
+
this._updateMaskList();
|
|
3853
|
+
this.canvas.renderAll();
|
|
3854
|
+
}
|
|
3855
|
+
} catch (error) {
|
|
3856
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", error, internalOptions);
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
let afterJson;
|
|
3860
|
+
try {
|
|
3861
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
3862
|
+
} catch (error) {
|
|
3863
|
+
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
3864
|
+
afterJson = null;
|
|
3865
|
+
}
|
|
3866
|
+
try {
|
|
3867
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
3868
|
+
} catch (error) {
|
|
3869
|
+
this._reportWarning("applyCrop: failed to push history command", error);
|
|
3870
|
+
}
|
|
3871
|
+
this._updateUI();
|
|
3872
|
+
this.canvas.renderAll();
|
|
3873
|
+
} finally {
|
|
3874
|
+
this._isApplyingCrop = false;
|
|
3875
|
+
this._endBusyOperation(operationToken);
|
|
3381
3876
|
}
|
|
3382
|
-
this._updateUI();
|
|
3383
|
-
this.canvas.renderAll();
|
|
3384
3877
|
}
|
|
3385
3878
|
/* ---------- Misc / UI ---------- */
|
|
3386
3879
|
/**
|
|
@@ -3410,9 +3903,11 @@ var ImageEditor = class {
|
|
|
3410
3903
|
const isInCropMode = !!this._cropMode;
|
|
3411
3904
|
const isBusy = this.isBusy();
|
|
3412
3905
|
if (isInCropMode) {
|
|
3906
|
+
const cropInteractionKeys = /* @__PURE__ */ new Set(["canvas", "canvasContainer", "imagePlaceholder", "imgPlaceholder"]);
|
|
3413
3907
|
for (const key of Object.keys(this.elements || {})) {
|
|
3414
3908
|
const element = this._getElement(key);
|
|
3415
3909
|
if (!element) continue;
|
|
3910
|
+
if (cropInteractionKeys.has(key)) continue;
|
|
3416
3911
|
if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3417
3912
|
this._setDisabled(key, false);
|
|
3418
3913
|
} else {
|
|
@@ -3450,9 +3945,44 @@ var ImageEditor = class {
|
|
|
3450
3945
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3451
3946
|
* @private
|
|
3452
3947
|
*/
|
|
3948
|
+
_rememberElementDisabledState(key, element) {
|
|
3949
|
+
if (!element) return;
|
|
3950
|
+
if (!this._elementOriginalDisabledState) this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
3951
|
+
if (this._elementOriginalDisabledState.has(key)) return;
|
|
3952
|
+
this._elementOriginalDisabledState.set(key, {
|
|
3953
|
+
element,
|
|
3954
|
+
hasDisabledProperty: "disabled" in element,
|
|
3955
|
+
disabled: "disabled" in element ? !!element.disabled : void 0,
|
|
3956
|
+
ariaDisabled: element.getAttribute ? element.getAttribute("aria-disabled") : null,
|
|
3957
|
+
pointerEvents: element.style ? element.style.pointerEvents || "" : ""
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
_restoreElementDisabledStates() {
|
|
3961
|
+
if (!this._elementOriginalDisabledState) return;
|
|
3962
|
+
for (const state of this._elementOriginalDisabledState.values()) {
|
|
3963
|
+
const element = state && state.element;
|
|
3964
|
+
if (!element) continue;
|
|
3965
|
+
try {
|
|
3966
|
+
if (state.hasDisabledProperty && "disabled" in element) {
|
|
3967
|
+
element.disabled = !!state.disabled;
|
|
3968
|
+
}
|
|
3969
|
+
if (element.getAttribute && element.setAttribute && element.removeAttribute) {
|
|
3970
|
+
if (state.ariaDisabled === null) {
|
|
3971
|
+
element.removeAttribute("aria-disabled");
|
|
3972
|
+
} else {
|
|
3973
|
+
element.setAttribute("aria-disabled", state.ariaDisabled);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
if (element.style) element.style.pointerEvents = state.pointerEvents || "";
|
|
3977
|
+
} catch (error) {
|
|
3978
|
+
void error;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3453
3982
|
_setDisabled(key, disabled) {
|
|
3454
3983
|
const element = this._getElement(key);
|
|
3455
3984
|
if (!element) return;
|
|
3985
|
+
this._rememberElementDisabledState(key, element);
|
|
3456
3986
|
if ("disabled" in element) {
|
|
3457
3987
|
element.disabled = !!disabled;
|
|
3458
3988
|
return;
|
|
@@ -3479,7 +4009,6 @@ var ImageEditor = class {
|
|
|
3479
4009
|
* @private
|
|
3480
4010
|
*/
|
|
3481
4011
|
_updatePlaceholderStatus() {
|
|
3482
|
-
if (!this.options.showPlaceholder) return;
|
|
3483
4012
|
this._setPlaceholderVisible(!this.originalImage);
|
|
3484
4013
|
}
|
|
3485
4014
|
/**
|
|
@@ -3489,10 +4018,11 @@ var ImageEditor = class {
|
|
|
3489
4018
|
* @private
|
|
3490
4019
|
*/
|
|
3491
4020
|
_setPlaceholderVisible(show) {
|
|
3492
|
-
|
|
4021
|
+
const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
|
|
4022
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
|
|
3493
4023
|
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
3494
4024
|
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
3495
|
-
this._setElementVisible(canvasVisibilityElement, !
|
|
4025
|
+
this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
|
|
3496
4026
|
}
|
|
3497
4027
|
}
|
|
3498
4028
|
_getCanvasVisibilityElement() {
|
|
@@ -3571,6 +4101,12 @@ var ImageEditor = class {
|
|
|
3571
4101
|
void error;
|
|
3572
4102
|
}
|
|
3573
4103
|
if (this._cropRect) this._removeCropRect();
|
|
4104
|
+
this._isApplyingCrop = false;
|
|
4105
|
+
try {
|
|
4106
|
+
this._restoreElementDisabledStates();
|
|
4107
|
+
} catch (error) {
|
|
4108
|
+
void error;
|
|
4109
|
+
}
|
|
3574
4110
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3575
4111
|
try {
|
|
3576
4112
|
this._restoreContainerOverflowState();
|
|
@@ -3618,6 +4154,7 @@ var ImageEditor = class {
|
|
|
3618
4154
|
this._handlersByElementKey = {};
|
|
3619
4155
|
this._elementCache = {};
|
|
3620
4156
|
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
4157
|
+
this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
3621
4158
|
this._clearMaskPlacementMemory();
|
|
3622
4159
|
this.originalImage = null;
|
|
3623
4160
|
this.baseImageScale = 1;
|
|
@@ -3626,6 +4163,7 @@ var ImageEditor = class {
|
|
|
3626
4163
|
this.isAnimating = false;
|
|
3627
4164
|
this._isLoading = false;
|
|
3628
4165
|
this._cropMode = false;
|
|
4166
|
+
this._isApplyingCrop = false;
|
|
3629
4167
|
this._cropRect = null;
|
|
3630
4168
|
this._cropHandlers = [];
|
|
3631
4169
|
this._cropPrevEvented = null;
|
|
@@ -3728,9 +4266,10 @@ var HistoryManager = class {
|
|
|
3728
4266
|
* @param {number} [maxSize=50] - Maximum number of commands to keep in history.
|
|
3729
4267
|
*/
|
|
3730
4268
|
constructor(maxSize = 50) {
|
|
4269
|
+
const numericMaxSize = Number(maxSize);
|
|
3731
4270
|
this.history = [];
|
|
3732
4271
|
this.currentIndex = -1;
|
|
3733
|
-
this.maxSize =
|
|
4272
|
+
this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
|
|
3734
4273
|
this.pending = Promise.resolve();
|
|
3735
4274
|
}
|
|
3736
4275
|
/**
|
|
@@ -3800,11 +4339,11 @@ var HistoryManager = class {
|
|
|
3800
4339
|
*
|
|
3801
4340
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
3802
4341
|
*/
|
|
3803
|
-
undo() {
|
|
4342
|
+
undo(options = {}) {
|
|
3804
4343
|
return this.enqueue(async () => {
|
|
3805
4344
|
if (this.currentIndex >= 0) {
|
|
3806
4345
|
const index = this.currentIndex;
|
|
3807
|
-
await this.history[index].undo();
|
|
4346
|
+
await this.history[index].undo(options);
|
|
3808
4347
|
this.currentIndex = index - 1;
|
|
3809
4348
|
}
|
|
3810
4349
|
});
|
|
@@ -3814,11 +4353,11 @@ var HistoryManager = class {
|
|
|
3814
4353
|
*
|
|
3815
4354
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
3816
4355
|
*/
|
|
3817
|
-
redo() {
|
|
4356
|
+
redo(options = {}) {
|
|
3818
4357
|
return this.enqueue(async () => {
|
|
3819
4358
|
if (this.currentIndex < this.history.length - 1) {
|
|
3820
4359
|
const index = this.currentIndex + 1;
|
|
3821
|
-
await this.history[index].execute();
|
|
4360
|
+
await this.history[index].execute(options);
|
|
3822
4361
|
this.currentIndex = index;
|
|
3823
4362
|
}
|
|
3824
4363
|
});
|