@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
package/dist/image-editor.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file image-editor.js
|
|
5
5
|
* @module image-editor
|
|
6
|
-
* @version 1.5.
|
|
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.
|
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
downsampleMimeType: null,
|
|
76
76
|
imageLoadTimeoutMs: 3e4,
|
|
77
77
|
exportMultiplier: 1,
|
|
78
|
+
maxExportPixels: 5e7,
|
|
79
|
+
maxHistorySize: 50,
|
|
78
80
|
exportImageAreaByDefault: true,
|
|
79
81
|
defaultMaskWidth: 50,
|
|
80
82
|
defaultMaskHeight: 80,
|
|
@@ -103,6 +105,7 @@
|
|
|
103
105
|
...userCrop
|
|
104
106
|
}
|
|
105
107
|
};
|
|
108
|
+
this._normalizeOptions();
|
|
106
109
|
this._fabricLoaded = !!ensureFabric();
|
|
107
110
|
if (!this._fabricLoaded) {
|
|
108
111
|
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
@@ -122,16 +125,18 @@
|
|
|
122
125
|
this._activeOperationToken = null;
|
|
123
126
|
this.elements = {};
|
|
124
127
|
this.isImageLoadedToCanvas = false;
|
|
125
|
-
this.maxHistorySize =
|
|
128
|
+
this.maxHistorySize = this.options.maxHistorySize;
|
|
126
129
|
this._handlersByElementKey = {};
|
|
127
130
|
this._elementCache = {};
|
|
128
131
|
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
132
|
+
this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
129
133
|
this._lastMask = null;
|
|
130
134
|
this._lastMaskInitialLeft = null;
|
|
131
135
|
this._lastMaskInitialTop = null;
|
|
132
136
|
this._lastMaskInitialWidth = null;
|
|
133
137
|
this._lastSnapshot = null;
|
|
134
138
|
this._cropMode = false;
|
|
139
|
+
this._isApplyingCrop = false;
|
|
135
140
|
this._cropRect = null;
|
|
136
141
|
this._cropHandlers = [];
|
|
137
142
|
this._cropPrevEvented = null;
|
|
@@ -144,6 +149,8 @@
|
|
|
144
149
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
145
150
|
this._disposed = false;
|
|
146
151
|
this._initialized = false;
|
|
152
|
+
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
153
|
+
this._cropRotationWarningEmitted = false;
|
|
147
154
|
this.onImageLoaded = typeof this.options.onImageLoaded === "function" ? this.options.onImageLoaded : null;
|
|
148
155
|
this.animationQueue = new AnimationQueue();
|
|
149
156
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -208,7 +215,13 @@
|
|
|
208
215
|
* });
|
|
209
216
|
*/
|
|
210
217
|
init(idMap = {}) {
|
|
211
|
-
if (!this._fabricLoaded)
|
|
218
|
+
if (!this._fabricLoaded) {
|
|
219
|
+
this._fabricLoaded = !!ensureFabric();
|
|
220
|
+
if (!this._fabricLoaded) {
|
|
221
|
+
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
212
225
|
if (this._initialized || this.canvas) this.dispose();
|
|
213
226
|
this._disposed = false;
|
|
214
227
|
this._initialized = true;
|
|
@@ -220,10 +233,11 @@
|
|
|
220
233
|
this._activeOperationName = null;
|
|
221
234
|
this._activeOperationToken = null;
|
|
222
235
|
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
236
|
+
this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
237
|
+
this._isApplyingCrop = false;
|
|
223
238
|
this._containerOriginalOverflow = null;
|
|
224
239
|
this._lastContainerViewportSize = null;
|
|
225
240
|
this._canvasElementOriginalStyle = null;
|
|
226
|
-
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
227
241
|
const defaults = {
|
|
228
242
|
canvas: "fabricCanvas",
|
|
229
243
|
canvasContainer: null,
|
|
@@ -262,6 +276,7 @@
|
|
|
262
276
|
redoButton: "redoButton",
|
|
263
277
|
redoBtn: null,
|
|
264
278
|
imageInput: "imageInput",
|
|
279
|
+
uploadArea: null,
|
|
265
280
|
enterCropModeButton: "enterCropModeButton",
|
|
266
281
|
cropBtn: null,
|
|
267
282
|
applyCropButton: "applyCropButton",
|
|
@@ -277,7 +292,7 @@
|
|
|
277
292
|
this._updateMaskList();
|
|
278
293
|
this._updateUI();
|
|
279
294
|
if (this.options.initialImageBase64) {
|
|
280
|
-
this.loadImage(this.options.initialImageBase64);
|
|
295
|
+
this.loadImage(this.options.initialImageBase64).catch((error) => this._reportError("initialImageBase64 could not be loaded", error));
|
|
281
296
|
} else {
|
|
282
297
|
this._updatePlaceholderStatus();
|
|
283
298
|
}
|
|
@@ -339,6 +354,54 @@
|
|
|
339
354
|
`ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
|
|
340
355
|
);
|
|
341
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
|
+
}
|
|
342
405
|
_reportError(message, error = null) {
|
|
343
406
|
const handler = this.options && this.options.onError;
|
|
344
407
|
if (typeof handler !== "function") return;
|
|
@@ -355,10 +418,18 @@
|
|
|
355
418
|
} catch {
|
|
356
419
|
}
|
|
357
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
|
+
}
|
|
358
429
|
_notifyImageLoaded() {
|
|
359
430
|
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
360
431
|
const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
|
|
361
|
-
|
|
432
|
+
this._emitSafeCallback(callback, "onImageLoaded callback failed");
|
|
362
433
|
}
|
|
363
434
|
/**
|
|
364
435
|
* Initializes the Fabric canvas, viewport elements, and selection event handlers.
|
|
@@ -478,13 +549,14 @@
|
|
|
478
549
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
479
550
|
this._captureContainerOverflowState();
|
|
480
551
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
481
|
-
|
|
552
|
+
const layoutMode = this._getImageLayoutMode();
|
|
553
|
+
if (layoutMode === "cover") {
|
|
482
554
|
this.containerElement.style.overflow = "scroll";
|
|
483
555
|
if (!shouldPreserveScroll) {
|
|
484
556
|
this.containerElement.scrollLeft = 0;
|
|
485
557
|
this.containerElement.scrollTop = 0;
|
|
486
558
|
}
|
|
487
|
-
} else if (
|
|
559
|
+
} else if (layoutMode === "fit") {
|
|
488
560
|
this.containerElement.style.overflow = "auto";
|
|
489
561
|
if (!shouldPreserveScroll) {
|
|
490
562
|
this.containerElement.scrollLeft = 0;
|
|
@@ -596,7 +668,6 @@
|
|
|
596
668
|
_loadImageFile(file) {
|
|
597
669
|
if (!this._isSupportedImageFile(file)) {
|
|
598
670
|
const error = new Error("Selected file is not a supported image");
|
|
599
|
-
this._reportError("Selected file is not a supported image", error);
|
|
600
671
|
return Promise.reject(error);
|
|
601
672
|
}
|
|
602
673
|
return new Promise((resolve, reject) => {
|
|
@@ -635,6 +706,12 @@
|
|
|
635
706
|
`Only one image layout mode should be enabled. Active modes: ${activeModes.join(", ")}.`
|
|
636
707
|
);
|
|
637
708
|
}
|
|
709
|
+
_getImageLayoutMode() {
|
|
710
|
+
if (this.options.fitImageToCanvas) return "fit";
|
|
711
|
+
if (this.options.coverImageToCanvas) return "cover";
|
|
712
|
+
if (this.options.expandCanvasToImage) return "expand";
|
|
713
|
+
return "contain";
|
|
714
|
+
}
|
|
638
715
|
/**
|
|
639
716
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
640
717
|
*
|
|
@@ -648,12 +725,17 @@
|
|
|
648
725
|
if (!this._fabricLoaded) return;
|
|
649
726
|
if (!this.canvas || this._disposed) return;
|
|
650
727
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
728
|
+
options = options || {};
|
|
651
729
|
this._assertIdleForOperation("loadImage", options);
|
|
652
|
-
|
|
653
|
-
this.
|
|
654
|
-
|
|
655
|
-
|
|
730
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
731
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
|
|
732
|
+
let transaction = null;
|
|
733
|
+
let shouldNotifyImageLoaded;
|
|
656
734
|
try {
|
|
735
|
+
this._isLoading = true;
|
|
736
|
+
this._updateUI();
|
|
737
|
+
this._warnOnImageLayoutOptionConflict();
|
|
738
|
+
transaction = this._captureLoadImageTransaction();
|
|
657
739
|
const imageElement = await this._createImageElement(imageBase64);
|
|
658
740
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
659
741
|
let loadSource = imageBase64;
|
|
@@ -693,7 +775,8 @@
|
|
|
693
775
|
const viewport = this._getContainerViewportSize();
|
|
694
776
|
const minWidth = viewport.width;
|
|
695
777
|
const minHeight = viewport.height;
|
|
696
|
-
|
|
778
|
+
const layoutMode = this._getImageLayoutMode();
|
|
779
|
+
if (layoutMode === "fit") {
|
|
697
780
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
698
781
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
699
782
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -701,13 +784,13 @@
|
|
|
701
784
|
fabricImage.set({ left: 0, top: 0 });
|
|
702
785
|
fabricImage.scale(fitScale);
|
|
703
786
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
704
|
-
} else if (
|
|
787
|
+
} else if (layoutMode === "cover") {
|
|
705
788
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
706
789
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
707
790
|
fabricImage.set({ left: 0, top: 0 });
|
|
708
791
|
fabricImage.scale(layout.scale);
|
|
709
792
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
710
|
-
} else if (
|
|
793
|
+
} else if (layoutMode === "expand") {
|
|
711
794
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
712
795
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
713
796
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -736,14 +819,21 @@
|
|
|
736
819
|
this._updateUI();
|
|
737
820
|
this.canvas.renderAll();
|
|
738
821
|
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
739
|
-
|
|
822
|
+
shouldNotifyImageLoaded = true;
|
|
740
823
|
} catch (error) {
|
|
741
|
-
await this._rollbackLoadImageTransaction(
|
|
824
|
+
await this._rollbackLoadImageTransaction(
|
|
825
|
+
transaction,
|
|
826
|
+
this._withInternalOperationOptions(operationToken)
|
|
827
|
+
);
|
|
742
828
|
throw error;
|
|
743
829
|
} finally {
|
|
744
830
|
this._isLoading = false;
|
|
831
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
745
832
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
746
833
|
}
|
|
834
|
+
if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
|
|
835
|
+
this._notifyImageLoaded();
|
|
836
|
+
}
|
|
747
837
|
}
|
|
748
838
|
/**
|
|
749
839
|
* Checks whether there is a loaded image on the current canvas.
|
|
@@ -760,7 +850,7 @@
|
|
|
760
850
|
* @public
|
|
761
851
|
*/
|
|
762
852
|
isBusy() {
|
|
763
|
-
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());
|
|
764
854
|
}
|
|
765
855
|
/**
|
|
766
856
|
* Creates an HTMLImageElement from a given data URL.
|
|
@@ -854,13 +944,13 @@
|
|
|
854
944
|
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
855
945
|
};
|
|
856
946
|
}
|
|
857
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
947
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
858
948
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
859
949
|
let didRestoreCanvasState = false;
|
|
860
950
|
let didFailCanvasRestore = false;
|
|
861
951
|
try {
|
|
862
952
|
if (transaction.canvasState) {
|
|
863
|
-
await this.loadFromState(transaction.canvasState);
|
|
953
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
864
954
|
didRestoreCanvasState = true;
|
|
865
955
|
}
|
|
866
956
|
} catch (error) {
|
|
@@ -1110,9 +1200,9 @@
|
|
|
1110
1200
|
}
|
|
1111
1201
|
_getScrollableCanvasSize(contentWidth, contentHeight, viewport = this._getContainerViewportSize()) {
|
|
1112
1202
|
if (this._hasFixedContainerScrollbars()) {
|
|
1113
|
-
const
|
|
1114
|
-
const safeWidth = Math.max(1, viewport.width -
|
|
1115
|
-
const safeHeight = Math.max(1, viewport.height -
|
|
1203
|
+
const safetyMargin2 = this._getScrollSafetyMargin();
|
|
1204
|
+
const safeWidth = Math.max(1, viewport.width - safetyMargin2);
|
|
1205
|
+
const safeHeight = Math.max(1, viewport.height - safetyMargin2);
|
|
1116
1206
|
return {
|
|
1117
1207
|
width: contentWidth > viewport.width + 0.5 ? this._ceilCanvasDimension(contentWidth) : safeWidth,
|
|
1118
1208
|
height: contentHeight > viewport.height + 0.5 ? this._ceilCanvasDimension(contentHeight) : safeHeight,
|
|
@@ -1138,9 +1228,17 @@
|
|
|
1138
1228
|
}
|
|
1139
1229
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1140
1230
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1231
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1232
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1233
|
+
const shouldReserveNoScrollbarMargin = layoutMode === "fit" || layoutMode === "cover";
|
|
1234
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1235
|
+
const margin = hasOppositeScrollbar ? safetyMargin : shouldReserveNoScrollbarMargin ? 1 : 0;
|
|
1236
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1237
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1238
|
+
};
|
|
1141
1239
|
return {
|
|
1142
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1143
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1240
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1241
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1144
1242
|
viewportWidth: effectiveWidth,
|
|
1145
1243
|
viewportHeight: effectiveHeight,
|
|
1146
1244
|
hasHorizontal,
|
|
@@ -1262,6 +1360,45 @@
|
|
|
1262
1360
|
});
|
|
1263
1361
|
}
|
|
1264
1362
|
}
|
|
1363
|
+
_getSerializableStateObjects() {
|
|
1364
|
+
if (!this.canvas) return [];
|
|
1365
|
+
return this.canvas.getObjects().filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1366
|
+
}
|
|
1367
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1368
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1369
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1370
|
+
const numericProperties = [
|
|
1371
|
+
"left",
|
|
1372
|
+
"top",
|
|
1373
|
+
"width",
|
|
1374
|
+
"height",
|
|
1375
|
+
"scaleX",
|
|
1376
|
+
"scaleY",
|
|
1377
|
+
"angle",
|
|
1378
|
+
"skewX",
|
|
1379
|
+
"skewY",
|
|
1380
|
+
"cropX",
|
|
1381
|
+
"cropY",
|
|
1382
|
+
"radius",
|
|
1383
|
+
"rx",
|
|
1384
|
+
"ry",
|
|
1385
|
+
"strokeWidth"
|
|
1386
|
+
];
|
|
1387
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1388
|
+
const fabricObject = fabricObjects[index];
|
|
1389
|
+
if (!serializedObject || !fabricObject) return;
|
|
1390
|
+
numericProperties.forEach((property) => {
|
|
1391
|
+
const numericValue = Number(fabricObject[property]);
|
|
1392
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1393
|
+
});
|
|
1394
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1395
|
+
serializedObject.points = fabricObject.points.map((point) => ({
|
|
1396
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1397
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1398
|
+
}));
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1265
1402
|
_restoreMaskControls(mask) {
|
|
1266
1403
|
if (!mask) return;
|
|
1267
1404
|
const cornerSize = Number(mask.cornerSize);
|
|
@@ -1307,6 +1444,7 @@
|
|
|
1307
1444
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1308
1445
|
if (Array.isArray(jsonObject.objects)) {
|
|
1309
1446
|
jsonObject.objects = jsonObject.objects.filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1447
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1310
1448
|
}
|
|
1311
1449
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1312
1450
|
return JSON.stringify(jsonObject);
|
|
@@ -1381,6 +1519,12 @@
|
|
|
1381
1519
|
if (!Number.isFinite(numericValue)) return false;
|
|
1382
1520
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1383
1521
|
}
|
|
1522
|
+
_hasScaledImageEdge(axis) {
|
|
1523
|
+
if (!this.originalImage) return false;
|
|
1524
|
+
const scale = Number(axis === "y" ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1525
|
+
if (!Number.isFinite(scale)) return false;
|
|
1526
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1527
|
+
}
|
|
1384
1528
|
_getPartialExportEdges(bounds) {
|
|
1385
1529
|
if (!bounds) return null;
|
|
1386
1530
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1389,8 +1533,8 @@
|
|
|
1389
1533
|
return {
|
|
1390
1534
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1391
1535
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1392
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1393
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1536
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge("x"),
|
|
1537
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge("y")
|
|
1394
1538
|
};
|
|
1395
1539
|
}
|
|
1396
1540
|
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
@@ -1450,7 +1594,8 @@
|
|
|
1450
1594
|
* @private
|
|
1451
1595
|
*/
|
|
1452
1596
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1453
|
-
const safeMultiplier =
|
|
1597
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1598
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1454
1599
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1455
1600
|
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1456
1601
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1466,6 +1611,25 @@
|
|
|
1466
1611
|
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1467
1612
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1468
1613
|
}
|
|
1614
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1615
|
+
const numericMultiplier = Number(multiplier);
|
|
1616
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1617
|
+
throw new Error("Export multiplier must be a finite positive number");
|
|
1618
|
+
}
|
|
1619
|
+
return Math.max(1, numericMultiplier);
|
|
1620
|
+
}
|
|
1621
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1622
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1623
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1624
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1625
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1626
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1627
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1628
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0 ? Math.floor(configuredMaxPixels) : 5e7;
|
|
1629
|
+
if (outputPixels > maxPixels) {
|
|
1630
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1469
1633
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1470
1634
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1471
1635
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1483,7 +1647,23 @@
|
|
|
1483
1647
|
_getJpegBackgroundColor() {
|
|
1484
1648
|
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1485
1649
|
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
|
|
1486
|
-
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
|
+
}
|
|
1487
1667
|
}
|
|
1488
1668
|
_isTransparentCssColor(color) {
|
|
1489
1669
|
const normalizedColor = String(color || "").trim().toLowerCase();
|
|
@@ -1510,6 +1690,7 @@
|
|
|
1510
1690
|
}
|
|
1511
1691
|
_decodeBase64Payload(base64Payload) {
|
|
1512
1692
|
const payload = String(base64Payload || "");
|
|
1693
|
+
if (!payload) throw new Error("Data URL base64 payload is empty");
|
|
1513
1694
|
if (typeof atob === "function") {
|
|
1514
1695
|
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1515
1696
|
}
|
|
@@ -1518,6 +1699,13 @@
|
|
|
1518
1699
|
}
|
|
1519
1700
|
throw new Error("Base64 decoding is unavailable");
|
|
1520
1701
|
}
|
|
1702
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
1703
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
1704
|
+
if (!match || !match[2]) {
|
|
1705
|
+
throw new Error("Export produced an invalid or empty base64 data URL");
|
|
1706
|
+
}
|
|
1707
|
+
return this._decodeBase64Payload(match[2]);
|
|
1708
|
+
}
|
|
1521
1709
|
/**
|
|
1522
1710
|
* Gets the top-left corner coordinates of the given object.
|
|
1523
1711
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1627,13 +1815,42 @@
|
|
|
1627
1815
|
const currentHeight = this.canvas.getHeight();
|
|
1628
1816
|
let requiredWidth = currentWidth;
|
|
1629
1817
|
let requiredHeight = currentHeight;
|
|
1630
|
-
|
|
1818
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1819
|
+
const usesScrollableFitBounds = layoutMode === "fit" || layoutMode === "cover";
|
|
1820
|
+
let contentWidth = 0;
|
|
1821
|
+
let contentHeight = 0;
|
|
1822
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1631
1823
|
if (!fabricObject) return;
|
|
1632
1824
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
1633
1825
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1634
|
-
|
|
1635
|
-
|
|
1826
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
1827
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
1828
|
+
contentWidth = Math.max(contentWidth, right);
|
|
1829
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
1830
|
+
return { right, bottom };
|
|
1831
|
+
};
|
|
1832
|
+
fabricObjects.forEach((fabricObject) => {
|
|
1833
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
1834
|
+
if (!bounds) return;
|
|
1835
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
1836
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1636
1837
|
});
|
|
1838
|
+
if (usesScrollableFitBounds) {
|
|
1839
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
1840
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1841
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
1842
|
+
});
|
|
1843
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
1844
|
+
Math.max(1, contentWidth),
|
|
1845
|
+
Math.max(1, contentHeight)
|
|
1846
|
+
);
|
|
1847
|
+
const newWidth2 = contentSize.hasHorizontal ? Math.max(currentWidth, contentSize.width) : contentSize.width;
|
|
1848
|
+
const newHeight2 = contentSize.hasVertical ? Math.max(currentHeight, contentSize.height) : contentSize.height;
|
|
1849
|
+
if (newWidth2 !== currentWidth || newHeight2 !== currentHeight) {
|
|
1850
|
+
this._setCanvasSizeInt(newWidth2, newHeight2);
|
|
1851
|
+
}
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1637
1854
|
let minWidth = 0;
|
|
1638
1855
|
let minHeight = 0;
|
|
1639
1856
|
if (this.containerElement) {
|
|
@@ -1651,16 +1868,60 @@
|
|
|
1651
1868
|
this._reportWarning("expandCanvasToFitObjects: failed to expand canvas", error);
|
|
1652
1869
|
}
|
|
1653
1870
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1871
|
+
_captureImageDisplayBounds() {
|
|
1872
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
1873
|
+
this.originalImage.setCoords();
|
|
1874
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
1875
|
+
const width = Number(bounds && bounds.width);
|
|
1876
|
+
const height = Number(bounds && bounds.height);
|
|
1877
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
1878
|
+
return {
|
|
1879
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
1880
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
1881
|
+
width,
|
|
1882
|
+
height
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
1886
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
1887
|
+
const imageWidth = Number(this.originalImage.width);
|
|
1888
|
+
const imageHeight = Number(this.originalImage.height);
|
|
1889
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
1890
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
1891
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
1892
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
1893
|
+
const left = Number(displayBounds.left) || 0;
|
|
1894
|
+
const top = Number(displayBounds.top) || 0;
|
|
1895
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
1896
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
1897
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
1898
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
1899
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1900
|
+
if (layoutMode === "fit" || layoutMode === "cover") {
|
|
1901
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
1902
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
1903
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
1904
|
+
}
|
|
1905
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
1906
|
+
this._setCanvasSizeInt(
|
|
1907
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
1908
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
this.originalImage.set({
|
|
1912
|
+
originX: "left",
|
|
1913
|
+
originY: "top",
|
|
1914
|
+
left,
|
|
1915
|
+
top,
|
|
1916
|
+
scaleX,
|
|
1917
|
+
scaleY
|
|
1918
|
+
});
|
|
1919
|
+
this.originalImage.setCoords();
|
|
1920
|
+
this.baseImageScale = scaleX;
|
|
1921
|
+
this.currentScale = 1;
|
|
1922
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1923
|
+
this._updateInputs();
|
|
1924
|
+
this.canvas.renderAll();
|
|
1664
1925
|
}
|
|
1665
1926
|
/**
|
|
1666
1927
|
* Scales the original image by a given factor, with animation.
|
|
@@ -1675,7 +1936,14 @@
|
|
|
1675
1936
|
} catch (error) {
|
|
1676
1937
|
return Promise.reject(error);
|
|
1677
1938
|
}
|
|
1678
|
-
return this.animationQueue.add(
|
|
1939
|
+
return this.animationQueue.add(async () => {
|
|
1940
|
+
const operationToken = this._beginBusyOperation("scaleImage");
|
|
1941
|
+
try {
|
|
1942
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
1943
|
+
} finally {
|
|
1944
|
+
this._endBusyOperation(operationToken);
|
|
1945
|
+
}
|
|
1946
|
+
}).finally(() => {
|
|
1679
1947
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1680
1948
|
});
|
|
1681
1949
|
}
|
|
@@ -1718,7 +1986,7 @@
|
|
|
1718
1986
|
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1719
1987
|
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1720
1988
|
}
|
|
1721
|
-
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1989
|
+
if ((this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) && !isOwnInternalOperation) {
|
|
1722
1990
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1723
1991
|
}
|
|
1724
1992
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -1805,10 +2073,12 @@
|
|
|
1805
2073
|
async _scaleImageImpl(factor, options = {}) {
|
|
1806
2074
|
if (!this.originalImage || this._disposed) return;
|
|
1807
2075
|
if (this.isAnimating) return;
|
|
2076
|
+
const numericFactor = Number(factor);
|
|
2077
|
+
if (!Number.isFinite(numericFactor)) return;
|
|
1808
2078
|
const saveHistory = options.saveHistory !== false;
|
|
1809
2079
|
let didStartAnimation = false;
|
|
1810
2080
|
try {
|
|
1811
|
-
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale,
|
|
2081
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
|
|
1812
2082
|
this.currentScale = factor;
|
|
1813
2083
|
this.isAnimating = true;
|
|
1814
2084
|
didStartAnimation = true;
|
|
@@ -1831,7 +2101,7 @@
|
|
|
1831
2101
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1832
2102
|
});
|
|
1833
2103
|
this._updateInputs();
|
|
1834
|
-
if (saveHistory) this.saveState();
|
|
2104
|
+
if (saveHistory) this.saveState(options);
|
|
1835
2105
|
} finally {
|
|
1836
2106
|
if (didStartAnimation) {
|
|
1837
2107
|
this.isAnimating = false;
|
|
@@ -1853,7 +2123,14 @@
|
|
|
1853
2123
|
} catch (error) {
|
|
1854
2124
|
return Promise.reject(error);
|
|
1855
2125
|
}
|
|
1856
|
-
return this.animationQueue.add(
|
|
2126
|
+
return this.animationQueue.add(async () => {
|
|
2127
|
+
const operationToken = this._beginBusyOperation("rotateImage");
|
|
2128
|
+
try {
|
|
2129
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2130
|
+
} finally {
|
|
2131
|
+
this._endBusyOperation(operationToken);
|
|
2132
|
+
}
|
|
2133
|
+
}).finally(() => {
|
|
1857
2134
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1858
2135
|
});
|
|
1859
2136
|
}
|
|
@@ -1867,7 +2144,8 @@
|
|
|
1867
2144
|
async _rotateImageImpl(degrees, options = {}) {
|
|
1868
2145
|
if (!this.originalImage || this._disposed) return;
|
|
1869
2146
|
if (this.isAnimating) return;
|
|
1870
|
-
|
|
2147
|
+
const numericDegrees = Number(degrees);
|
|
2148
|
+
if (!Number.isFinite(numericDegrees)) return;
|
|
1871
2149
|
const saveHistory = options.saveHistory !== false;
|
|
1872
2150
|
const image = this.originalImage;
|
|
1873
2151
|
const previousOriginX = image.originX || "left";
|
|
@@ -1876,6 +2154,7 @@
|
|
|
1876
2154
|
let didStartAnimation = false;
|
|
1877
2155
|
let didCompleteRotation = false;
|
|
1878
2156
|
try {
|
|
2157
|
+
degrees = numericDegrees;
|
|
1879
2158
|
this.currentRotation = degrees;
|
|
1880
2159
|
this.isAnimating = true;
|
|
1881
2160
|
didStartAnimation = true;
|
|
@@ -1896,7 +2175,7 @@
|
|
|
1896
2175
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1897
2176
|
});
|
|
1898
2177
|
this._updateInputs();
|
|
1899
|
-
if (saveHistory) this.saveState();
|
|
2178
|
+
if (saveHistory) this.saveState(options);
|
|
1900
2179
|
didCompleteRotation = true;
|
|
1901
2180
|
} finally {
|
|
1902
2181
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -1923,19 +2202,22 @@
|
|
|
1923
2202
|
return Promise.reject(error);
|
|
1924
2203
|
}
|
|
1925
2204
|
return this.animationQueue.add(async () => {
|
|
2205
|
+
const operationToken = this._beginBusyOperation("resetImageTransform");
|
|
1926
2206
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1927
2207
|
try {
|
|
1928
|
-
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1929
|
-
await this._rotateImageImpl(0, { saveHistory: false });
|
|
2208
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2209
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
1930
2210
|
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1931
2211
|
this._pushStateTransition(before, after);
|
|
1932
2212
|
} catch (error) {
|
|
1933
2213
|
try {
|
|
1934
|
-
await this.loadFromState(before);
|
|
2214
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
1935
2215
|
} catch (restoreError) {
|
|
1936
2216
|
this._reportError("resetImageTransform rollback failed", restoreError);
|
|
1937
2217
|
}
|
|
1938
2218
|
throw error;
|
|
2219
|
+
} finally {
|
|
2220
|
+
this._endBusyOperation(operationToken);
|
|
1939
2221
|
}
|
|
1940
2222
|
}).finally(() => {
|
|
1941
2223
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
@@ -1960,8 +2242,13 @@
|
|
|
1960
2242
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
1961
2243
|
* @public
|
|
1962
2244
|
*/
|
|
1963
|
-
loadFromState(serializedState) {
|
|
2245
|
+
loadFromState(serializedState, options = {}) {
|
|
1964
2246
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2247
|
+
try {
|
|
2248
|
+
this._assertIdleForOperation("loadFromState", options);
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
return Promise.reject(error);
|
|
2251
|
+
}
|
|
1965
2252
|
if (this._cropMode || this._cropRect) {
|
|
1966
2253
|
this._removeCropRect();
|
|
1967
2254
|
this._restoreCropObjectState();
|
|
@@ -2118,22 +2405,29 @@
|
|
|
2118
2405
|
* @returns {void}
|
|
2119
2406
|
* @public
|
|
2120
2407
|
*/
|
|
2121
|
-
saveState() {
|
|
2408
|
+
saveState(options = {}) {
|
|
2122
2409
|
if (!this.canvas) return;
|
|
2410
|
+
try {
|
|
2411
|
+
this._assertIdleForOperation("saveState", options);
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
this._reportError("saveState blocked", error);
|
|
2414
|
+
this._updateUI();
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2123
2417
|
try {
|
|
2124
2418
|
const after = this._captureCanvasStateOrThrow("saveState");
|
|
2125
2419
|
const before = this._lastSnapshot || after;
|
|
2126
2420
|
if (after === before) return;
|
|
2127
2421
|
let executedOnce = false;
|
|
2128
2422
|
const command = new Command(
|
|
2129
|
-
() => {
|
|
2423
|
+
(commandOptions = {}) => {
|
|
2130
2424
|
if (executedOnce) {
|
|
2131
|
-
return this.loadFromState(after);
|
|
2425
|
+
return this.loadFromState(after, commandOptions);
|
|
2132
2426
|
}
|
|
2133
2427
|
executedOnce = true;
|
|
2134
2428
|
return void 0;
|
|
2135
2429
|
},
|
|
2136
|
-
() => this.loadFromState(before)
|
|
2430
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2137
2431
|
);
|
|
2138
2432
|
this.historyManager.execute(command);
|
|
2139
2433
|
this._lastSnapshot = after;
|
|
@@ -2162,8 +2456,8 @@
|
|
|
2162
2456
|
if (before === after) return;
|
|
2163
2457
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2164
2458
|
const command = new Command(
|
|
2165
|
-
() => this.loadFromState(after),
|
|
2166
|
-
() => this.loadFromState(before)
|
|
2459
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2460
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2167
2461
|
);
|
|
2168
2462
|
this.historyManager.push(command);
|
|
2169
2463
|
this._lastSnapshot = after;
|
|
@@ -2176,8 +2470,16 @@
|
|
|
2176
2470
|
* @public
|
|
2177
2471
|
*/
|
|
2178
2472
|
undo() {
|
|
2179
|
-
|
|
2473
|
+
try {
|
|
2474
|
+
this._assertIdleForOperation("undo");
|
|
2475
|
+
} catch (error) {
|
|
2476
|
+
return Promise.reject(error);
|
|
2477
|
+
}
|
|
2478
|
+
const operationToken = this._beginBusyOperation("undo");
|
|
2479
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2180
2480
|
this._updateUI();
|
|
2481
|
+
}).finally(() => {
|
|
2482
|
+
this._endBusyOperation(operationToken);
|
|
2181
2483
|
}).catch((error) => {
|
|
2182
2484
|
this._reportError("undo failed", error);
|
|
2183
2485
|
throw error;
|
|
@@ -2190,8 +2492,16 @@
|
|
|
2190
2492
|
* @public
|
|
2191
2493
|
*/
|
|
2192
2494
|
redo() {
|
|
2193
|
-
|
|
2495
|
+
try {
|
|
2496
|
+
this._assertIdleForOperation("redo");
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
return Promise.reject(error);
|
|
2499
|
+
}
|
|
2500
|
+
const operationToken = this._beginBusyOperation("redo");
|
|
2501
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2194
2502
|
this._updateUI();
|
|
2503
|
+
}).finally(() => {
|
|
2504
|
+
this._endBusyOperation(operationToken);
|
|
2195
2505
|
}).catch((error) => {
|
|
2196
2506
|
this._reportError("redo failed", error);
|
|
2197
2507
|
throw error;
|
|
@@ -2307,30 +2617,64 @@
|
|
|
2307
2617
|
}
|
|
2308
2618
|
return value != null ? value : fallback;
|
|
2309
2619
|
};
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2620
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
2621
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
2622
|
+
return null;
|
|
2623
|
+
};
|
|
2624
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
2625
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
2626
|
+
const numericValue = Number(resolvedValue);
|
|
2627
|
+
if (!Number.isFinite(numericValue)) {
|
|
2628
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
2629
|
+
}
|
|
2630
|
+
if (constraints.positive && numericValue <= 0) {
|
|
2631
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
2632
|
+
}
|
|
2633
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
2634
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
2635
|
+
}
|
|
2636
|
+
return numericValue;
|
|
2637
|
+
};
|
|
2638
|
+
try {
|
|
2639
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, "width", "gap", { nonNegative: true });
|
|
2640
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, "width", "width", { positive: true });
|
|
2641
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, "height", "height", { positive: true });
|
|
2642
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, "width", "angle");
|
|
2643
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, "width", "alpha")));
|
|
2644
|
+
if (maskConfig.left === void 0 && this._lastMask) {
|
|
2645
|
+
const previousMask = this._lastMask;
|
|
2646
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
2647
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2648
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2649
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
2650
|
+
} else {
|
|
2651
|
+
left = resolveNumber(maskConfig.left, firstOffset, "width", "left");
|
|
2652
|
+
top = resolveNumber(maskConfig.top, firstOffset, "height", "top");
|
|
2653
|
+
}
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
return rejectInvalidMask("invalid numeric configuration", error);
|
|
2319
2656
|
}
|
|
2320
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
2321
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height");
|
|
2322
2657
|
maskConfig.left = left;
|
|
2323
2658
|
maskConfig.top = top;
|
|
2324
2659
|
let mask;
|
|
2325
2660
|
if (typeof maskConfig.fabricGenerator === "function") {
|
|
2326
|
-
|
|
2661
|
+
try {
|
|
2662
|
+
mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
|
|
2663
|
+
} catch (error) {
|
|
2664
|
+
return rejectInvalidMask("fabricGenerator failed", error);
|
|
2665
|
+
}
|
|
2327
2666
|
} else {
|
|
2328
2667
|
switch (shapeType) {
|
|
2329
2668
|
case "circle":
|
|
2669
|
+
try {
|
|
2670
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, "min", "radius", { positive: true });
|
|
2671
|
+
} catch (error) {
|
|
2672
|
+
return rejectInvalidMask("invalid circle radius", error);
|
|
2673
|
+
}
|
|
2330
2674
|
mask = new fabric.Circle({
|
|
2331
2675
|
left,
|
|
2332
2676
|
top,
|
|
2333
|
-
radius:
|
|
2677
|
+
radius: maskConfig.radius,
|
|
2334
2678
|
fill: maskConfig.color,
|
|
2335
2679
|
opacity: maskConfig.alpha,
|
|
2336
2680
|
angle: maskConfig.angle,
|
|
@@ -2338,11 +2682,17 @@
|
|
|
2338
2682
|
});
|
|
2339
2683
|
break;
|
|
2340
2684
|
case "ellipse":
|
|
2685
|
+
try {
|
|
2686
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, "width", "rx", { positive: true });
|
|
2687
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, "height", "ry", { positive: true });
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
return rejectInvalidMask("invalid ellipse radius", error);
|
|
2690
|
+
}
|
|
2341
2691
|
mask = new fabric.Ellipse({
|
|
2342
2692
|
left,
|
|
2343
2693
|
top,
|
|
2344
|
-
rx:
|
|
2345
|
-
ry:
|
|
2694
|
+
rx: maskConfig.rx,
|
|
2695
|
+
ry: maskConfig.ry,
|
|
2346
2696
|
fill: maskConfig.color,
|
|
2347
2697
|
opacity: maskConfig.alpha,
|
|
2348
2698
|
angle: maskConfig.angle,
|
|
@@ -2351,8 +2701,31 @@
|
|
|
2351
2701
|
break;
|
|
2352
2702
|
case "polygon": {
|
|
2353
2703
|
let polygonPoints = maskConfig.points || [];
|
|
2354
|
-
if (Array.isArray(polygonPoints)
|
|
2355
|
-
|
|
2704
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
2705
|
+
return rejectInvalidMask("polygon masks require at least three points");
|
|
2706
|
+
}
|
|
2707
|
+
try {
|
|
2708
|
+
polygonPoints = polygonPoints.map((point) => {
|
|
2709
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
2710
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
2711
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
2712
|
+
throw new Error("polygon point coordinates must be finite numbers");
|
|
2713
|
+
}
|
|
2714
|
+
return { x, y };
|
|
2715
|
+
});
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
return rejectInvalidMask("invalid polygon points", error);
|
|
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");
|
|
2356
2729
|
}
|
|
2357
2730
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2358
2731
|
left,
|
|
@@ -2366,11 +2739,17 @@
|
|
|
2366
2739
|
}
|
|
2367
2740
|
case "rect":
|
|
2368
2741
|
default:
|
|
2742
|
+
try {
|
|
2743
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, "width", "rx", { nonNegative: true });
|
|
2744
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, "height", "ry", { nonNegative: true });
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
return rejectInvalidMask("invalid rectangle corner radius", error);
|
|
2747
|
+
}
|
|
2369
2748
|
mask = new fabric.Rect({
|
|
2370
2749
|
left,
|
|
2371
2750
|
top,
|
|
2372
|
-
width:
|
|
2373
|
-
height:
|
|
2751
|
+
width: maskConfig.width,
|
|
2752
|
+
height: maskConfig.height,
|
|
2374
2753
|
fill: maskConfig.color,
|
|
2375
2754
|
opacity: maskConfig.alpha,
|
|
2376
2755
|
angle: maskConfig.angle,
|
|
@@ -2408,10 +2787,10 @@
|
|
|
2408
2787
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2409
2788
|
});
|
|
2410
2789
|
this._rebindMaskEvents(mask);
|
|
2411
|
-
this.
|
|
2790
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2412
2791
|
this._lastMaskInitialLeft = left;
|
|
2413
2792
|
this._lastMaskInitialTop = top;
|
|
2414
|
-
this._lastMaskInitialWidth =
|
|
2793
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2415
2794
|
const maskId = ++this.maskCounter;
|
|
2416
2795
|
mask.set({
|
|
2417
2796
|
maskId,
|
|
@@ -2426,7 +2805,12 @@
|
|
|
2426
2805
|
this._updateUI();
|
|
2427
2806
|
this.canvas.renderAll();
|
|
2428
2807
|
this.saveState();
|
|
2429
|
-
if (typeof maskConfig.onCreate === "function")
|
|
2808
|
+
if (typeof maskConfig.onCreate === "function") {
|
|
2809
|
+
this._emitSafeCallback(
|
|
2810
|
+
() => maskConfig.onCreate(mask, this.canvas),
|
|
2811
|
+
"createMask onCreate callback failed"
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2430
2814
|
return mask;
|
|
2431
2815
|
}
|
|
2432
2816
|
/**
|
|
@@ -2630,8 +3014,15 @@
|
|
|
2630
3014
|
this._removeLabelForMask(mask);
|
|
2631
3015
|
let textObject = null;
|
|
2632
3016
|
if (this.options.label && typeof this.options.label.create === "function") {
|
|
2633
|
-
|
|
2634
|
-
|
|
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")) {
|
|
2635
3026
|
this._reportWarning("label.create() returned an invalid Fabric object; using the default label");
|
|
2636
3027
|
textObject = null;
|
|
2637
3028
|
}
|
|
@@ -2652,7 +3043,12 @@
|
|
|
2652
3043
|
};
|
|
2653
3044
|
if (this.options.label) {
|
|
2654
3045
|
if (typeof this.options.label.getText === "function") {
|
|
2655
|
-
|
|
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
|
+
}
|
|
2656
3052
|
}
|
|
2657
3053
|
if (this.options.label.textOptions) {
|
|
2658
3054
|
Object.assign(textOptions, this.options.label.textOptions);
|
|
@@ -2839,6 +3235,7 @@
|
|
|
2839
3235
|
this._assertIdleForOperation("mergeMasks");
|
|
2840
3236
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2841
3237
|
if (!masks.length) return;
|
|
3238
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
2842
3239
|
const beforeJson = this._serializeCanvasState();
|
|
2843
3240
|
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2844
3241
|
this.canvas.discardActiveObject();
|
|
@@ -2857,12 +3254,13 @@
|
|
|
2857
3254
|
preserveScroll: true,
|
|
2858
3255
|
resetMaskCounter: false
|
|
2859
3256
|
}));
|
|
3257
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
2860
3258
|
const afterJson = this._serializeCanvasState();
|
|
2861
3259
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2862
3260
|
} catch (error) {
|
|
2863
3261
|
this._reportError("merge error", error);
|
|
2864
3262
|
try {
|
|
2865
|
-
await this.loadFromState(beforeJson);
|
|
3263
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
2866
3264
|
} catch (restoreError) {
|
|
2867
3265
|
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2868
3266
|
}
|
|
@@ -2919,24 +3317,65 @@
|
|
|
2919
3317
|
*/
|
|
2920
3318
|
async exportImageBase64(options = {}) {
|
|
2921
3319
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3320
|
+
options = options || {};
|
|
2922
3321
|
this._assertIdleForOperation("exportImageBase64", options);
|
|
3322
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3323
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageBase64");
|
|
2923
3324
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2924
3325
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2925
3326
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2926
3327
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3328
|
+
try {
|
|
3329
|
+
if (!exportImageArea) {
|
|
3330
|
+
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
3331
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3332
|
+
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
3333
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
3334
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
3335
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
3336
|
+
try {
|
|
3337
|
+
masks2.forEach((mask) => {
|
|
3338
|
+
mask.set({ visible: false });
|
|
3339
|
+
});
|
|
3340
|
+
this.canvas.discardActiveObject();
|
|
3341
|
+
this.canvas.renderAll();
|
|
3342
|
+
this.originalImage.setCoords();
|
|
3343
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3344
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3345
|
+
return await this._exportCanvasRegionToDataURL({
|
|
3346
|
+
...exportRegion,
|
|
3347
|
+
multiplier,
|
|
3348
|
+
quality,
|
|
3349
|
+
format,
|
|
3350
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3351
|
+
});
|
|
3352
|
+
} finally {
|
|
3353
|
+
maskVisibilityBackups.forEach((backup) => {
|
|
3354
|
+
try {
|
|
3355
|
+
backup.object.set({ visible: backup.visible });
|
|
3356
|
+
} catch (error) {
|
|
3357
|
+
void error;
|
|
3358
|
+
}
|
|
3359
|
+
});
|
|
3360
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
3361
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
3362
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3363
|
+
this.canvas.renderAll();
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3367
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3368
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3369
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2934
3370
|
try {
|
|
2935
|
-
|
|
2936
|
-
mask.set({ visible: false });
|
|
2937
|
-
});
|
|
3371
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2938
3372
|
this.canvas.discardActiveObject();
|
|
2939
3373
|
this.canvas.renderAll();
|
|
3374
|
+
masks.forEach((mask) => {
|
|
3375
|
+
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
3376
|
+
mask.setCoords();
|
|
3377
|
+
});
|
|
3378
|
+
this.canvas.renderAll();
|
|
2940
3379
|
this.originalImage.setCoords();
|
|
2941
3380
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2942
3381
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
@@ -2948,47 +3387,13 @@
|
|
|
2948
3387
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2949
3388
|
});
|
|
2950
3389
|
} finally {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
} catch (error) {
|
|
2955
|
-
void error;
|
|
2956
|
-
}
|
|
2957
|
-
});
|
|
2958
|
-
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2959
|
-
this._restoreMaskLabelBackups(labelBackups2);
|
|
2960
|
-
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3390
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3391
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3392
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2961
3393
|
this.canvas.renderAll();
|
|
2962
3394
|
}
|
|
2963
|
-
}
|
|
2964
|
-
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2965
|
-
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2966
|
-
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2967
|
-
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2968
|
-
try {
|
|
2969
|
-
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2970
|
-
this.canvas.discardActiveObject();
|
|
2971
|
-
this.canvas.renderAll();
|
|
2972
|
-
masks.forEach((mask) => {
|
|
2973
|
-
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
2974
|
-
mask.setCoords();
|
|
2975
|
-
});
|
|
2976
|
-
this.canvas.renderAll();
|
|
2977
|
-
this.originalImage.setCoords();
|
|
2978
|
-
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2979
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2980
|
-
return await this._exportCanvasRegionToDataURL({
|
|
2981
|
-
...exportRegion,
|
|
2982
|
-
multiplier,
|
|
2983
|
-
quality,
|
|
2984
|
-
format,
|
|
2985
|
-
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2986
|
-
});
|
|
2987
3395
|
} finally {
|
|
2988
|
-
this.
|
|
2989
|
-
this._restoreMaskLabelBackups(labelBackups);
|
|
2990
|
-
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2991
|
-
this.canvas.renderAll();
|
|
3396
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2992
3397
|
}
|
|
2993
3398
|
}
|
|
2994
3399
|
/**
|
|
@@ -3021,7 +3426,10 @@
|
|
|
3021
3426
|
*/
|
|
3022
3427
|
async exportImageFile(options = {}) {
|
|
3023
3428
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3024
|
-
|
|
3429
|
+
options = options || {};
|
|
3430
|
+
this._assertIdleForOperation("exportImageFile", options);
|
|
3431
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3432
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageFile");
|
|
3025
3433
|
const {
|
|
3026
3434
|
mergeMask = true,
|
|
3027
3435
|
fileType = "jpeg",
|
|
@@ -3031,48 +3439,52 @@
|
|
|
3031
3439
|
} = options;
|
|
3032
3440
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3033
3441
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
3034
|
-
|
|
3035
|
-
|
|
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
|
-
|
|
3442
|
+
try {
|
|
3443
|
+
let imageBase64;
|
|
3444
|
+
if (mergeMask) {
|
|
3445
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3446
|
+
exportImageArea: true,
|
|
3447
|
+
multiplier,
|
|
3448
|
+
quality: normalizedQuality,
|
|
3449
|
+
fileType: safeFileType
|
|
3450
|
+
}));
|
|
3451
|
+
} else {
|
|
3452
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3453
|
+
exportImageArea: false,
|
|
3454
|
+
multiplier,
|
|
3455
|
+
quality: normalizedQuality,
|
|
3456
|
+
fileType: safeFileType
|
|
3457
|
+
}));
|
|
3458
|
+
}
|
|
3459
|
+
let imageDataUrl = imageBase64;
|
|
3460
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3461
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3462
|
+
const imageElement = new window.Image();
|
|
3463
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3464
|
+
imageElement.onload = () => {
|
|
3465
|
+
try {
|
|
3466
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
3467
|
+
offscreenCanvas.width = imageElement.width;
|
|
3468
|
+
offscreenCanvas.height = imageElement.height;
|
|
3469
|
+
const context = offscreenCanvas.getContext("2d");
|
|
3470
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
3471
|
+
context.drawImage(imageElement, 0, 0);
|
|
3472
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3473
|
+
resolve(convertedDataUrl);
|
|
3474
|
+
} catch (error) {
|
|
3475
|
+
reject(error);
|
|
3476
|
+
}
|
|
3477
|
+
};
|
|
3478
|
+
imageElement.onerror = reject;
|
|
3479
|
+
imageElement.src = imageBase64;
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3483
|
+
const mime = `image/${safeFileType}`;
|
|
3484
|
+
return new File([bytes], fileName, { type: mime });
|
|
3485
|
+
} finally {
|
|
3486
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3072
3487
|
}
|
|
3073
|
-
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
3074
|
-
const mime = `image/${safeFileType}`;
|
|
3075
|
-
return new File([bytes], fileName, { type: mime });
|
|
3076
3488
|
}
|
|
3077
3489
|
_clearMaskPlacementMemory() {
|
|
3078
3490
|
this._lastMask = null;
|
|
@@ -3080,7 +3492,7 @@
|
|
|
3080
3492
|
this._lastMaskInitialTop = null;
|
|
3081
3493
|
this._lastMaskInitialWidth = null;
|
|
3082
3494
|
}
|
|
3083
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
3495
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
3084
3496
|
this._reportError(message, error);
|
|
3085
3497
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
3086
3498
|
this._cropRect = null;
|
|
@@ -3091,7 +3503,7 @@
|
|
|
3091
3503
|
this._prevSelectionSetting = void 0;
|
|
3092
3504
|
if (beforeJson) {
|
|
3093
3505
|
try {
|
|
3094
|
-
await this.loadFromState(beforeJson);
|
|
3506
|
+
await this.loadFromState(beforeJson, options);
|
|
3095
3507
|
} catch (restoreError) {
|
|
3096
3508
|
this._reportError("applyCrop: rollback failed", restoreError);
|
|
3097
3509
|
}
|
|
@@ -3137,6 +3549,49 @@
|
|
|
3137
3549
|
this._cropRect = null;
|
|
3138
3550
|
this._cropHandlers = [];
|
|
3139
3551
|
}
|
|
3552
|
+
_getCropRectContentBounds(cropRect) {
|
|
3553
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
3554
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
3555
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
3556
|
+
return {
|
|
3557
|
+
left: Number(cropRect.left) || 0,
|
|
3558
|
+
top: Number(cropRect.top) || 0,
|
|
3559
|
+
width,
|
|
3560
|
+
height
|
|
3561
|
+
};
|
|
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
|
+
}
|
|
3140
3595
|
/**
|
|
3141
3596
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
3142
3597
|
*
|
|
@@ -3148,6 +3603,10 @@
|
|
|
3148
3603
|
*/
|
|
3149
3604
|
enterCropMode() {
|
|
3150
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
|
+
}
|
|
3151
3610
|
if (!this._canMutateNow("enterCropMode")) return;
|
|
3152
3611
|
if (!this.isImageLoaded()) return;
|
|
3153
3612
|
this._removeCropRect();
|
|
@@ -3160,14 +3619,19 @@
|
|
|
3160
3619
|
const padding = this.options.crop && this.options.crop.padding ? this.options.crop.padding : 10;
|
|
3161
3620
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
3162
3621
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
3163
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
3164
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
3622
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
3623
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
3165
3624
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
3166
3625
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
3167
3626
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3168
3627
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3169
3628
|
const width = minCropWidth;
|
|
3170
3629
|
const height = minCropHeight;
|
|
3630
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
3631
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
3632
|
+
this._cropRotationWarningEmitted = true;
|
|
3633
|
+
this._reportWarning("crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported");
|
|
3634
|
+
}
|
|
3171
3635
|
const cropRect = new fabric.Rect({
|
|
3172
3636
|
left,
|
|
3173
3637
|
top,
|
|
@@ -3179,8 +3643,8 @@
|
|
|
3179
3643
|
strokeWidth: 1,
|
|
3180
3644
|
strokeUniform: true,
|
|
3181
3645
|
selectable: true,
|
|
3182
|
-
hasRotatingPoint:
|
|
3183
|
-
lockRotation:
|
|
3646
|
+
hasRotatingPoint: false,
|
|
3647
|
+
lockRotation: true,
|
|
3184
3648
|
cornerSize: 8,
|
|
3185
3649
|
objectCaching: false,
|
|
3186
3650
|
originX: "left",
|
|
@@ -3217,7 +3681,7 @@
|
|
|
3217
3681
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3218
3682
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3219
3683
|
cropRect.setCoords();
|
|
3220
|
-
const cropBounds =
|
|
3684
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3221
3685
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3222
3686
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3223
3687
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3267,6 +3731,10 @@
|
|
|
3267
3731
|
* @public
|
|
3268
3732
|
*/
|
|
3269
3733
|
cancelCrop() {
|
|
3734
|
+
if (this._isApplyingCrop) {
|
|
3735
|
+
this._reportWarning("cancelCrop ignored because a crop is already being applied");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3270
3738
|
if (!this.canvas || !this._cropMode) return;
|
|
3271
3739
|
this._removeCropRect();
|
|
3272
3740
|
this._restoreCropObjectState();
|
|
@@ -3290,95 +3758,120 @@
|
|
|
3290
3758
|
*/
|
|
3291
3759
|
async applyCrop() {
|
|
3292
3760
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3293
|
-
this.
|
|
3294
|
-
|
|
3295
|
-
const rectBounds = this._cropRect.getBoundingRect(true, true);
|
|
3296
|
-
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3297
|
-
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3298
|
-
this._restoreCropObjectState();
|
|
3299
|
-
let beforeJson;
|
|
3300
|
-
try {
|
|
3301
|
-
beforeJson = this._serializeCanvasState();
|
|
3302
|
-
} catch (error) {
|
|
3303
|
-
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3304
|
-
beforeJson = null;
|
|
3305
|
-
}
|
|
3306
|
-
if (!beforeJson) {
|
|
3307
|
-
this.cancelCrop();
|
|
3761
|
+
if (this._isApplyingCrop) {
|
|
3762
|
+
this._reportWarning("applyCrop ignored because a crop is already being applied");
|
|
3308
3763
|
return;
|
|
3309
3764
|
}
|
|
3310
|
-
|
|
3765
|
+
this._assertIdleForOperation("applyCrop");
|
|
3766
|
+
this._isApplyingCrop = true;
|
|
3767
|
+
const operationToken = this._beginBusyOperation("applyCrop");
|
|
3768
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3311
3769
|
try {
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
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
|
+
}
|
|
3778
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3779
|
+
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3780
|
+
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3781
|
+
this._restoreCropObjectState();
|
|
3782
|
+
let beforeJson;
|
|
3783
|
+
try {
|
|
3784
|
+
beforeJson = this._serializeCanvasState();
|
|
3785
|
+
} catch (error) {
|
|
3786
|
+
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3787
|
+
beforeJson = null;
|
|
3788
|
+
}
|
|
3789
|
+
if (!beforeJson) {
|
|
3790
|
+
this._removeCropRect();
|
|
3791
|
+
this._cropMode = false;
|
|
3792
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3793
|
+
this._prevSelectionSetting = void 0;
|
|
3328
3794
|
this.canvas.discardActiveObject();
|
|
3795
|
+
this._updateUI();
|
|
3329
3796
|
this.canvas.renderAll();
|
|
3797
|
+
return;
|
|
3330
3798
|
}
|
|
3331
|
-
|
|
3332
|
-
|
|
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
|
-
|
|
3799
|
+
const preservedMasks = [];
|
|
3800
|
+
try {
|
|
3801
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3802
|
+
if (masks && masks.length) {
|
|
3803
|
+
masks.forEach((mask) => {
|
|
3804
|
+
mask.setCoords();
|
|
3805
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3806
|
+
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;
|
|
3807
|
+
this._removeLabelForMask(mask);
|
|
3808
|
+
this._cleanupMaskEvents(mask);
|
|
3809
|
+
this.canvas.remove(mask);
|
|
3810
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3811
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3812
|
+
mask.set({ visible: true });
|
|
3813
|
+
preservedMasks.push(mask);
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
this._clearMaskPlacementMemory();
|
|
3817
|
+
this.canvas.discardActiveObject();
|
|
3818
|
+
this.canvas.renderAll();
|
|
3819
|
+
}
|
|
3820
|
+
} catch (error) {
|
|
3821
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error, internalOptions);
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
this._removeCropRect();
|
|
3825
|
+
this._cropMode = false;
|
|
3826
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3827
|
+
this._prevSelectionSetting = void 0;
|
|
3828
|
+
let croppedBase64;
|
|
3829
|
+
try {
|
|
3830
|
+
croppedBase64 = await this._exportCanvasRegionToDataURL({
|
|
3831
|
+
...cropRegion,
|
|
3832
|
+
multiplier: 1,
|
|
3833
|
+
quality: this._normalizeQuality(this.options.downsampleQuality),
|
|
3834
|
+
format: "jpeg"
|
|
3358
3835
|
});
|
|
3359
|
-
|
|
3360
|
-
this.
|
|
3361
|
-
|
|
3362
|
-
this.canvas.renderAll();
|
|
3836
|
+
} catch (error) {
|
|
3837
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to create cropped image", error, internalOptions);
|
|
3838
|
+
return;
|
|
3363
3839
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3840
|
+
try {
|
|
3841
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3842
|
+
if (preservedMasks.length) {
|
|
3843
|
+
preservedMasks.forEach((mask) => {
|
|
3844
|
+
this._rebindMaskEvents(mask);
|
|
3845
|
+
this.canvas.add(mask);
|
|
3846
|
+
this.canvas.bringToFront(mask);
|
|
3847
|
+
});
|
|
3848
|
+
this._lastMask = preservedMasks[preservedMasks.length - 1];
|
|
3849
|
+
this.maskCounter = preservedMasks.reduce((max, mask) => Math.max(max, mask.maskId || 0), this.maskCounter);
|
|
3850
|
+
this._updateMaskList();
|
|
3851
|
+
this.canvas.renderAll();
|
|
3852
|
+
}
|
|
3853
|
+
} catch (error) {
|
|
3854
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", error, internalOptions);
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
let afterJson;
|
|
3858
|
+
try {
|
|
3859
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
3860
|
+
} catch (error) {
|
|
3861
|
+
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
3862
|
+
afterJson = null;
|
|
3863
|
+
}
|
|
3864
|
+
try {
|
|
3865
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
3866
|
+
} catch (error) {
|
|
3867
|
+
this._reportWarning("applyCrop: failed to push history command", error);
|
|
3868
|
+
}
|
|
3869
|
+
this._updateUI();
|
|
3870
|
+
this.canvas.renderAll();
|
|
3871
|
+
} finally {
|
|
3872
|
+
this._isApplyingCrop = false;
|
|
3873
|
+
this._endBusyOperation(operationToken);
|
|
3379
3874
|
}
|
|
3380
|
-
this._updateUI();
|
|
3381
|
-
this.canvas.renderAll();
|
|
3382
3875
|
}
|
|
3383
3876
|
/* ---------- Misc / UI ---------- */
|
|
3384
3877
|
/**
|
|
@@ -3408,9 +3901,11 @@
|
|
|
3408
3901
|
const isInCropMode = !!this._cropMode;
|
|
3409
3902
|
const isBusy = this.isBusy();
|
|
3410
3903
|
if (isInCropMode) {
|
|
3904
|
+
const cropInteractionKeys = /* @__PURE__ */ new Set(["canvas", "canvasContainer", "imagePlaceholder", "imgPlaceholder"]);
|
|
3411
3905
|
for (const key of Object.keys(this.elements || {})) {
|
|
3412
3906
|
const element = this._getElement(key);
|
|
3413
3907
|
if (!element) continue;
|
|
3908
|
+
if (cropInteractionKeys.has(key)) continue;
|
|
3414
3909
|
if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3415
3910
|
this._setDisabled(key, false);
|
|
3416
3911
|
} else {
|
|
@@ -3448,9 +3943,44 @@
|
|
|
3448
3943
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3449
3944
|
* @private
|
|
3450
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
|
+
}
|
|
3451
3980
|
_setDisabled(key, disabled) {
|
|
3452
3981
|
const element = this._getElement(key);
|
|
3453
3982
|
if (!element) return;
|
|
3983
|
+
this._rememberElementDisabledState(key, element);
|
|
3454
3984
|
if ("disabled" in element) {
|
|
3455
3985
|
element.disabled = !!disabled;
|
|
3456
3986
|
return;
|
|
@@ -3477,7 +4007,6 @@
|
|
|
3477
4007
|
* @private
|
|
3478
4008
|
*/
|
|
3479
4009
|
_updatePlaceholderStatus() {
|
|
3480
|
-
if (!this.options.showPlaceholder) return;
|
|
3481
4010
|
this._setPlaceholderVisible(!this.originalImage);
|
|
3482
4011
|
}
|
|
3483
4012
|
/**
|
|
@@ -3487,10 +4016,11 @@
|
|
|
3487
4016
|
* @private
|
|
3488
4017
|
*/
|
|
3489
4018
|
_setPlaceholderVisible(show) {
|
|
3490
|
-
|
|
4019
|
+
const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
|
|
4020
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
|
|
3491
4021
|
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
3492
4022
|
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
3493
|
-
this._setElementVisible(canvasVisibilityElement, !
|
|
4023
|
+
this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
|
|
3494
4024
|
}
|
|
3495
4025
|
}
|
|
3496
4026
|
_getCanvasVisibilityElement() {
|
|
@@ -3569,6 +4099,12 @@
|
|
|
3569
4099
|
void error;
|
|
3570
4100
|
}
|
|
3571
4101
|
if (this._cropRect) this._removeCropRect();
|
|
4102
|
+
this._isApplyingCrop = false;
|
|
4103
|
+
try {
|
|
4104
|
+
this._restoreElementDisabledStates();
|
|
4105
|
+
} catch (error) {
|
|
4106
|
+
void error;
|
|
4107
|
+
}
|
|
3572
4108
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3573
4109
|
try {
|
|
3574
4110
|
this._restoreContainerOverflowState();
|
|
@@ -3616,6 +4152,7 @@
|
|
|
3616
4152
|
this._handlersByElementKey = {};
|
|
3617
4153
|
this._elementCache = {};
|
|
3618
4154
|
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
4155
|
+
this._elementOriginalDisabledState = /* @__PURE__ */ new Map();
|
|
3619
4156
|
this._clearMaskPlacementMemory();
|
|
3620
4157
|
this.originalImage = null;
|
|
3621
4158
|
this.baseImageScale = 1;
|
|
@@ -3624,6 +4161,7 @@
|
|
|
3624
4161
|
this.isAnimating = false;
|
|
3625
4162
|
this._isLoading = false;
|
|
3626
4163
|
this._cropMode = false;
|
|
4164
|
+
this._isApplyingCrop = false;
|
|
3627
4165
|
this._cropRect = null;
|
|
3628
4166
|
this._cropHandlers = [];
|
|
3629
4167
|
this._cropPrevEvented = null;
|
|
@@ -3726,9 +4264,10 @@
|
|
|
3726
4264
|
* @param {number} [maxSize=50] - Maximum number of commands to keep in history.
|
|
3727
4265
|
*/
|
|
3728
4266
|
constructor(maxSize = 50) {
|
|
4267
|
+
const numericMaxSize = Number(maxSize);
|
|
3729
4268
|
this.history = [];
|
|
3730
4269
|
this.currentIndex = -1;
|
|
3731
|
-
this.maxSize =
|
|
4270
|
+
this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
|
|
3732
4271
|
this.pending = Promise.resolve();
|
|
3733
4272
|
}
|
|
3734
4273
|
/**
|
|
@@ -3798,11 +4337,11 @@
|
|
|
3798
4337
|
*
|
|
3799
4338
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
3800
4339
|
*/
|
|
3801
|
-
undo() {
|
|
4340
|
+
undo(options = {}) {
|
|
3802
4341
|
return this.enqueue(async () => {
|
|
3803
4342
|
if (this.currentIndex >= 0) {
|
|
3804
4343
|
const index = this.currentIndex;
|
|
3805
|
-
await this.history[index].undo();
|
|
4344
|
+
await this.history[index].undo(options);
|
|
3806
4345
|
this.currentIndex = index - 1;
|
|
3807
4346
|
}
|
|
3808
4347
|
});
|
|
@@ -3812,11 +4351,11 @@
|
|
|
3812
4351
|
*
|
|
3813
4352
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
3814
4353
|
*/
|
|
3815
|
-
redo() {
|
|
4354
|
+
redo(options = {}) {
|
|
3816
4355
|
return this.enqueue(async () => {
|
|
3817
4356
|
if (this.currentIndex < this.history.length - 1) {
|
|
3818
4357
|
const index = this.currentIndex + 1;
|
|
3819
|
-
await this.history[index].execute();
|
|
4358
|
+
await this.history[index].execute(options);
|
|
3820
4359
|
this.currentIndex = index;
|
|
3821
4360
|
}
|
|
3822
4361
|
});
|