@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/src/image-editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.5.
|
|
4
|
+
* @version 1.5.2
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -132,6 +132,8 @@ function ensureFabric() {
|
|
|
132
132
|
* @param {number} [options.downsampleQuality=0.92] - JPEG quality for downsampling/export.
|
|
133
133
|
* @param {number} [options.imageLoadTimeoutMs=30000] - Timeout for image decode operations.
|
|
134
134
|
* @param {number} [options.exportMultiplier=1] - Scale output image by this multiplier on export.
|
|
135
|
+
* @param {number} [options.maxExportPixels=50000000] - Maximum output pixels allowed per export.
|
|
136
|
+
* @param {number} [options.maxHistorySize=50] - Maximum undo/redo history entries to keep. Large base64 images can make each snapshot expensive.
|
|
135
137
|
* @param {boolean} [options.exportImageAreaByDefault=true] - Export only the image area (clipped to masks).
|
|
136
138
|
* @param {number} [options.defaultMaskWidth=50] - Default width of new masks.
|
|
137
139
|
* @param {number} [options.defaultMaskHeight=80] - Default height of new masks.
|
|
@@ -202,6 +204,8 @@ function ensureFabric() {
|
|
|
202
204
|
imageLoadTimeoutMs: 30000,
|
|
203
205
|
|
|
204
206
|
exportMultiplier: 1,
|
|
207
|
+
maxExportPixels: 50000000,
|
|
208
|
+
maxHistorySize: 50,
|
|
205
209
|
exportImageAreaByDefault: true,
|
|
206
210
|
|
|
207
211
|
defaultMaskWidth: 50,
|
|
@@ -234,6 +238,7 @@ function ensureFabric() {
|
|
|
234
238
|
...userCrop
|
|
235
239
|
}
|
|
236
240
|
};
|
|
241
|
+
this._normalizeOptions();
|
|
237
242
|
|
|
238
243
|
// Verify that Fabric.js is present before any canvas work starts.
|
|
239
244
|
this._fabricLoaded = !!ensureFabric();
|
|
@@ -258,11 +263,12 @@ function ensureFabric() {
|
|
|
258
263
|
this._activeOperationToken = null;
|
|
259
264
|
this.elements = {};
|
|
260
265
|
this.isImageLoadedToCanvas = false;
|
|
261
|
-
this.maxHistorySize =
|
|
266
|
+
this.maxHistorySize = this.options.maxHistorySize;
|
|
262
267
|
|
|
263
268
|
this._handlersByElementKey = {};
|
|
264
269
|
this._elementCache = {};
|
|
265
270
|
this._elementOriginalPointerEvents = new Map();
|
|
271
|
+
this._elementOriginalDisabledState = new Map();
|
|
266
272
|
|
|
267
273
|
this._lastMask = null;
|
|
268
274
|
this._lastMaskInitialLeft = null;
|
|
@@ -271,6 +277,7 @@ function ensureFabric() {
|
|
|
271
277
|
this._lastSnapshot = null;
|
|
272
278
|
|
|
273
279
|
this._cropMode = false;
|
|
280
|
+
this._isApplyingCrop = false;
|
|
274
281
|
this._cropRect = null;
|
|
275
282
|
this._cropHandlers = [];
|
|
276
283
|
this._cropPrevEvented = null;
|
|
@@ -283,6 +290,8 @@ function ensureFabric() {
|
|
|
283
290
|
this._activeAnimationRejectors = new Set();
|
|
284
291
|
this._disposed = false;
|
|
285
292
|
this._initialized = false;
|
|
293
|
+
this._deprecatedElementKeyWarnings = new Set();
|
|
294
|
+
this._cropRotationWarningEmitted = false;
|
|
286
295
|
|
|
287
296
|
this.onImageLoaded = typeof this.options.onImageLoaded === 'function' ? this.options.onImageLoaded : null;
|
|
288
297
|
|
|
@@ -356,7 +365,13 @@ function ensureFabric() {
|
|
|
356
365
|
* });
|
|
357
366
|
*/
|
|
358
367
|
init(idMap = {}) {
|
|
359
|
-
if (!this._fabricLoaded)
|
|
368
|
+
if (!this._fabricLoaded) {
|
|
369
|
+
this._fabricLoaded = !!ensureFabric();
|
|
370
|
+
if (!this._fabricLoaded) {
|
|
371
|
+
this._reportError('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
360
375
|
if (this._initialized || this.canvas) this.dispose();
|
|
361
376
|
this._disposed = false;
|
|
362
377
|
this._initialized = true;
|
|
@@ -368,10 +383,11 @@ function ensureFabric() {
|
|
|
368
383
|
this._activeOperationName = null;
|
|
369
384
|
this._activeOperationToken = null;
|
|
370
385
|
this._elementOriginalPointerEvents = new Map();
|
|
386
|
+
this._elementOriginalDisabledState = new Map();
|
|
387
|
+
this._isApplyingCrop = false;
|
|
371
388
|
this._containerOriginalOverflow = null;
|
|
372
389
|
this._lastContainerViewportSize = null;
|
|
373
390
|
this._canvasElementOriginalStyle = null;
|
|
374
|
-
this._deprecatedElementKeyWarnings = new Set();
|
|
375
391
|
|
|
376
392
|
const defaults = {
|
|
377
393
|
canvas: 'fabricCanvas',
|
|
@@ -410,6 +426,7 @@ function ensureFabric() {
|
|
|
410
426
|
redoButton: 'redoButton',
|
|
411
427
|
redoBtn: null,
|
|
412
428
|
imageInput: 'imageInput',
|
|
429
|
+
uploadArea: null,
|
|
413
430
|
enterCropModeButton: 'enterCropModeButton',
|
|
414
431
|
cropBtn: null,
|
|
415
432
|
applyCropButton: 'applyCropButton',
|
|
@@ -429,7 +446,8 @@ function ensureFabric() {
|
|
|
429
446
|
|
|
430
447
|
// Auto-load initial image if provided
|
|
431
448
|
if (this.options.initialImageBase64) {
|
|
432
|
-
this.loadImage(this.options.initialImageBase64)
|
|
449
|
+
this.loadImage(this.options.initialImageBase64)
|
|
450
|
+
.catch(error => this._reportError('initialImageBase64 could not be loaded', error));
|
|
433
451
|
} else {
|
|
434
452
|
this._updatePlaceholderStatus();
|
|
435
453
|
}
|
|
@@ -502,6 +520,66 @@ function ensureFabric() {
|
|
|
502
520
|
);
|
|
503
521
|
}
|
|
504
522
|
|
|
523
|
+
_normalizeFiniteNumber(value, fallback) {
|
|
524
|
+
const numericValue = Number(value);
|
|
525
|
+
return Number.isFinite(numericValue) ? numericValue : fallback;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
_normalizePositiveNumber(value, fallback) {
|
|
529
|
+
const numericValue = this._normalizeFiniteNumber(value, fallback);
|
|
530
|
+
return numericValue > 0 ? numericValue : fallback;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
_normalizeNonNegativeNumber(value, fallback) {
|
|
534
|
+
const numericValue = this._normalizeFiniteNumber(value, fallback);
|
|
535
|
+
return numericValue >= 0 ? numericValue : fallback;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_normalizePositiveInteger(value, fallback) {
|
|
539
|
+
const numericValue = this._normalizePositiveNumber(value, fallback);
|
|
540
|
+
return Math.max(1, Math.floor(numericValue));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
_normalizeOptions() {
|
|
544
|
+
const options = this.options || {};
|
|
545
|
+
options.canvasWidth = this._normalizePositiveNumber(options.canvasWidth, 800);
|
|
546
|
+
options.canvasHeight = this._normalizePositiveNumber(options.canvasHeight, 600);
|
|
547
|
+
options.animationDuration = this._normalizeNonNegativeNumber(options.animationDuration, 300);
|
|
548
|
+
|
|
549
|
+
const minScale = this._normalizePositiveNumber(options.minScale, 0.1);
|
|
550
|
+
const maxScale = this._normalizePositiveNumber(options.maxScale, 5);
|
|
551
|
+
if (minScale > maxScale) {
|
|
552
|
+
options.minScale = 0.1;
|
|
553
|
+
options.maxScale = 5;
|
|
554
|
+
} else {
|
|
555
|
+
options.minScale = minScale;
|
|
556
|
+
options.maxScale = maxScale;
|
|
557
|
+
}
|
|
558
|
+
options.scaleStep = this._normalizePositiveNumber(options.scaleStep, 0.05);
|
|
559
|
+
options.rotationStep = this._normalizeFiniteNumber(options.rotationStep, 90);
|
|
560
|
+
|
|
561
|
+
options.downsampleMaxWidth = this._normalizePositiveNumber(options.downsampleMaxWidth, 4000);
|
|
562
|
+
options.downsampleMaxHeight = this._normalizePositiveNumber(options.downsampleMaxHeight, 3000);
|
|
563
|
+
options.downsampleQuality = options.downsampleQuality == null
|
|
564
|
+
? 0.92
|
|
565
|
+
: Math.max(0, Math.min(1, this._normalizeFiniteNumber(options.downsampleQuality, 0.92)));
|
|
566
|
+
options.imageLoadTimeoutMs = this._normalizePositiveNumber(options.imageLoadTimeoutMs, 30000);
|
|
567
|
+
|
|
568
|
+
options.exportMultiplier = this._normalizePositiveNumber(options.exportMultiplier, 1);
|
|
569
|
+
options.maxExportPixels = this._normalizePositiveInteger(options.maxExportPixels, 50000000);
|
|
570
|
+
options.maxHistorySize = this._normalizePositiveInteger(options.maxHistorySize, 50);
|
|
571
|
+
|
|
572
|
+
options.defaultMaskWidth = this._normalizePositiveNumber(options.defaultMaskWidth, 50);
|
|
573
|
+
options.defaultMaskHeight = this._normalizePositiveNumber(options.defaultMaskHeight, 80);
|
|
574
|
+
options.maskLabelOffset = this._normalizeNonNegativeNumber(options.maskLabelOffset, 3);
|
|
575
|
+
|
|
576
|
+
if (options.crop) {
|
|
577
|
+
options.crop.minWidth = this._normalizePositiveNumber(options.crop.minWidth, 100);
|
|
578
|
+
options.crop.minHeight = this._normalizePositiveNumber(options.crop.minHeight, 100);
|
|
579
|
+
options.crop.padding = this._normalizeNonNegativeNumber(options.crop.padding, 10);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
505
583
|
_reportError(message, error = null) {
|
|
506
584
|
const handler = this.options && this.options.onError;
|
|
507
585
|
if (typeof handler !== 'function') return;
|
|
@@ -524,10 +602,19 @@ function ensureFabric() {
|
|
|
524
602
|
}
|
|
525
603
|
}
|
|
526
604
|
|
|
605
|
+
_emitSafeCallback(callback, message) {
|
|
606
|
+
if (typeof callback !== 'function') return;
|
|
607
|
+
try {
|
|
608
|
+
callback();
|
|
609
|
+
} catch (error) {
|
|
610
|
+
this._reportWarning(message, error);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
527
614
|
_notifyImageLoaded() {
|
|
528
615
|
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
529
616
|
const callback = typeof optionsCallback === 'function' ? optionsCallback : this.onImageLoaded;
|
|
530
|
-
|
|
617
|
+
this._emitSafeCallback(callback, 'onImageLoaded callback failed');
|
|
531
618
|
}
|
|
532
619
|
|
|
533
620
|
/**
|
|
@@ -660,13 +747,14 @@ function ensureFabric() {
|
|
|
660
747
|
this._captureContainerOverflowState();
|
|
661
748
|
|
|
662
749
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
663
|
-
|
|
750
|
+
const layoutMode = this._getImageLayoutMode();
|
|
751
|
+
if (layoutMode === 'cover') {
|
|
664
752
|
this.containerElement.style.overflow = 'scroll';
|
|
665
753
|
if (!shouldPreserveScroll) {
|
|
666
754
|
this.containerElement.scrollLeft = 0;
|
|
667
755
|
this.containerElement.scrollTop = 0;
|
|
668
756
|
}
|
|
669
|
-
} else if (
|
|
757
|
+
} else if (layoutMode === 'fit') {
|
|
670
758
|
this.containerElement.style.overflow = 'auto';
|
|
671
759
|
if (!shouldPreserveScroll) {
|
|
672
760
|
this.containerElement.scrollLeft = 0;
|
|
@@ -792,7 +880,6 @@ function ensureFabric() {
|
|
|
792
880
|
_loadImageFile(file) {
|
|
793
881
|
if (!this._isSupportedImageFile(file)) {
|
|
794
882
|
const error = new Error('Selected file is not a supported image');
|
|
795
|
-
this._reportError('Selected file is not a supported image', error);
|
|
796
883
|
return Promise.reject(error);
|
|
797
884
|
}
|
|
798
885
|
|
|
@@ -838,6 +925,13 @@ function ensureFabric() {
|
|
|
838
925
|
);
|
|
839
926
|
}
|
|
840
927
|
|
|
928
|
+
_getImageLayoutMode() {
|
|
929
|
+
if (this.options.fitImageToCanvas) return 'fit';
|
|
930
|
+
if (this.options.coverImageToCanvas) return 'cover';
|
|
931
|
+
if (this.options.expandCanvasToImage) return 'expand';
|
|
932
|
+
return 'contain';
|
|
933
|
+
}
|
|
934
|
+
|
|
841
935
|
/**
|
|
842
936
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
843
937
|
*
|
|
@@ -851,14 +945,22 @@ function ensureFabric() {
|
|
|
851
945
|
if (!this._fabricLoaded) return;
|
|
852
946
|
if (!this.canvas || this._disposed) return;
|
|
853
947
|
if (!imageBase64 || typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) return;
|
|
948
|
+
options = options || {};
|
|
854
949
|
this._assertIdleForOperation('loadImage', options);
|
|
855
950
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
951
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
952
|
+
const operationToken = isNestedOperation
|
|
953
|
+
? this._getInternalOperationToken(options)
|
|
954
|
+
: this._beginBusyOperation('loadImage');
|
|
955
|
+
let transaction = null;
|
|
956
|
+
let shouldNotifyImageLoaded;
|
|
860
957
|
|
|
861
958
|
try {
|
|
959
|
+
this._isLoading = true;
|
|
960
|
+
this._updateUI();
|
|
961
|
+
this._warnOnImageLayoutOptionConflict();
|
|
962
|
+
transaction = this._captureLoadImageTransaction();
|
|
963
|
+
|
|
862
964
|
const imageElement = await this._createImageElement(imageBase64);
|
|
863
965
|
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
864
966
|
|
|
@@ -906,8 +1008,9 @@ function ensureFabric() {
|
|
|
906
1008
|
const viewport = this._getContainerViewportSize();
|
|
907
1009
|
const minWidth = viewport.width;
|
|
908
1010
|
const minHeight = viewport.height;
|
|
1011
|
+
const layoutMode = this._getImageLayoutMode();
|
|
909
1012
|
|
|
910
|
-
if (
|
|
1013
|
+
if (layoutMode === 'fit') {
|
|
911
1014
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
912
1015
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
913
1016
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -915,13 +1018,13 @@ function ensureFabric() {
|
|
|
915
1018
|
fabricImage.set({ left: 0, top: 0 });
|
|
916
1019
|
fabricImage.scale(fitScale);
|
|
917
1020
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
918
|
-
} else if (
|
|
1021
|
+
} else if (layoutMode === 'cover') {
|
|
919
1022
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
920
1023
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
921
1024
|
fabricImage.set({ left: 0, top: 0 });
|
|
922
1025
|
fabricImage.scale(layout.scale);
|
|
923
1026
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
924
|
-
} else if (
|
|
1027
|
+
} else if (layoutMode === 'expand') {
|
|
925
1028
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
926
1029
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
927
1030
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -954,15 +1057,21 @@ function ensureFabric() {
|
|
|
954
1057
|
this._updateUI();
|
|
955
1058
|
this.canvas.renderAll();
|
|
956
1059
|
this._lastSnapshot = this._captureCanvasStateOrThrow('loadImage');
|
|
957
|
-
|
|
958
|
-
this._notifyImageLoaded();
|
|
1060
|
+
shouldNotifyImageLoaded = true;
|
|
959
1061
|
} catch (error) {
|
|
960
|
-
await this._rollbackLoadImageTransaction(
|
|
1062
|
+
await this._rollbackLoadImageTransaction(
|
|
1063
|
+
transaction,
|
|
1064
|
+
this._withInternalOperationOptions(operationToken)
|
|
1065
|
+
);
|
|
961
1066
|
throw error;
|
|
962
1067
|
} finally {
|
|
963
1068
|
this._isLoading = false;
|
|
1069
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
964
1070
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
965
1071
|
}
|
|
1072
|
+
if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
|
|
1073
|
+
this._notifyImageLoaded();
|
|
1074
|
+
}
|
|
966
1075
|
}
|
|
967
1076
|
|
|
968
1077
|
/**
|
|
@@ -990,6 +1099,7 @@ function ensureFabric() {
|
|
|
990
1099
|
return !!(
|
|
991
1100
|
this.isAnimating ||
|
|
992
1101
|
this._cropMode ||
|
|
1102
|
+
this._isApplyingCrop ||
|
|
993
1103
|
this._isLoading ||
|
|
994
1104
|
this._activeOperationToken ||
|
|
995
1105
|
(this.animationQueue && this.animationQueue.isBusy())
|
|
@@ -1092,13 +1202,13 @@ function ensureFabric() {
|
|
|
1092
1202
|
};
|
|
1093
1203
|
}
|
|
1094
1204
|
|
|
1095
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
1205
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
1096
1206
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
1097
1207
|
let didRestoreCanvasState = false;
|
|
1098
1208
|
let didFailCanvasRestore = false;
|
|
1099
1209
|
try {
|
|
1100
1210
|
if (transaction.canvasState) {
|
|
1101
|
-
await this.loadFromState(transaction.canvasState);
|
|
1211
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
1102
1212
|
didRestoreCanvasState = true;
|
|
1103
1213
|
}
|
|
1104
1214
|
} catch (error) {
|
|
@@ -1413,10 +1523,18 @@ function ensureFabric() {
|
|
|
1413
1523
|
|
|
1414
1524
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1415
1525
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1526
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1527
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1528
|
+
const shouldReserveNoScrollbarMargin = layoutMode === 'fit' || layoutMode === 'cover';
|
|
1529
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1530
|
+
const margin = hasOppositeScrollbar ? safetyMargin : (shouldReserveNoScrollbarMargin ? 1 : 0);
|
|
1531
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1532
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1533
|
+
};
|
|
1416
1534
|
|
|
1417
1535
|
return {
|
|
1418
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1419
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1536
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1537
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1420
1538
|
viewportWidth: effectiveWidth,
|
|
1421
1539
|
viewportHeight: effectiveHeight,
|
|
1422
1540
|
hasHorizontal,
|
|
@@ -1549,6 +1667,50 @@ function ensureFabric() {
|
|
|
1549
1667
|
}
|
|
1550
1668
|
}
|
|
1551
1669
|
|
|
1670
|
+
_getSerializableStateObjects() {
|
|
1671
|
+
if (!this.canvas) return [];
|
|
1672
|
+
return this.canvas.getObjects().filter(object => !object.isCropRect && !object.maskLabel);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1676
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1677
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1678
|
+
const numericProperties = [
|
|
1679
|
+
'left',
|
|
1680
|
+
'top',
|
|
1681
|
+
'width',
|
|
1682
|
+
'height',
|
|
1683
|
+
'scaleX',
|
|
1684
|
+
'scaleY',
|
|
1685
|
+
'angle',
|
|
1686
|
+
'skewX',
|
|
1687
|
+
'skewY',
|
|
1688
|
+
'cropX',
|
|
1689
|
+
'cropY',
|
|
1690
|
+
'radius',
|
|
1691
|
+
'rx',
|
|
1692
|
+
'ry',
|
|
1693
|
+
'strokeWidth'
|
|
1694
|
+
];
|
|
1695
|
+
|
|
1696
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1697
|
+
const fabricObject = fabricObjects[index];
|
|
1698
|
+
if (!serializedObject || !fabricObject) return;
|
|
1699
|
+
|
|
1700
|
+
numericProperties.forEach(property => {
|
|
1701
|
+
const numericValue = Number(fabricObject[property]);
|
|
1702
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1706
|
+
serializedObject.points = fabricObject.points.map(point => ({
|
|
1707
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1708
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1709
|
+
}));
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1552
1714
|
_restoreMaskControls(mask) {
|
|
1553
1715
|
if (!mask) return;
|
|
1554
1716
|
|
|
@@ -1598,6 +1760,7 @@ function ensureFabric() {
|
|
|
1598
1760
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1599
1761
|
if (Array.isArray(jsonObject.objects)) {
|
|
1600
1762
|
jsonObject.objects = jsonObject.objects.filter(object => !object.isCropRect && !object.maskLabel);
|
|
1763
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1601
1764
|
}
|
|
1602
1765
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1603
1766
|
return JSON.stringify(jsonObject);
|
|
@@ -1680,6 +1843,13 @@ function ensureFabric() {
|
|
|
1680
1843
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1681
1844
|
}
|
|
1682
1845
|
|
|
1846
|
+
_hasScaledImageEdge(axis) {
|
|
1847
|
+
if (!this.originalImage) return false;
|
|
1848
|
+
const scale = Number(axis === 'y' ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1849
|
+
if (!Number.isFinite(scale)) return false;
|
|
1850
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1683
1853
|
_getPartialExportEdges(bounds) {
|
|
1684
1854
|
if (!bounds) return null;
|
|
1685
1855
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1689,8 +1859,8 @@ function ensureFabric() {
|
|
|
1689
1859
|
return {
|
|
1690
1860
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1691
1861
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1692
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1693
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1862
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge('x'),
|
|
1863
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge('y')
|
|
1694
1864
|
};
|
|
1695
1865
|
}
|
|
1696
1866
|
|
|
@@ -1756,7 +1926,8 @@ function ensureFabric() {
|
|
|
1756
1926
|
* @private
|
|
1757
1927
|
*/
|
|
1758
1928
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg', sealPartialEdges = null }) {
|
|
1759
|
-
const safeMultiplier =
|
|
1929
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1930
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1760
1931
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1761
1932
|
const exportFormat = safeFormat === 'jpeg' ? 'png' : safeFormat;
|
|
1762
1933
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1774,6 +1945,30 @@ function ensureFabric() {
|
|
|
1774
1945
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1775
1946
|
}
|
|
1776
1947
|
|
|
1948
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1949
|
+
const numericMultiplier = Number(multiplier);
|
|
1950
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1951
|
+
throw new Error('Export multiplier must be a finite positive number');
|
|
1952
|
+
}
|
|
1953
|
+
return Math.max(1, numericMultiplier);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1957
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1958
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1959
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1960
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1961
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1962
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1963
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0
|
|
1964
|
+
? Math.floor(configuredMaxPixels)
|
|
1965
|
+
: 50000000;
|
|
1966
|
+
|
|
1967
|
+
if (outputPixels > maxPixels) {
|
|
1968
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1777
1972
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1778
1973
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1779
1974
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1792,7 +1987,24 @@ function ensureFabric() {
|
|
|
1792
1987
|
_getJpegBackgroundColor() {
|
|
1793
1988
|
const backgroundColor = String(this.options.backgroundColor || '').trim();
|
|
1794
1989
|
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
|
|
1795
|
-
return backgroundColor;
|
|
1990
|
+
return this._isValidCanvasFillStyle(backgroundColor) ? backgroundColor : '#ffffff';
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
_isValidCanvasFillStyle(color) {
|
|
1994
|
+
try {
|
|
1995
|
+
if (typeof document === 'undefined' || !document.createElement) return false;
|
|
1996
|
+
const validationCanvas = document.createElement('canvas');
|
|
1997
|
+
const context = validationCanvas.getContext && validationCanvas.getContext('2d');
|
|
1998
|
+
if (!context) return false;
|
|
1999
|
+
context.fillStyle = '#010203';
|
|
2000
|
+
context.fillStyle = color;
|
|
2001
|
+
if (context.fillStyle !== '#010203') return true;
|
|
2002
|
+
context.fillStyle = '#040506';
|
|
2003
|
+
context.fillStyle = color;
|
|
2004
|
+
return context.fillStyle !== '#040506';
|
|
2005
|
+
} catch {
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
1796
2008
|
}
|
|
1797
2009
|
|
|
1798
2010
|
_isTransparentCssColor(color) {
|
|
@@ -1826,6 +2038,7 @@ function ensureFabric() {
|
|
|
1826
2038
|
|
|
1827
2039
|
_decodeBase64Payload(base64Payload) {
|
|
1828
2040
|
const payload = String(base64Payload || '');
|
|
2041
|
+
if (!payload) throw new Error('Data URL base64 payload is empty');
|
|
1829
2042
|
if (typeof atob === 'function') {
|
|
1830
2043
|
return Uint8Array.from(atob(payload), char => char.charCodeAt(0));
|
|
1831
2044
|
}
|
|
@@ -1835,6 +2048,14 @@ function ensureFabric() {
|
|
|
1835
2048
|
throw new Error('Base64 decoding is unavailable');
|
|
1836
2049
|
}
|
|
1837
2050
|
|
|
2051
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
2052
|
+
const match = String(dataUrl || '').match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
2053
|
+
if (!match || !match[2]) {
|
|
2054
|
+
throw new Error('Export produced an invalid or empty base64 data URL');
|
|
2055
|
+
}
|
|
2056
|
+
return this._decodeBase64Payload(match[2]);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
1838
2059
|
/**
|
|
1839
2060
|
* Gets the top-left corner coordinates of the given object.
|
|
1840
2061
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1953,13 +2174,49 @@ function ensureFabric() {
|
|
|
1953
2174
|
const currentHeight = this.canvas.getHeight();
|
|
1954
2175
|
let requiredWidth = currentWidth;
|
|
1955
2176
|
let requiredHeight = currentHeight;
|
|
1956
|
-
|
|
2177
|
+
const layoutMode = this._getImageLayoutMode();
|
|
2178
|
+
const usesScrollableFitBounds = layoutMode === 'fit' || layoutMode === 'cover';
|
|
2179
|
+
let contentWidth = 0;
|
|
2180
|
+
let contentHeight = 0;
|
|
2181
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1957
2182
|
if (!fabricObject) return;
|
|
1958
2183
|
if (typeof fabricObject.setCoords === 'function') fabricObject.setCoords();
|
|
1959
2184
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1960
|
-
|
|
1961
|
-
|
|
2185
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
2186
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
2187
|
+
contentWidth = Math.max(contentWidth, right);
|
|
2188
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
2189
|
+
return { right, bottom };
|
|
2190
|
+
};
|
|
2191
|
+
fabricObjects.forEach(fabricObject => {
|
|
2192
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
2193
|
+
if (!bounds) return;
|
|
2194
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
2195
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1962
2196
|
});
|
|
2197
|
+
if (usesScrollableFitBounds) {
|
|
2198
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
2199
|
+
this.canvas.getObjects().forEach(object => {
|
|
2200
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
2204
|
+
Math.max(1, contentWidth),
|
|
2205
|
+
Math.max(1, contentHeight)
|
|
2206
|
+
);
|
|
2207
|
+
|
|
2208
|
+
const newWidth = contentSize.hasHorizontal
|
|
2209
|
+
? Math.max(currentWidth, contentSize.width)
|
|
2210
|
+
: contentSize.width;
|
|
2211
|
+
const newHeight = contentSize.hasVertical
|
|
2212
|
+
? Math.max(currentHeight, contentSize.height)
|
|
2213
|
+
: contentSize.height;
|
|
2214
|
+
|
|
2215
|
+
if (newWidth !== currentWidth || newHeight !== currentHeight) {
|
|
2216
|
+
this._setCanvasSizeInt(newWidth, newHeight);
|
|
2217
|
+
}
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
1963
2220
|
let minWidth = 0;
|
|
1964
2221
|
let minHeight = 0;
|
|
1965
2222
|
if (this.containerElement) {
|
|
@@ -1979,16 +2236,65 @@ function ensureFabric() {
|
|
|
1979
2236
|
}
|
|
1980
2237
|
}
|
|
1981
2238
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2239
|
+
_captureImageDisplayBounds() {
|
|
2240
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
2241
|
+
this.originalImage.setCoords();
|
|
2242
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
2243
|
+
const width = Number(bounds && bounds.width);
|
|
2244
|
+
const height = Number(bounds && bounds.height);
|
|
2245
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
2246
|
+
|
|
2247
|
+
return {
|
|
2248
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
2249
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
2250
|
+
width,
|
|
2251
|
+
height
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
2256
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
2257
|
+
const imageWidth = Number(this.originalImage.width);
|
|
2258
|
+
const imageHeight = Number(this.originalImage.height);
|
|
2259
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
2260
|
+
|
|
2261
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
2262
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
2263
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
2264
|
+
|
|
2265
|
+
const left = Number(displayBounds.left) || 0;
|
|
2266
|
+
const top = Number(displayBounds.top) || 0;
|
|
2267
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
2268
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
2269
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
2270
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
2271
|
+
const layoutMode = this._getImageLayoutMode();
|
|
2272
|
+
if (layoutMode === 'fit' || layoutMode === 'cover') {
|
|
2273
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
2274
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
2275
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
2276
|
+
}
|
|
2277
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
2278
|
+
this._setCanvasSizeInt(
|
|
2279
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
2280
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
this.originalImage.set({
|
|
2285
|
+
originX: 'left',
|
|
2286
|
+
originY: 'top',
|
|
2287
|
+
left,
|
|
2288
|
+
top,
|
|
2289
|
+
scaleX,
|
|
2290
|
+
scaleY
|
|
2291
|
+
});
|
|
2292
|
+
this.originalImage.setCoords();
|
|
2293
|
+
this.baseImageScale = scaleX;
|
|
2294
|
+
this.currentScale = 1;
|
|
2295
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
2296
|
+
this._updateInputs();
|
|
2297
|
+
this.canvas.renderAll();
|
|
1992
2298
|
}
|
|
1993
2299
|
|
|
1994
2300
|
/**
|
|
@@ -2004,7 +2310,14 @@ function ensureFabric() {
|
|
|
2004
2310
|
} catch (error) {
|
|
2005
2311
|
return Promise.reject(error);
|
|
2006
2312
|
}
|
|
2007
|
-
return this.animationQueue.add(() =>
|
|
2313
|
+
return this.animationQueue.add(async () => {
|
|
2314
|
+
const operationToken = this._beginBusyOperation('scaleImage');
|
|
2315
|
+
try {
|
|
2316
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
2317
|
+
} finally {
|
|
2318
|
+
this._endBusyOperation(operationToken);
|
|
2319
|
+
}
|
|
2320
|
+
})
|
|
2008
2321
|
.finally(() => {
|
|
2009
2322
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
2010
2323
|
});
|
|
@@ -2056,7 +2369,7 @@ function ensureFabric() {
|
|
|
2056
2369
|
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
2057
2370
|
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
2058
2371
|
}
|
|
2059
|
-
if (this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) {
|
|
2372
|
+
if ((this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) && !isOwnInternalOperation) {
|
|
2060
2373
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
2061
2374
|
}
|
|
2062
2375
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -2147,10 +2460,12 @@ function ensureFabric() {
|
|
|
2147
2460
|
async _scaleImageImpl(factor, options = {}) {
|
|
2148
2461
|
if (!this.originalImage || this._disposed) return;
|
|
2149
2462
|
if (this.isAnimating) return;
|
|
2463
|
+
const numericFactor = Number(factor);
|
|
2464
|
+
if (!Number.isFinite(numericFactor)) return;
|
|
2150
2465
|
const saveHistory = options.saveHistory !== false;
|
|
2151
2466
|
let didStartAnimation = false;
|
|
2152
2467
|
try {
|
|
2153
|
-
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale,
|
|
2468
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
|
|
2154
2469
|
this.currentScale = factor;
|
|
2155
2470
|
this.isAnimating = true;
|
|
2156
2471
|
didStartAnimation = true;
|
|
@@ -2179,7 +2494,7 @@ function ensureFabric() {
|
|
|
2179
2494
|
this.canvas.getObjects().forEach(object => { if (object.maskId) this._syncMaskLabel(object); });
|
|
2180
2495
|
|
|
2181
2496
|
this._updateInputs();
|
|
2182
|
-
if (saveHistory) this.saveState();
|
|
2497
|
+
if (saveHistory) this.saveState(options);
|
|
2183
2498
|
} finally {
|
|
2184
2499
|
if (didStartAnimation) {
|
|
2185
2500
|
this.isAnimating = false;
|
|
@@ -2202,7 +2517,14 @@ function ensureFabric() {
|
|
|
2202
2517
|
} catch (error) {
|
|
2203
2518
|
return Promise.reject(error);
|
|
2204
2519
|
}
|
|
2205
|
-
return this.animationQueue.add(() =>
|
|
2520
|
+
return this.animationQueue.add(async () => {
|
|
2521
|
+
const operationToken = this._beginBusyOperation('rotateImage');
|
|
2522
|
+
try {
|
|
2523
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2524
|
+
} finally {
|
|
2525
|
+
this._endBusyOperation(operationToken);
|
|
2526
|
+
}
|
|
2527
|
+
})
|
|
2206
2528
|
.finally(() => {
|
|
2207
2529
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
2208
2530
|
});
|
|
@@ -2218,7 +2540,8 @@ function ensureFabric() {
|
|
|
2218
2540
|
async _rotateImageImpl(degrees, options = {}) {
|
|
2219
2541
|
if (!this.originalImage || this._disposed) return;
|
|
2220
2542
|
if (this.isAnimating) return;
|
|
2221
|
-
|
|
2543
|
+
const numericDegrees = Number(degrees);
|
|
2544
|
+
if (!Number.isFinite(numericDegrees)) return;
|
|
2222
2545
|
const saveHistory = options.saveHistory !== false;
|
|
2223
2546
|
const image = this.originalImage;
|
|
2224
2547
|
const previousOriginX = image.originX || 'left';
|
|
@@ -2227,6 +2550,7 @@ function ensureFabric() {
|
|
|
2227
2550
|
let didStartAnimation = false;
|
|
2228
2551
|
let didCompleteRotation = false;
|
|
2229
2552
|
try {
|
|
2553
|
+
degrees = numericDegrees;
|
|
2230
2554
|
this.currentRotation = degrees;
|
|
2231
2555
|
this.isAnimating = true;
|
|
2232
2556
|
didStartAnimation = true;
|
|
@@ -2253,7 +2577,7 @@ function ensureFabric() {
|
|
|
2253
2577
|
this.canvas.getObjects().forEach(object => { if (object.maskId) this._syncMaskLabel(object); });
|
|
2254
2578
|
|
|
2255
2579
|
this._updateInputs();
|
|
2256
|
-
if (saveHistory) this.saveState();
|
|
2580
|
+
if (saveHistory) this.saveState(options);
|
|
2257
2581
|
didCompleteRotation = true;
|
|
2258
2582
|
} finally {
|
|
2259
2583
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -2282,19 +2606,22 @@ function ensureFabric() {
|
|
|
2282
2606
|
}
|
|
2283
2607
|
|
|
2284
2608
|
return this.animationQueue.add(async () => {
|
|
2609
|
+
const operationToken = this._beginBusyOperation('resetImageTransform');
|
|
2285
2610
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow('resetImageTransform');
|
|
2286
2611
|
try {
|
|
2287
|
-
await this._scaleImageImpl(1, { saveHistory: false });
|
|
2288
|
-
await this._rotateImageImpl(0, { saveHistory: false });
|
|
2612
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2613
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2289
2614
|
const after = this._captureCanvasStateOrThrow('resetImageTransform');
|
|
2290
2615
|
this._pushStateTransition(before, after);
|
|
2291
2616
|
} catch (error) {
|
|
2292
2617
|
try {
|
|
2293
|
-
await this.loadFromState(before);
|
|
2618
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
2294
2619
|
} catch (restoreError) {
|
|
2295
2620
|
this._reportError('resetImageTransform rollback failed', restoreError);
|
|
2296
2621
|
}
|
|
2297
2622
|
throw error;
|
|
2623
|
+
} finally {
|
|
2624
|
+
this._endBusyOperation(operationToken);
|
|
2298
2625
|
}
|
|
2299
2626
|
}).finally(() => {
|
|
2300
2627
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
@@ -2321,8 +2648,13 @@ function ensureFabric() {
|
|
|
2321
2648
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
2322
2649
|
* @public
|
|
2323
2650
|
*/
|
|
2324
|
-
loadFromState(serializedState) {
|
|
2651
|
+
loadFromState(serializedState, options = {}) {
|
|
2325
2652
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2653
|
+
try {
|
|
2654
|
+
this._assertIdleForOperation('loadFromState', options);
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
return Promise.reject(error);
|
|
2657
|
+
}
|
|
2326
2658
|
if (this._cropMode || this._cropRect) {
|
|
2327
2659
|
this._removeCropRect();
|
|
2328
2660
|
this._restoreCropObjectState();
|
|
@@ -2508,9 +2840,17 @@ function ensureFabric() {
|
|
|
2508
2840
|
* @returns {void}
|
|
2509
2841
|
* @public
|
|
2510
2842
|
*/
|
|
2511
|
-
saveState() {
|
|
2843
|
+
saveState(options = {}) {
|
|
2512
2844
|
if (!this.canvas) return;
|
|
2513
2845
|
|
|
2846
|
+
try {
|
|
2847
|
+
this._assertIdleForOperation('saveState', options);
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
this._reportError('saveState blocked', error);
|
|
2850
|
+
this._updateUI();
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2514
2854
|
try {
|
|
2515
2855
|
const after = this._captureCanvasStateOrThrow('saveState');
|
|
2516
2856
|
const before = this._lastSnapshot || after;
|
|
@@ -2518,14 +2858,14 @@ function ensureFabric() {
|
|
|
2518
2858
|
let executedOnce = false;
|
|
2519
2859
|
|
|
2520
2860
|
const command = new Command(
|
|
2521
|
-
() => {
|
|
2861
|
+
(commandOptions = {}) => {
|
|
2522
2862
|
if (executedOnce) {
|
|
2523
|
-
return this.loadFromState(after);
|
|
2863
|
+
return this.loadFromState(after, commandOptions);
|
|
2524
2864
|
}
|
|
2525
2865
|
executedOnce = true;
|
|
2526
2866
|
return undefined;
|
|
2527
2867
|
},
|
|
2528
|
-
() => this.loadFromState(before)
|
|
2868
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2529
2869
|
);
|
|
2530
2870
|
|
|
2531
2871
|
this.historyManager.execute(command);
|
|
@@ -2557,8 +2897,8 @@ function ensureFabric() {
|
|
|
2557
2897
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2558
2898
|
|
|
2559
2899
|
const command = new Command(
|
|
2560
|
-
() => this.loadFromState(after),
|
|
2561
|
-
() => this.loadFromState(before)
|
|
2900
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2901
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2562
2902
|
);
|
|
2563
2903
|
this.historyManager.push(command);
|
|
2564
2904
|
this._lastSnapshot = after;
|
|
@@ -2572,8 +2912,17 @@ function ensureFabric() {
|
|
|
2572
2912
|
* @public
|
|
2573
2913
|
*/
|
|
2574
2914
|
undo() {
|
|
2575
|
-
|
|
2915
|
+
try {
|
|
2916
|
+
this._assertIdleForOperation('undo');
|
|
2917
|
+
} catch (error) {
|
|
2918
|
+
return Promise.reject(error);
|
|
2919
|
+
}
|
|
2920
|
+
const operationToken = this._beginBusyOperation('undo');
|
|
2921
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken))
|
|
2576
2922
|
.then(() => { this._updateUI(); })
|
|
2923
|
+
.finally(() => {
|
|
2924
|
+
this._endBusyOperation(operationToken);
|
|
2925
|
+
})
|
|
2577
2926
|
.catch(error => {
|
|
2578
2927
|
this._reportError('undo failed', error);
|
|
2579
2928
|
throw error;
|
|
@@ -2587,8 +2936,17 @@ function ensureFabric() {
|
|
|
2587
2936
|
* @public
|
|
2588
2937
|
*/
|
|
2589
2938
|
redo() {
|
|
2590
|
-
|
|
2939
|
+
try {
|
|
2940
|
+
this._assertIdleForOperation('redo');
|
|
2941
|
+
} catch (error) {
|
|
2942
|
+
return Promise.reject(error);
|
|
2943
|
+
}
|
|
2944
|
+
const operationToken = this._beginBusyOperation('redo');
|
|
2945
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken))
|
|
2591
2946
|
.then(() => { this._updateUI(); })
|
|
2947
|
+
.finally(() => {
|
|
2948
|
+
this._endBusyOperation(operationToken);
|
|
2949
|
+
})
|
|
2592
2950
|
.catch(error => {
|
|
2593
2951
|
this._reportError('redo failed', error);
|
|
2594
2952
|
throw error;
|
|
@@ -2712,33 +3070,70 @@ function ensureFabric() {
|
|
|
2712
3070
|
return value != null ? value : fallback;
|
|
2713
3071
|
};
|
|
2714
3072
|
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
3073
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
3074
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
3075
|
+
return null;
|
|
3076
|
+
};
|
|
3077
|
+
|
|
3078
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
3079
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
3080
|
+
const numericValue = Number(resolvedValue);
|
|
3081
|
+
if (!Number.isFinite(numericValue)) {
|
|
3082
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
3083
|
+
}
|
|
3084
|
+
if (constraints.positive && numericValue <= 0) {
|
|
3085
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
3086
|
+
}
|
|
3087
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
3088
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
3089
|
+
}
|
|
3090
|
+
return numericValue;
|
|
3091
|
+
};
|
|
3092
|
+
|
|
3093
|
+
try {
|
|
3094
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, 'width', 'gap', { nonNegative: true });
|
|
3095
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, 'width', 'width', { positive: true });
|
|
3096
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, 'height', 'height', { positive: true });
|
|
3097
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, 'width', 'angle');
|
|
3098
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, 'width', 'alpha')));
|
|
3099
|
+
|
|
3100
|
+
if (maskConfig.left === undefined && this._lastMask) {
|
|
3101
|
+
const previousMask = this._lastMask;
|
|
3102
|
+
if (typeof previousMask.setCoords === 'function') previousMask.setCoords();
|
|
3103
|
+
const previousBounds = typeof previousMask.getBoundingRect === 'function'
|
|
3104
|
+
? previousMask.getBoundingRect(true, true)
|
|
3105
|
+
: { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
3106
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
3107
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
3108
|
+
} else {
|
|
3109
|
+
left = resolveNumber(maskConfig.left, firstOffset, 'width', 'left');
|
|
3110
|
+
top = resolveNumber(maskConfig.top, firstOffset, 'height', 'top');
|
|
3111
|
+
}
|
|
3112
|
+
} catch (error) {
|
|
3113
|
+
return rejectInvalidMask('invalid numeric configuration', error);
|
|
2726
3114
|
}
|
|
2727
3115
|
|
|
2728
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, 'width');
|
|
2729
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, 'height');
|
|
2730
3116
|
maskConfig.left = left;
|
|
2731
3117
|
maskConfig.top = top;
|
|
2732
3118
|
|
|
2733
3119
|
let mask;
|
|
2734
3120
|
if (typeof maskConfig.fabricGenerator === 'function') {
|
|
2735
|
-
|
|
3121
|
+
try {
|
|
3122
|
+
mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
return rejectInvalidMask('fabricGenerator failed', error);
|
|
3125
|
+
}
|
|
2736
3126
|
} else {
|
|
2737
3127
|
switch (shapeType) {
|
|
2738
3128
|
case 'circle':
|
|
3129
|
+
try {
|
|
3130
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, 'min', 'radius', { positive: true });
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
return rejectInvalidMask('invalid circle radius', error);
|
|
3133
|
+
}
|
|
2739
3134
|
mask = new fabric.Circle({
|
|
2740
3135
|
left, top,
|
|
2741
|
-
radius:
|
|
3136
|
+
radius: maskConfig.radius,
|
|
2742
3137
|
fill: maskConfig.color,
|
|
2743
3138
|
opacity: maskConfig.alpha,
|
|
2744
3139
|
angle: maskConfig.angle,
|
|
@@ -2746,10 +3141,16 @@ function ensureFabric() {
|
|
|
2746
3141
|
});
|
|
2747
3142
|
break;
|
|
2748
3143
|
case 'ellipse':
|
|
3144
|
+
try {
|
|
3145
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, 'width', 'rx', { positive: true });
|
|
3146
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, 'height', 'ry', { positive: true });
|
|
3147
|
+
} catch (error) {
|
|
3148
|
+
return rejectInvalidMask('invalid ellipse radius', error);
|
|
3149
|
+
}
|
|
2749
3150
|
mask = new fabric.Ellipse({
|
|
2750
3151
|
left, top,
|
|
2751
|
-
rx:
|
|
2752
|
-
ry:
|
|
3152
|
+
rx: maskConfig.rx,
|
|
3153
|
+
ry: maskConfig.ry,
|
|
2753
3154
|
fill: maskConfig.color,
|
|
2754
3155
|
opacity: maskConfig.alpha,
|
|
2755
3156
|
angle: maskConfig.angle,
|
|
@@ -2758,11 +3159,31 @@ function ensureFabric() {
|
|
|
2758
3159
|
break;
|
|
2759
3160
|
case 'polygon': {
|
|
2760
3161
|
let polygonPoints = maskConfig.points || [];
|
|
2761
|
-
if (Array.isArray(polygonPoints)
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
3162
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
3163
|
+
return rejectInvalidMask('polygon masks require at least three points');
|
|
3164
|
+
}
|
|
3165
|
+
try {
|
|
3166
|
+
polygonPoints = polygonPoints.map(point => {
|
|
3167
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
3168
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
3169
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
3170
|
+
throw new Error('polygon point coordinates must be finite numbers');
|
|
3171
|
+
}
|
|
3172
|
+
return { x, y };
|
|
3173
|
+
});
|
|
3174
|
+
} catch (error) {
|
|
3175
|
+
return rejectInvalidMask('invalid polygon points', error);
|
|
3176
|
+
}
|
|
3177
|
+
const uniquePointKeys = new Set(polygonPoints.map(point => `${point.x}:${point.y}`));
|
|
3178
|
+
if (uniquePointKeys.size !== polygonPoints.length) {
|
|
3179
|
+
return rejectInvalidMask('polygon points must not contain duplicates');
|
|
3180
|
+
}
|
|
3181
|
+
const doubleArea = polygonPoints.reduce((area, point, index) => {
|
|
3182
|
+
const nextPoint = polygonPoints[(index + 1) % polygonPoints.length];
|
|
3183
|
+
return area + point.x * nextPoint.y - nextPoint.x * point.y;
|
|
3184
|
+
}, 0);
|
|
3185
|
+
if (Math.abs(doubleArea) < 0.000001) {
|
|
3186
|
+
return rejectInvalidMask('polygon masks must have a non-zero area');
|
|
2766
3187
|
}
|
|
2767
3188
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2768
3189
|
left, top,
|
|
@@ -2775,10 +3196,16 @@ function ensureFabric() {
|
|
|
2775
3196
|
}
|
|
2776
3197
|
case 'rect':
|
|
2777
3198
|
default:
|
|
3199
|
+
try {
|
|
3200
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, 'width', 'rx', { nonNegative: true });
|
|
3201
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, 'height', 'ry', { nonNegative: true });
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
return rejectInvalidMask('invalid rectangle corner radius', error);
|
|
3204
|
+
}
|
|
2778
3205
|
mask = new fabric.Rect({
|
|
2779
3206
|
left, top,
|
|
2780
|
-
width:
|
|
2781
|
-
height:
|
|
3207
|
+
width: maskConfig.width,
|
|
3208
|
+
height: maskConfig.height,
|
|
2782
3209
|
fill: maskConfig.color,
|
|
2783
3210
|
opacity: maskConfig.alpha,
|
|
2784
3211
|
angle: maskConfig.angle,
|
|
@@ -2819,12 +3246,12 @@ function ensureFabric() {
|
|
|
2819
3246
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2820
3247
|
});
|
|
2821
3248
|
this._rebindMaskEvents(mask);
|
|
2822
|
-
this.
|
|
3249
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2823
3250
|
|
|
2824
3251
|
// Store placement values so the next mask can be positioned beside this one.
|
|
2825
3252
|
this._lastMaskInitialLeft = left;
|
|
2826
3253
|
this._lastMaskInitialTop = top;
|
|
2827
|
-
this._lastMaskInitialWidth =
|
|
3254
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2828
3255
|
|
|
2829
3256
|
const maskId = ++this.maskCounter;
|
|
2830
3257
|
mask.set({
|
|
@@ -2842,7 +3269,12 @@ function ensureFabric() {
|
|
|
2842
3269
|
this.canvas.renderAll();
|
|
2843
3270
|
this.saveState();
|
|
2844
3271
|
|
|
2845
|
-
if (typeof maskConfig.onCreate === 'function')
|
|
3272
|
+
if (typeof maskConfig.onCreate === 'function') {
|
|
3273
|
+
this._emitSafeCallback(
|
|
3274
|
+
() => maskConfig.onCreate(mask, this.canvas),
|
|
3275
|
+
'createMask onCreate callback failed'
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
2846
3278
|
return mask;
|
|
2847
3279
|
}
|
|
2848
3280
|
|
|
@@ -3057,8 +3489,15 @@ function ensureFabric() {
|
|
|
3057
3489
|
this._removeLabelForMask(mask);
|
|
3058
3490
|
let textObject = null;
|
|
3059
3491
|
if (this.options.label && typeof this.options.label.create === 'function') {
|
|
3060
|
-
|
|
3061
|
-
|
|
3492
|
+
let didLabelCreateThrow = false;
|
|
3493
|
+
try {
|
|
3494
|
+
textObject = this.options.label.create(mask, fabric);
|
|
3495
|
+
} catch (error) {
|
|
3496
|
+
didLabelCreateThrow = true;
|
|
3497
|
+
this._reportWarning('label.create() failed; using the default label', error);
|
|
3498
|
+
textObject = null;
|
|
3499
|
+
}
|
|
3500
|
+
if (!didLabelCreateThrow && (!textObject || typeof textObject.set !== 'function')) {
|
|
3062
3501
|
this._reportWarning('label.create() returned an invalid Fabric object; using the default label');
|
|
3063
3502
|
textObject = null;
|
|
3064
3503
|
}
|
|
@@ -3079,7 +3518,12 @@ function ensureFabric() {
|
|
|
3079
3518
|
};
|
|
3080
3519
|
if (this.options.label) {
|
|
3081
3520
|
if (typeof this.options.label.getText === 'function') {
|
|
3082
|
-
|
|
3521
|
+
try {
|
|
3522
|
+
labelText = this.options.label.getText(mask, this._getMaskCreationIndex(mask));
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
this._reportWarning('label.getText() failed; using the mask name', error);
|
|
3525
|
+
labelText = mask.maskName;
|
|
3526
|
+
}
|
|
3083
3527
|
}
|
|
3084
3528
|
// Merge external styles
|
|
3085
3529
|
if (this.options.label.textOptions) {
|
|
@@ -3274,6 +3718,7 @@ function ensureFabric() {
|
|
|
3274
3718
|
this._assertIdleForOperation('mergeMasks');
|
|
3275
3719
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3276
3720
|
if (!masks.length) return;
|
|
3721
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
3277
3722
|
const beforeJson = this._serializeCanvasState();
|
|
3278
3723
|
const operationToken = this._beginBusyOperation('mergeMasks');
|
|
3279
3724
|
|
|
@@ -3294,12 +3739,13 @@ function ensureFabric() {
|
|
|
3294
3739
|
preserveScroll: true,
|
|
3295
3740
|
resetMaskCounter: false
|
|
3296
3741
|
}));
|
|
3742
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
3297
3743
|
const afterJson = this._serializeCanvasState();
|
|
3298
3744
|
this._pushStateTransition(beforeJson, afterJson);
|
|
3299
3745
|
} catch (error) {
|
|
3300
3746
|
this._reportError('merge error', error);
|
|
3301
3747
|
try {
|
|
3302
|
-
await this.loadFromState(beforeJson);
|
|
3748
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
3303
3749
|
} catch (restoreError) {
|
|
3304
3750
|
this._reportError('mergeMasks rollback failed', restoreError);
|
|
3305
3751
|
}
|
|
@@ -3361,13 +3807,19 @@ function ensureFabric() {
|
|
|
3361
3807
|
*/
|
|
3362
3808
|
async exportImageBase64(options = {}) {
|
|
3363
3809
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
3810
|
+
options = options || {};
|
|
3364
3811
|
this._assertIdleForOperation('exportImageBase64', options);
|
|
3812
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3813
|
+
const operationToken = isNestedOperation
|
|
3814
|
+
? this._getInternalOperationToken(options)
|
|
3815
|
+
: this._beginBusyOperation('exportImageBase64');
|
|
3365
3816
|
const exportImageArea = typeof options.exportImageArea === 'boolean' ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
3366
3817
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
3367
3818
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
3368
3819
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
3369
3820
|
|
|
3370
|
-
|
|
3821
|
+
try {
|
|
3822
|
+
if (!exportImageArea) {
|
|
3371
3823
|
const masks = this.canvas.getObjects().filter(object => object.maskId || object.maskLabel);
|
|
3372
3824
|
const editableMasks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3373
3825
|
const maskVisibilityBackups = masks.map(mask => ({ object: mask, visible: mask.visible }));
|
|
@@ -3399,15 +3851,15 @@ function ensureFabric() {
|
|
|
3399
3851
|
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3400
3852
|
this.canvas.renderAll();
|
|
3401
3853
|
}
|
|
3402
|
-
|
|
3854
|
+
}
|
|
3403
3855
|
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3856
|
+
// Render masks as export shapes without mutating their editable styles.
|
|
3857
|
+
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3858
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3859
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3860
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
3409
3861
|
|
|
3410
|
-
|
|
3862
|
+
try {
|
|
3411
3863
|
// Labels are UI overlays and should not be part of the flattened export.
|
|
3412
3864
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
3413
3865
|
this.canvas.discardActiveObject();
|
|
@@ -3438,6 +3890,9 @@ function ensureFabric() {
|
|
|
3438
3890
|
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3439
3891
|
this.canvas.renderAll();
|
|
3440
3892
|
}
|
|
3893
|
+
} finally {
|
|
3894
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3895
|
+
}
|
|
3441
3896
|
}
|
|
3442
3897
|
|
|
3443
3898
|
/**
|
|
@@ -3471,7 +3926,12 @@ function ensureFabric() {
|
|
|
3471
3926
|
*/
|
|
3472
3927
|
async exportImageFile(options = {}) {
|
|
3473
3928
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
3474
|
-
|
|
3929
|
+
options = options || {};
|
|
3930
|
+
this._assertIdleForOperation('exportImageFile', options);
|
|
3931
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3932
|
+
const operationToken = isNestedOperation
|
|
3933
|
+
? this._getInternalOperationToken(options)
|
|
3934
|
+
: this._beginBusyOperation('exportImageFile');
|
|
3475
3935
|
const {
|
|
3476
3936
|
mergeMask = true,
|
|
3477
3937
|
fileType = 'jpeg',
|
|
@@ -3483,52 +3943,56 @@ function ensureFabric() {
|
|
|
3483
3943
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3484
3944
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
3485
3945
|
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3946
|
+
try {
|
|
3947
|
+
// Generate the data URL in the requested export mode.
|
|
3948
|
+
let imageBase64;
|
|
3949
|
+
if (mergeMask) {
|
|
3950
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3951
|
+
exportImageArea: true,
|
|
3952
|
+
multiplier,
|
|
3953
|
+
quality: normalizedQuality,
|
|
3954
|
+
fileType: safeFileType
|
|
3955
|
+
}));
|
|
3956
|
+
} else {
|
|
3957
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3958
|
+
exportImageArea: false,
|
|
3959
|
+
multiplier,
|
|
3960
|
+
quality: normalizedQuality,
|
|
3961
|
+
fileType: safeFileType
|
|
3962
|
+
}));
|
|
3963
|
+
}
|
|
3503
3964
|
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3965
|
+
// Convert to the required image format
|
|
3966
|
+
let imageDataUrl = imageBase64;
|
|
3967
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3968
|
+
// Redraw the exported data URL when the browser returned a different image format.
|
|
3969
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3970
|
+
const imageElement = new window.Image();
|
|
3971
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3972
|
+
imageElement.onload = () => {
|
|
3973
|
+
try {
|
|
3974
|
+
const offscreenCanvas = document.createElement('canvas');
|
|
3975
|
+
offscreenCanvas.width = imageElement.width;
|
|
3976
|
+
offscreenCanvas.height = imageElement.height;
|
|
3977
|
+
const context = offscreenCanvas.getContext('2d');
|
|
3978
|
+
if (!context) throw new Error('Unable to create 2D canvas context for export conversion');
|
|
3979
|
+
context.drawImage(imageElement, 0, 0);
|
|
3980
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3981
|
+
resolve(convertedDataUrl);
|
|
3982
|
+
} catch (error) { reject(error); }
|
|
3983
|
+
};
|
|
3984
|
+
imageElement.onerror = reject;
|
|
3985
|
+
imageElement.src = imageBase64;
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3527
3988
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3989
|
+
// Convert the final data URL to a File with the requested MIME type.
|
|
3990
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3991
|
+
const mime = `image/${safeFileType}`;
|
|
3992
|
+
return new File([bytes], fileName, { type: mime });
|
|
3993
|
+
} finally {
|
|
3994
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3995
|
+
}
|
|
3532
3996
|
}
|
|
3533
3997
|
|
|
3534
3998
|
_clearMaskPlacementMemory() {
|
|
@@ -3538,7 +4002,7 @@ function ensureFabric() {
|
|
|
3538
4002
|
this._lastMaskInitialWidth = null;
|
|
3539
4003
|
}
|
|
3540
4004
|
|
|
3541
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
4005
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
3542
4006
|
this._reportError(message, error);
|
|
3543
4007
|
|
|
3544
4008
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
@@ -3551,7 +4015,7 @@ function ensureFabric() {
|
|
|
3551
4015
|
|
|
3552
4016
|
if (beforeJson) {
|
|
3553
4017
|
try {
|
|
3554
|
-
await this.loadFromState(beforeJson);
|
|
4018
|
+
await this.loadFromState(beforeJson, options);
|
|
3555
4019
|
} catch (restoreError) {
|
|
3556
4020
|
this._reportError('applyCrop: rollback failed', restoreError);
|
|
3557
4021
|
}
|
|
@@ -3596,6 +4060,54 @@ function ensureFabric() {
|
|
|
3596
4060
|
this._cropHandlers = [];
|
|
3597
4061
|
}
|
|
3598
4062
|
|
|
4063
|
+
_getCropRectContentBounds(cropRect) {
|
|
4064
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
4065
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
4066
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
4067
|
+
return {
|
|
4068
|
+
left: Number(cropRect.left) || 0,
|
|
4069
|
+
top: Number(cropRect.top) || 0,
|
|
4070
|
+
width,
|
|
4071
|
+
height
|
|
4072
|
+
};
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
_getCropRectRawBounds(cropRect) {
|
|
4076
|
+
if (!cropRect) return { left: NaN, top: NaN, width: NaN, height: NaN };
|
|
4077
|
+
return {
|
|
4078
|
+
left: Number(cropRect.left),
|
|
4079
|
+
top: Number(cropRect.top),
|
|
4080
|
+
width: Number(cropRect.width) * Math.abs(Number(cropRect.scaleX)),
|
|
4081
|
+
height: Number(cropRect.height) * Math.abs(Number(cropRect.scaleY))
|
|
4082
|
+
};
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
_isValidCropRegion(cropBounds, imageBounds) {
|
|
4086
|
+
if (!cropBounds || !imageBounds) return false;
|
|
4087
|
+
const left = Number(cropBounds.left);
|
|
4088
|
+
const top = Number(cropBounds.top);
|
|
4089
|
+
const width = Number(cropBounds.width);
|
|
4090
|
+
const height = Number(cropBounds.height);
|
|
4091
|
+
const imageLeft = Number(imageBounds.left);
|
|
4092
|
+
const imageTop = Number(imageBounds.top);
|
|
4093
|
+
const imageWidth = Number(imageBounds.width);
|
|
4094
|
+
const imageHeight = Number(imageBounds.height);
|
|
4095
|
+
if (![left, top, width, height, imageLeft, imageTop, imageWidth, imageHeight].every(Number.isFinite)) return false;
|
|
4096
|
+
if (width <= 0 || height <= 0 || imageWidth <= 0 || imageHeight <= 0) return false;
|
|
4097
|
+
|
|
4098
|
+
const right = left + width;
|
|
4099
|
+
const bottom = top + height;
|
|
4100
|
+
const imageRight = imageLeft + imageWidth;
|
|
4101
|
+
const imageBottom = imageTop + imageHeight;
|
|
4102
|
+
const overlapsImage = left < imageRight && right > imageLeft && top < imageBottom && bottom > imageTop;
|
|
4103
|
+
if (!overlapsImage) return false;
|
|
4104
|
+
|
|
4105
|
+
const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
|
|
4106
|
+
const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
|
|
4107
|
+
if (!Number.isFinite(canvasWidth) || !Number.isFinite(canvasHeight) || canvasWidth <= 0 || canvasHeight <= 0) return false;
|
|
4108
|
+
return left < canvasWidth && right > 0 && top < canvasHeight && bottom > 0;
|
|
4109
|
+
}
|
|
4110
|
+
|
|
3599
4111
|
/**
|
|
3600
4112
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
3601
4113
|
*
|
|
@@ -3607,6 +4119,10 @@ function ensureFabric() {
|
|
|
3607
4119
|
*/
|
|
3608
4120
|
enterCropMode() {
|
|
3609
4121
|
if (!this.canvas || !this.originalImage || this._cropMode) return;
|
|
4122
|
+
if (this._isApplyingCrop) {
|
|
4123
|
+
this._reportWarning('enterCropMode ignored because a crop is already being applied');
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
3610
4126
|
if (!this._canMutateNow('enterCropMode')) return;
|
|
3611
4127
|
if (!this.isImageLoaded()) return;
|
|
3612
4128
|
this._removeCropRect();
|
|
@@ -3626,14 +4142,19 @@ function ensureFabric() {
|
|
|
3626
4142
|
const padding = (this.options.crop && this.options.crop.padding) ? this.options.crop.padding : 10;
|
|
3627
4143
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
3628
4144
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
3629
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
3630
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
4145
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
4146
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
3631
4147
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
3632
4148
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
3633
4149
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3634
4150
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3635
4151
|
const width = minCropWidth;
|
|
3636
4152
|
const height = minCropHeight;
|
|
4153
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
4154
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
4155
|
+
this._cropRotationWarningEmitted = true;
|
|
4156
|
+
this._reportWarning('crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported');
|
|
4157
|
+
}
|
|
3637
4158
|
|
|
3638
4159
|
// Visual style for the temporary crop rectangle.
|
|
3639
4160
|
const cropRect = new fabric.Rect({
|
|
@@ -3645,8 +4166,8 @@ function ensureFabric() {
|
|
|
3645
4166
|
strokeWidth: 1,
|
|
3646
4167
|
strokeUniform: true,
|
|
3647
4168
|
selectable: true,
|
|
3648
|
-
hasRotatingPoint:
|
|
3649
|
-
lockRotation:
|
|
4169
|
+
hasRotatingPoint: false,
|
|
4170
|
+
lockRotation: true,
|
|
3650
4171
|
cornerSize: 8,
|
|
3651
4172
|
objectCaching: false,
|
|
3652
4173
|
originX: 'left',
|
|
@@ -3689,7 +4210,7 @@ function ensureFabric() {
|
|
|
3689
4210
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3690
4211
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3691
4212
|
cropRect.setCoords();
|
|
3692
|
-
const cropBounds =
|
|
4213
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3693
4214
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3694
4215
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3695
4216
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3741,6 +4262,10 @@ function ensureFabric() {
|
|
|
3741
4262
|
* @public
|
|
3742
4263
|
*/
|
|
3743
4264
|
cancelCrop() {
|
|
4265
|
+
if (this._isApplyingCrop) {
|
|
4266
|
+
this._reportWarning('cancelCrop ignored because a crop is already being applied');
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
3744
4269
|
if (!this.canvas || !this._cropMode) return;
|
|
3745
4270
|
this._removeCropRect();
|
|
3746
4271
|
this._restoreCropObjectState();
|
|
@@ -3767,11 +4292,26 @@ function ensureFabric() {
|
|
|
3767
4292
|
*/
|
|
3768
4293
|
async applyCrop() {
|
|
3769
4294
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
4295
|
+
if (this._isApplyingCrop) {
|
|
4296
|
+
this._reportWarning('applyCrop ignored because a crop is already being applied');
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
3770
4299
|
this._assertIdleForOperation('applyCrop');
|
|
4300
|
+
this._isApplyingCrop = true;
|
|
4301
|
+
const operationToken = this._beginBusyOperation('applyCrop');
|
|
4302
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3771
4303
|
|
|
4304
|
+
try {
|
|
3772
4305
|
// Fabric does not update control coordinates automatically after programmatic transforms.
|
|
3773
4306
|
this._cropRect.setCoords();
|
|
3774
|
-
|
|
4307
|
+
this.originalImage.setCoords();
|
|
4308
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
4309
|
+
const rawCropBounds = this._getCropRectRawBounds(this._cropRect);
|
|
4310
|
+
if (!this._isValidCropRegion(rawCropBounds, imageBounds)) {
|
|
4311
|
+
this._reportWarning('applyCrop: crop region is invalid');
|
|
4312
|
+
return;
|
|
4313
|
+
}
|
|
4314
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3775
4315
|
|
|
3776
4316
|
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3777
4317
|
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
@@ -3786,7 +4326,13 @@ function ensureFabric() {
|
|
|
3786
4326
|
beforeJson = null;
|
|
3787
4327
|
}
|
|
3788
4328
|
if (!beforeJson) {
|
|
3789
|
-
this.
|
|
4329
|
+
this._removeCropRect();
|
|
4330
|
+
this._cropMode = false;
|
|
4331
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
4332
|
+
this._prevSelectionSetting = undefined;
|
|
4333
|
+
this.canvas.discardActiveObject();
|
|
4334
|
+
this._updateUI();
|
|
4335
|
+
this.canvas.renderAll();
|
|
3790
4336
|
return;
|
|
3791
4337
|
}
|
|
3792
4338
|
|
|
@@ -3817,7 +4363,7 @@ function ensureFabric() {
|
|
|
3817
4363
|
this.canvas.renderAll();
|
|
3818
4364
|
}
|
|
3819
4365
|
} catch (error) {
|
|
3820
|
-
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error);
|
|
4366
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error, internalOptions);
|
|
3821
4367
|
return;
|
|
3822
4368
|
}
|
|
3823
4369
|
|
|
@@ -3838,13 +4384,13 @@ function ensureFabric() {
|
|
|
3838
4384
|
format: 'jpeg'
|
|
3839
4385
|
});
|
|
3840
4386
|
} catch (error) {
|
|
3841
|
-
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to create cropped image', error);
|
|
4387
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to create cropped image', error, internalOptions);
|
|
3842
4388
|
return;
|
|
3843
4389
|
}
|
|
3844
4390
|
|
|
3845
4391
|
// Load the cropped image as the new base image.
|
|
3846
4392
|
try {
|
|
3847
|
-
await this.loadImage(croppedBase64, { resetMaskCounter: false });
|
|
4393
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3848
4394
|
if (preservedMasks.length) {
|
|
3849
4395
|
preservedMasks.forEach(mask => {
|
|
3850
4396
|
this._rebindMaskEvents(mask);
|
|
@@ -3857,7 +4403,7 @@ function ensureFabric() {
|
|
|
3857
4403
|
this.canvas.renderAll();
|
|
3858
4404
|
}
|
|
3859
4405
|
} catch (error) {
|
|
3860
|
-
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: loadImage(croppedBase64) failed', error);
|
|
4406
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: loadImage(croppedBase64) failed', error, internalOptions);
|
|
3861
4407
|
return;
|
|
3862
4408
|
}
|
|
3863
4409
|
|
|
@@ -3879,6 +4425,10 @@ function ensureFabric() {
|
|
|
3879
4425
|
// Refresh UI state after crop completion.
|
|
3880
4426
|
this._updateUI();
|
|
3881
4427
|
this.canvas.renderAll();
|
|
4428
|
+
} finally {
|
|
4429
|
+
this._isApplyingCrop = false;
|
|
4430
|
+
this._endBusyOperation(operationToken);
|
|
4431
|
+
}
|
|
3882
4432
|
}
|
|
3883
4433
|
|
|
3884
4434
|
|
|
@@ -3913,10 +4463,12 @@ function ensureFabric() {
|
|
|
3913
4463
|
const isBusy = this.isBusy();
|
|
3914
4464
|
|
|
3915
4465
|
if (isInCropMode) {
|
|
3916
|
-
// Disable
|
|
4466
|
+
// Disable operation controls while keeping canvas interaction and viewport scrolling available.
|
|
4467
|
+
const cropInteractionKeys = new Set(['canvas', 'canvasContainer', 'imagePlaceholder', 'imgPlaceholder']);
|
|
3917
4468
|
for (const key of Object.keys(this.elements || {})) {
|
|
3918
4469
|
const element = this._getElement(key);
|
|
3919
4470
|
if (!element) continue;
|
|
4471
|
+
if (cropInteractionKeys.has(key)) continue;
|
|
3920
4472
|
if (key === 'applyCropButton' || key === 'cancelCropButton' || key === 'applyCropBtn' || key === 'cancelCropBtn') {
|
|
3921
4473
|
this._setDisabled(key, false);
|
|
3922
4474
|
} else {
|
|
@@ -3956,9 +4508,44 @@ function ensureFabric() {
|
|
|
3956
4508
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3957
4509
|
* @private
|
|
3958
4510
|
*/
|
|
4511
|
+
_rememberElementDisabledState(key, element) {
|
|
4512
|
+
if (!element) return;
|
|
4513
|
+
if (!this._elementOriginalDisabledState) this._elementOriginalDisabledState = new Map();
|
|
4514
|
+
if (this._elementOriginalDisabledState.has(key)) return;
|
|
4515
|
+
this._elementOriginalDisabledState.set(key, {
|
|
4516
|
+
element,
|
|
4517
|
+
hasDisabledProperty: 'disabled' in element,
|
|
4518
|
+
disabled: ('disabled' in element) ? !!element.disabled : undefined,
|
|
4519
|
+
ariaDisabled: element.getAttribute ? element.getAttribute('aria-disabled') : null,
|
|
4520
|
+
pointerEvents: element.style ? (element.style.pointerEvents || '') : ''
|
|
4521
|
+
});
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
_restoreElementDisabledStates() {
|
|
4525
|
+
if (!this._elementOriginalDisabledState) return;
|
|
4526
|
+
for (const state of this._elementOriginalDisabledState.values()) {
|
|
4527
|
+
const element = state && state.element;
|
|
4528
|
+
if (!element) continue;
|
|
4529
|
+
try {
|
|
4530
|
+
if (state.hasDisabledProperty && 'disabled' in element) {
|
|
4531
|
+
element.disabled = !!state.disabled;
|
|
4532
|
+
}
|
|
4533
|
+
if (element.getAttribute && element.setAttribute && element.removeAttribute) {
|
|
4534
|
+
if (state.ariaDisabled === null) {
|
|
4535
|
+
element.removeAttribute('aria-disabled');
|
|
4536
|
+
} else {
|
|
4537
|
+
element.setAttribute('aria-disabled', state.ariaDisabled);
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
if (element.style) element.style.pointerEvents = state.pointerEvents || '';
|
|
4541
|
+
} catch (error) { void error; }
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
|
|
3959
4545
|
_setDisabled(key, disabled) {
|
|
3960
4546
|
const element = this._getElement(key);
|
|
3961
4547
|
if (!element) return;
|
|
4548
|
+
this._rememberElementDisabledState(key, element);
|
|
3962
4549
|
if ('disabled' in element) {
|
|
3963
4550
|
element.disabled = !!disabled;
|
|
3964
4551
|
return;
|
|
@@ -3988,7 +4575,6 @@ function ensureFabric() {
|
|
|
3988
4575
|
* @private
|
|
3989
4576
|
*/
|
|
3990
4577
|
_updatePlaceholderStatus() {
|
|
3991
|
-
if (!this.options.showPlaceholder) return;
|
|
3992
4578
|
this._setPlaceholderVisible(!this.originalImage);
|
|
3993
4579
|
}
|
|
3994
4580
|
|
|
@@ -3999,10 +4585,11 @@ function ensureFabric() {
|
|
|
3999
4585
|
* @private
|
|
4000
4586
|
*/
|
|
4001
4587
|
_setPlaceholderVisible(show) {
|
|
4002
|
-
|
|
4588
|
+
const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
|
|
4589
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
|
|
4003
4590
|
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
4004
4591
|
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
4005
|
-
this._setElementVisible(canvasVisibilityElement, !
|
|
4592
|
+
this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
|
|
4006
4593
|
}
|
|
4007
4594
|
}
|
|
4008
4595
|
|
|
@@ -4088,6 +4675,9 @@ function ensureFabric() {
|
|
|
4088
4675
|
} catch (error) { void error; }
|
|
4089
4676
|
|
|
4090
4677
|
if (this._cropRect) this._removeCropRect();
|
|
4678
|
+
this._isApplyingCrop = false;
|
|
4679
|
+
|
|
4680
|
+
try { this._restoreElementDisabledStates(); } catch (error) { void error; }
|
|
4091
4681
|
|
|
4092
4682
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
4093
4683
|
try { this._restoreContainerOverflowState(); } catch (error) { void error; }
|
|
@@ -4125,6 +4715,7 @@ function ensureFabric() {
|
|
|
4125
4715
|
this._handlersByElementKey = {};
|
|
4126
4716
|
this._elementCache = {};
|
|
4127
4717
|
this._elementOriginalPointerEvents = new Map();
|
|
4718
|
+
this._elementOriginalDisabledState = new Map();
|
|
4128
4719
|
this._clearMaskPlacementMemory();
|
|
4129
4720
|
this.originalImage = null;
|
|
4130
4721
|
this.baseImageScale = 1;
|
|
@@ -4133,6 +4724,7 @@ function ensureFabric() {
|
|
|
4133
4724
|
this.isAnimating = false;
|
|
4134
4725
|
this._isLoading = false;
|
|
4135
4726
|
this._cropMode = false;
|
|
4727
|
+
this._isApplyingCrop = false;
|
|
4136
4728
|
this._cropRect = null;
|
|
4137
4729
|
this._cropHandlers = [];
|
|
4138
4730
|
this._cropPrevEvented = null;
|
|
@@ -4168,6 +4760,7 @@ function ensureFabric() {
|
|
|
4168
4760
|
|
|
4169
4761
|
/**
|
|
4170
4762
|
* @callback HistoryTaskCallback
|
|
4763
|
+
* @param {Object} [options] - Internal operation options passed by the editor.
|
|
4171
4764
|
* @returns {void|Promise<void>} Result of a history operation.
|
|
4172
4765
|
*/
|
|
4173
4766
|
|
|
@@ -4304,12 +4897,13 @@ function ensureFabric() {
|
|
|
4304
4897
|
* @param {number} [maxSize=50] - Maximum number of commands to keep in history.
|
|
4305
4898
|
*/
|
|
4306
4899
|
constructor(maxSize = 50) {
|
|
4900
|
+
const numericMaxSize = Number(maxSize);
|
|
4307
4901
|
/** @type {Array<Command>} */
|
|
4308
4902
|
this.history = [];
|
|
4309
4903
|
/** @type {number} */
|
|
4310
4904
|
this.currentIndex = -1;
|
|
4311
4905
|
/** @type {number} */
|
|
4312
|
-
this.maxSize =
|
|
4906
|
+
this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
|
|
4313
4907
|
/** @type {Promise<void>} */
|
|
4314
4908
|
this.pending = Promise.resolve();
|
|
4315
4909
|
}
|
|
@@ -4389,11 +4983,11 @@ function ensureFabric() {
|
|
|
4389
4983
|
*
|
|
4390
4984
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
4391
4985
|
*/
|
|
4392
|
-
undo() {
|
|
4986
|
+
undo(options = {}) {
|
|
4393
4987
|
return this.enqueue(async () => {
|
|
4394
4988
|
if (this.currentIndex >= 0) {
|
|
4395
4989
|
const index = this.currentIndex;
|
|
4396
|
-
await this.history[index].undo();
|
|
4990
|
+
await this.history[index].undo(options);
|
|
4397
4991
|
this.currentIndex = index - 1;
|
|
4398
4992
|
}
|
|
4399
4993
|
});
|
|
@@ -4404,11 +4998,11 @@ function ensureFabric() {
|
|
|
4404
4998
|
*
|
|
4405
4999
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
4406
5000
|
*/
|
|
4407
|
-
redo() {
|
|
5001
|
+
redo(options = {}) {
|
|
4408
5002
|
return this.enqueue(async () => {
|
|
4409
5003
|
if (this.currentIndex < this.history.length - 1) {
|
|
4410
5004
|
const index = this.currentIndex + 1;
|
|
4411
|
-
await this.history[index].execute();
|
|
5005
|
+
await this.history[index].execute(options);
|
|
4412
5006
|
this.currentIndex = index;
|
|
4413
5007
|
}
|
|
4414
5008
|
});
|