@bensitu/image-editor 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -16
- package/dist/image-editor.cjs +241 -19
- package/dist/image-editor.cjs.map +2 -2
- package/dist/image-editor.esm.js +241 -19
- package/dist/image-editor.esm.js.map +2 -2
- package/dist/image-editor.esm.min.js +2 -2
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +2 -2
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +241 -19
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +241 -19
- package/dist/image-editor.js.map +2 -2
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +2 -0
- package/package.json +1 -1
- package/src/image-editor.js +258 -20
package/image-editor.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ declare module '@bensitu/image-editor' {
|
|
|
56
56
|
|
|
57
57
|
exportMultiplier?: number;
|
|
58
58
|
maxExportPixels?: number;
|
|
59
|
+
/** Maximum undo/redo history entries to keep. Large base64 images can make each snapshot expensive. */
|
|
60
|
+
maxHistorySize?: number;
|
|
59
61
|
exportImageAreaByDefault?: boolean;
|
|
60
62
|
|
|
61
63
|
defaultMaskWidth?: number;
|
package/package.json
CHANGED
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.
|
|
@@ -133,6 +133,7 @@ function ensureFabric() {
|
|
|
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
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.
|
|
136
137
|
* @param {boolean} [options.exportImageAreaByDefault=true] - Export only the image area (clipped to masks).
|
|
137
138
|
* @param {number} [options.defaultMaskWidth=50] - Default width of new masks.
|
|
138
139
|
* @param {number} [options.defaultMaskHeight=80] - Default height of new masks.
|
|
@@ -204,6 +205,7 @@ function ensureFabric() {
|
|
|
204
205
|
|
|
205
206
|
exportMultiplier: 1,
|
|
206
207
|
maxExportPixels: 50000000,
|
|
208
|
+
maxHistorySize: 50,
|
|
207
209
|
exportImageAreaByDefault: true,
|
|
208
210
|
|
|
209
211
|
defaultMaskWidth: 50,
|
|
@@ -236,6 +238,7 @@ function ensureFabric() {
|
|
|
236
238
|
...userCrop
|
|
237
239
|
}
|
|
238
240
|
};
|
|
241
|
+
this._normalizeOptions();
|
|
239
242
|
|
|
240
243
|
// Verify that Fabric.js is present before any canvas work starts.
|
|
241
244
|
this._fabricLoaded = !!ensureFabric();
|
|
@@ -260,11 +263,12 @@ function ensureFabric() {
|
|
|
260
263
|
this._activeOperationToken = null;
|
|
261
264
|
this.elements = {};
|
|
262
265
|
this.isImageLoadedToCanvas = false;
|
|
263
|
-
this.maxHistorySize =
|
|
266
|
+
this.maxHistorySize = this.options.maxHistorySize;
|
|
264
267
|
|
|
265
268
|
this._handlersByElementKey = {};
|
|
266
269
|
this._elementCache = {};
|
|
267
270
|
this._elementOriginalPointerEvents = new Map();
|
|
271
|
+
this._elementOriginalDisabledState = new Map();
|
|
268
272
|
|
|
269
273
|
this._lastMask = null;
|
|
270
274
|
this._lastMaskInitialLeft = null;
|
|
@@ -273,6 +277,7 @@ function ensureFabric() {
|
|
|
273
277
|
this._lastSnapshot = null;
|
|
274
278
|
|
|
275
279
|
this._cropMode = false;
|
|
280
|
+
this._isApplyingCrop = false;
|
|
276
281
|
this._cropRect = null;
|
|
277
282
|
this._cropHandlers = [];
|
|
278
283
|
this._cropPrevEvented = null;
|
|
@@ -378,6 +383,8 @@ function ensureFabric() {
|
|
|
378
383
|
this._activeOperationName = null;
|
|
379
384
|
this._activeOperationToken = null;
|
|
380
385
|
this._elementOriginalPointerEvents = new Map();
|
|
386
|
+
this._elementOriginalDisabledState = new Map();
|
|
387
|
+
this._isApplyingCrop = false;
|
|
381
388
|
this._containerOriginalOverflow = null;
|
|
382
389
|
this._lastContainerViewportSize = null;
|
|
383
390
|
this._canvasElementOriginalStyle = null;
|
|
@@ -513,6 +520,66 @@ function ensureFabric() {
|
|
|
513
520
|
);
|
|
514
521
|
}
|
|
515
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
|
+
|
|
516
583
|
_reportError(message, error = null) {
|
|
517
584
|
const handler = this.options && this.options.onError;
|
|
518
585
|
if (typeof handler !== 'function') return;
|
|
@@ -535,10 +602,19 @@ function ensureFabric() {
|
|
|
535
602
|
}
|
|
536
603
|
}
|
|
537
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
|
+
|
|
538
614
|
_notifyImageLoaded() {
|
|
539
615
|
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
540
616
|
const callback = typeof optionsCallback === 'function' ? optionsCallback : this.onImageLoaded;
|
|
541
|
-
|
|
617
|
+
this._emitSafeCallback(callback, 'onImageLoaded callback failed');
|
|
542
618
|
}
|
|
543
619
|
|
|
544
620
|
/**
|
|
@@ -804,7 +880,6 @@ function ensureFabric() {
|
|
|
804
880
|
_loadImageFile(file) {
|
|
805
881
|
if (!this._isSupportedImageFile(file)) {
|
|
806
882
|
const error = new Error('Selected file is not a supported image');
|
|
807
|
-
this._reportError('Selected file is not a supported image', error);
|
|
808
883
|
return Promise.reject(error);
|
|
809
884
|
}
|
|
810
885
|
|
|
@@ -878,6 +953,7 @@ function ensureFabric() {
|
|
|
878
953
|
? this._getInternalOperationToken(options)
|
|
879
954
|
: this._beginBusyOperation('loadImage');
|
|
880
955
|
let transaction = null;
|
|
956
|
+
let shouldNotifyImageLoaded;
|
|
881
957
|
|
|
882
958
|
try {
|
|
883
959
|
this._isLoading = true;
|
|
@@ -981,8 +1057,7 @@ function ensureFabric() {
|
|
|
981
1057
|
this._updateUI();
|
|
982
1058
|
this.canvas.renderAll();
|
|
983
1059
|
this._lastSnapshot = this._captureCanvasStateOrThrow('loadImage');
|
|
984
|
-
|
|
985
|
-
this._notifyImageLoaded();
|
|
1060
|
+
shouldNotifyImageLoaded = true;
|
|
986
1061
|
} catch (error) {
|
|
987
1062
|
await this._rollbackLoadImageTransaction(
|
|
988
1063
|
transaction,
|
|
@@ -994,6 +1069,9 @@ function ensureFabric() {
|
|
|
994
1069
|
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
995
1070
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
996
1071
|
}
|
|
1072
|
+
if (shouldNotifyImageLoaded && !this._disposed && this.canvas) {
|
|
1073
|
+
this._notifyImageLoaded();
|
|
1074
|
+
}
|
|
997
1075
|
}
|
|
998
1076
|
|
|
999
1077
|
/**
|
|
@@ -1021,6 +1099,7 @@ function ensureFabric() {
|
|
|
1021
1099
|
return !!(
|
|
1022
1100
|
this.isAnimating ||
|
|
1023
1101
|
this._cropMode ||
|
|
1102
|
+
this._isApplyingCrop ||
|
|
1024
1103
|
this._isLoading ||
|
|
1025
1104
|
this._activeOperationToken ||
|
|
1026
1105
|
(this.animationQueue && this.animationQueue.isBusy())
|
|
@@ -1908,7 +1987,24 @@ function ensureFabric() {
|
|
|
1908
1987
|
_getJpegBackgroundColor() {
|
|
1909
1988
|
const backgroundColor = String(this.options.backgroundColor || '').trim();
|
|
1910
1989
|
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
|
|
1911
|
-
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
|
+
}
|
|
1912
2008
|
}
|
|
1913
2009
|
|
|
1914
2010
|
_isTransparentCssColor(color) {
|
|
@@ -2364,10 +2460,12 @@ function ensureFabric() {
|
|
|
2364
2460
|
async _scaleImageImpl(factor, options = {}) {
|
|
2365
2461
|
if (!this.originalImage || this._disposed) return;
|
|
2366
2462
|
if (this.isAnimating) return;
|
|
2463
|
+
const numericFactor = Number(factor);
|
|
2464
|
+
if (!Number.isFinite(numericFactor)) return;
|
|
2367
2465
|
const saveHistory = options.saveHistory !== false;
|
|
2368
2466
|
let didStartAnimation = false;
|
|
2369
2467
|
try {
|
|
2370
|
-
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale,
|
|
2468
|
+
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, numericFactor));
|
|
2371
2469
|
this.currentScale = factor;
|
|
2372
2470
|
this.isAnimating = true;
|
|
2373
2471
|
didStartAnimation = true;
|
|
@@ -2442,7 +2540,8 @@ function ensureFabric() {
|
|
|
2442
2540
|
async _rotateImageImpl(degrees, options = {}) {
|
|
2443
2541
|
if (!this.originalImage || this._disposed) return;
|
|
2444
2542
|
if (this.isAnimating) return;
|
|
2445
|
-
|
|
2543
|
+
const numericDegrees = Number(degrees);
|
|
2544
|
+
if (!Number.isFinite(numericDegrees)) return;
|
|
2446
2545
|
const saveHistory = options.saveHistory !== false;
|
|
2447
2546
|
const image = this.originalImage;
|
|
2448
2547
|
const previousOriginX = image.originX || 'left';
|
|
@@ -2451,6 +2550,7 @@ function ensureFabric() {
|
|
|
2451
2550
|
let didStartAnimation = false;
|
|
2452
2551
|
let didCompleteRotation = false;
|
|
2453
2552
|
try {
|
|
2553
|
+
degrees = numericDegrees;
|
|
2454
2554
|
this.currentRotation = degrees;
|
|
2455
2555
|
this.isAnimating = true;
|
|
2456
2556
|
didStartAnimation = true;
|
|
@@ -3018,7 +3118,11 @@ function ensureFabric() {
|
|
|
3018
3118
|
|
|
3019
3119
|
let mask;
|
|
3020
3120
|
if (typeof maskConfig.fabricGenerator === 'function') {
|
|
3021
|
-
|
|
3121
|
+
try {
|
|
3122
|
+
mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
return rejectInvalidMask('fabricGenerator failed', error);
|
|
3125
|
+
}
|
|
3022
3126
|
} else {
|
|
3023
3127
|
switch (shapeType) {
|
|
3024
3128
|
case 'circle':
|
|
@@ -3070,6 +3174,17 @@ function ensureFabric() {
|
|
|
3070
3174
|
} catch (error) {
|
|
3071
3175
|
return rejectInvalidMask('invalid polygon points', error);
|
|
3072
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');
|
|
3187
|
+
}
|
|
3073
3188
|
mask = new fabric.Polygon(polygonPoints, {
|
|
3074
3189
|
left, top,
|
|
3075
3190
|
fill: maskConfig.color,
|
|
@@ -3154,7 +3269,12 @@ function ensureFabric() {
|
|
|
3154
3269
|
this.canvas.renderAll();
|
|
3155
3270
|
this.saveState();
|
|
3156
3271
|
|
|
3157
|
-
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
|
+
}
|
|
3158
3278
|
return mask;
|
|
3159
3279
|
}
|
|
3160
3280
|
|
|
@@ -3369,8 +3489,15 @@ function ensureFabric() {
|
|
|
3369
3489
|
this._removeLabelForMask(mask);
|
|
3370
3490
|
let textObject = null;
|
|
3371
3491
|
if (this.options.label && typeof this.options.label.create === 'function') {
|
|
3372
|
-
|
|
3373
|
-
|
|
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')) {
|
|
3374
3501
|
this._reportWarning('label.create() returned an invalid Fabric object; using the default label');
|
|
3375
3502
|
textObject = null;
|
|
3376
3503
|
}
|
|
@@ -3391,7 +3518,12 @@ function ensureFabric() {
|
|
|
3391
3518
|
};
|
|
3392
3519
|
if (this.options.label) {
|
|
3393
3520
|
if (typeof this.options.label.getText === 'function') {
|
|
3394
|
-
|
|
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
|
+
}
|
|
3395
3527
|
}
|
|
3396
3528
|
// Merge external styles
|
|
3397
3529
|
if (this.options.label.textOptions) {
|
|
@@ -3940,6 +4072,42 @@ function ensureFabric() {
|
|
|
3940
4072
|
};
|
|
3941
4073
|
}
|
|
3942
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
|
+
|
|
3943
4111
|
/**
|
|
3944
4112
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
3945
4113
|
*
|
|
@@ -3951,6 +4119,10 @@ function ensureFabric() {
|
|
|
3951
4119
|
*/
|
|
3952
4120
|
enterCropMode() {
|
|
3953
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
|
+
}
|
|
3954
4126
|
if (!this._canMutateNow('enterCropMode')) return;
|
|
3955
4127
|
if (!this.isImageLoaded()) return;
|
|
3956
4128
|
this._removeCropRect();
|
|
@@ -4090,6 +4262,10 @@ function ensureFabric() {
|
|
|
4090
4262
|
* @public
|
|
4091
4263
|
*/
|
|
4092
4264
|
cancelCrop() {
|
|
4265
|
+
if (this._isApplyingCrop) {
|
|
4266
|
+
this._reportWarning('cancelCrop ignored because a crop is already being applied');
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4093
4269
|
if (!this.canvas || !this._cropMode) return;
|
|
4094
4270
|
this._removeCropRect();
|
|
4095
4271
|
this._restoreCropObjectState();
|
|
@@ -4116,13 +4292,25 @@ function ensureFabric() {
|
|
|
4116
4292
|
*/
|
|
4117
4293
|
async applyCrop() {
|
|
4118
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
|
+
}
|
|
4119
4299
|
this._assertIdleForOperation('applyCrop');
|
|
4300
|
+
this._isApplyingCrop = true;
|
|
4120
4301
|
const operationToken = this._beginBusyOperation('applyCrop');
|
|
4121
4302
|
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
4122
4303
|
|
|
4123
4304
|
try {
|
|
4124
4305
|
// Fabric does not update control coordinates automatically after programmatic transforms.
|
|
4125
4306
|
this._cropRect.setCoords();
|
|
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
|
+
}
|
|
4126
4314
|
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
4127
4315
|
|
|
4128
4316
|
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
@@ -4138,7 +4326,13 @@ function ensureFabric() {
|
|
|
4138
4326
|
beforeJson = null;
|
|
4139
4327
|
}
|
|
4140
4328
|
if (!beforeJson) {
|
|
4141
|
-
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();
|
|
4142
4336
|
return;
|
|
4143
4337
|
}
|
|
4144
4338
|
|
|
@@ -4232,6 +4426,7 @@ function ensureFabric() {
|
|
|
4232
4426
|
this._updateUI();
|
|
4233
4427
|
this.canvas.renderAll();
|
|
4234
4428
|
} finally {
|
|
4429
|
+
this._isApplyingCrop = false;
|
|
4235
4430
|
this._endBusyOperation(operationToken);
|
|
4236
4431
|
}
|
|
4237
4432
|
}
|
|
@@ -4268,10 +4463,12 @@ function ensureFabric() {
|
|
|
4268
4463
|
const isBusy = this.isBusy();
|
|
4269
4464
|
|
|
4270
4465
|
if (isInCropMode) {
|
|
4271
|
-
// Disable
|
|
4466
|
+
// Disable operation controls while keeping canvas interaction and viewport scrolling available.
|
|
4467
|
+
const cropInteractionKeys = new Set(['canvas', 'canvasContainer', 'imagePlaceholder', 'imgPlaceholder']);
|
|
4272
4468
|
for (const key of Object.keys(this.elements || {})) {
|
|
4273
4469
|
const element = this._getElement(key);
|
|
4274
4470
|
if (!element) continue;
|
|
4471
|
+
if (cropInteractionKeys.has(key)) continue;
|
|
4275
4472
|
if (key === 'applyCropButton' || key === 'cancelCropButton' || key === 'applyCropBtn' || key === 'cancelCropBtn') {
|
|
4276
4473
|
this._setDisabled(key, false);
|
|
4277
4474
|
} else {
|
|
@@ -4311,9 +4508,44 @@ function ensureFabric() {
|
|
|
4311
4508
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
4312
4509
|
* @private
|
|
4313
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
|
+
|
|
4314
4545
|
_setDisabled(key, disabled) {
|
|
4315
4546
|
const element = this._getElement(key);
|
|
4316
4547
|
if (!element) return;
|
|
4548
|
+
this._rememberElementDisabledState(key, element);
|
|
4317
4549
|
if ('disabled' in element) {
|
|
4318
4550
|
element.disabled = !!disabled;
|
|
4319
4551
|
return;
|
|
@@ -4343,7 +4575,6 @@ function ensureFabric() {
|
|
|
4343
4575
|
* @private
|
|
4344
4576
|
*/
|
|
4345
4577
|
_updatePlaceholderStatus() {
|
|
4346
|
-
if (!this.options.showPlaceholder) return;
|
|
4347
4578
|
this._setPlaceholderVisible(!this.originalImage);
|
|
4348
4579
|
}
|
|
4349
4580
|
|
|
@@ -4354,10 +4585,11 @@ function ensureFabric() {
|
|
|
4354
4585
|
* @private
|
|
4355
4586
|
*/
|
|
4356
4587
|
_setPlaceholderVisible(show) {
|
|
4357
|
-
|
|
4588
|
+
const shouldShowPlaceholder = !!show && this.options.showPlaceholder !== false;
|
|
4589
|
+
if (this.placeholderElement) this._setElementVisible(this.placeholderElement, shouldShowPlaceholder);
|
|
4358
4590
|
const canvasVisibilityElement = this._getCanvasVisibilityElement();
|
|
4359
4591
|
if (canvasVisibilityElement && canvasVisibilityElement !== this.placeholderElement) {
|
|
4360
|
-
this._setElementVisible(canvasVisibilityElement, !
|
|
4592
|
+
this._setElementVisible(canvasVisibilityElement, !shouldShowPlaceholder);
|
|
4361
4593
|
}
|
|
4362
4594
|
}
|
|
4363
4595
|
|
|
@@ -4443,6 +4675,9 @@ function ensureFabric() {
|
|
|
4443
4675
|
} catch (error) { void error; }
|
|
4444
4676
|
|
|
4445
4677
|
if (this._cropRect) this._removeCropRect();
|
|
4678
|
+
this._isApplyingCrop = false;
|
|
4679
|
+
|
|
4680
|
+
try { this._restoreElementDisabledStates(); } catch (error) { void error; }
|
|
4446
4681
|
|
|
4447
4682
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
4448
4683
|
try { this._restoreContainerOverflowState(); } catch (error) { void error; }
|
|
@@ -4480,6 +4715,7 @@ function ensureFabric() {
|
|
|
4480
4715
|
this._handlersByElementKey = {};
|
|
4481
4716
|
this._elementCache = {};
|
|
4482
4717
|
this._elementOriginalPointerEvents = new Map();
|
|
4718
|
+
this._elementOriginalDisabledState = new Map();
|
|
4483
4719
|
this._clearMaskPlacementMemory();
|
|
4484
4720
|
this.originalImage = null;
|
|
4485
4721
|
this.baseImageScale = 1;
|
|
@@ -4488,6 +4724,7 @@ function ensureFabric() {
|
|
|
4488
4724
|
this.isAnimating = false;
|
|
4489
4725
|
this._isLoading = false;
|
|
4490
4726
|
this._cropMode = false;
|
|
4727
|
+
this._isApplyingCrop = false;
|
|
4491
4728
|
this._cropRect = null;
|
|
4492
4729
|
this._cropHandlers = [];
|
|
4493
4730
|
this._cropPrevEvented = null;
|
|
@@ -4660,12 +4897,13 @@ function ensureFabric() {
|
|
|
4660
4897
|
* @param {number} [maxSize=50] - Maximum number of commands to keep in history.
|
|
4661
4898
|
*/
|
|
4662
4899
|
constructor(maxSize = 50) {
|
|
4900
|
+
const numericMaxSize = Number(maxSize);
|
|
4663
4901
|
/** @type {Array<Command>} */
|
|
4664
4902
|
this.history = [];
|
|
4665
4903
|
/** @type {number} */
|
|
4666
4904
|
this.currentIndex = -1;
|
|
4667
4905
|
/** @type {number} */
|
|
4668
|
-
this.maxSize =
|
|
4906
|
+
this.maxSize = Number.isFinite(numericMaxSize) && numericMaxSize > 0 ? Math.floor(numericMaxSize) : 50;
|
|
4669
4907
|
/** @type {Promise<void>} */
|
|
4670
4908
|
this.pending = Promise.resolve();
|
|
4671
4909
|
}
|