@bensitu/image-editor 1.4.1 → 1.5.0
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 +445 -131
- package/dist/image-editor.esm.js +537 -190
- 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 +537 -190
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +537 -190
- 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 +61 -19
- package/package.json +2 -1
- package/src/image-editor.js +588 -191
package/src/image-editor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.
|
|
4
|
+
* @version 1.5.0
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
let fabric = null;
|
|
11
|
-
const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
|
|
11
|
+
const INTERNAL_OPERATION_TOKEN = Symbol.for('ImageEditorInternalOperation');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Returns the ambient global scope used to discover a globally loaded Fabric.js namespace.
|
|
@@ -284,7 +284,7 @@ function ensureFabric() {
|
|
|
284
284
|
this._disposed = false;
|
|
285
285
|
this._initialized = false;
|
|
286
286
|
|
|
287
|
-
this.onImageLoaded = typeof options.onImageLoaded === 'function' ? options.onImageLoaded : null;
|
|
287
|
+
this.onImageLoaded = typeof this.options.onImageLoaded === 'function' ? this.options.onImageLoaded : null;
|
|
288
288
|
|
|
289
289
|
this.animationQueue = new AnimationQueue();
|
|
290
290
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -338,10 +338,12 @@ function ensureFabric() {
|
|
|
338
338
|
* Use this method to set up the editor UI before interacting with it.
|
|
339
339
|
*
|
|
340
340
|
* @param {Object} [idMap={}] - Optional mapping from logical element names to actual DOM element IDs.
|
|
341
|
-
* Supported keys include: canvas, canvasContainer,
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
341
|
+
* Supported keys include: canvas, canvasContainer, imagePlaceholder, scalePercentageInput,
|
|
342
|
+
* rotateLeftDegreesInput, rotateRightDegreesInput, rotateLeftButton, rotateRightButton,
|
|
343
|
+
* createMaskButton, removeSelectedMaskButton, removeAllMasksButton, mergeMasksButton,
|
|
344
|
+
* downloadImageButton, maskList, zoomInButton, zoomOutButton, resetImageTransformButton,
|
|
345
|
+
* undoButton, redoButton, imageInput, uploadArea, enterCropModeButton, applyCropButton,
|
|
346
|
+
* and cancelCropButton. Deprecated 1.x names remain supported as aliases.
|
|
345
347
|
*
|
|
346
348
|
* @returns {void}
|
|
347
349
|
*
|
|
@@ -350,7 +352,7 @@ function ensureFabric() {
|
|
|
350
352
|
* @example
|
|
351
353
|
* editor.init({
|
|
352
354
|
* canvas: 'myFabricCanvasId',
|
|
353
|
-
*
|
|
355
|
+
* downloadImageButton: 'myDownloadButtonId'
|
|
354
356
|
* });
|
|
355
357
|
*/
|
|
356
358
|
init(idMap = {}) {
|
|
@@ -369,34 +371,54 @@ function ensureFabric() {
|
|
|
369
371
|
this._containerOriginalOverflow = null;
|
|
370
372
|
this._lastContainerViewportSize = null;
|
|
371
373
|
this._canvasElementOriginalStyle = null;
|
|
374
|
+
this._deprecatedElementKeyWarnings = new Set();
|
|
372
375
|
|
|
373
376
|
const defaults = {
|
|
374
377
|
canvas: 'fabricCanvas',
|
|
375
378
|
canvasContainer: null, // Pass an ID here if you have a scrollable viewport container
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
imagePlaceholder: 'imagePlaceholder',
|
|
380
|
+
imgPlaceholder: null,
|
|
381
|
+
scalePercentageInput: 'scalePercentageInput',
|
|
382
|
+
scaleRate: null,
|
|
383
|
+
rotateLeftDegreesInput: 'rotateLeftDegreesInput',
|
|
384
|
+
rotationLeftInput: null,
|
|
385
|
+
rotateRightDegreesInput: 'rotateRightDegreesInput',
|
|
386
|
+
rotationRightInput: null,
|
|
387
|
+
rotateLeftButton: 'rotateLeftButton',
|
|
388
|
+
rotateLeftBtn: null,
|
|
389
|
+
rotateRightButton: 'rotateRightButton',
|
|
390
|
+
rotateRightBtn: null,
|
|
391
|
+
createMaskButton: 'createMaskButton',
|
|
392
|
+
addMaskBtn: null,
|
|
393
|
+
removeSelectedMaskButton: 'removeSelectedMaskButton',
|
|
394
|
+
removeMaskBtn: null,
|
|
395
|
+
removeAllMasksButton: 'removeAllMasksButton',
|
|
396
|
+
removeAllMasksBtn: null,
|
|
397
|
+
mergeMasksButton: 'mergeMasksButton',
|
|
398
|
+
mergeBtn: null,
|
|
399
|
+
downloadImageButton: 'downloadImageButton',
|
|
400
|
+
downloadBtn: null,
|
|
387
401
|
maskList: 'maskList',
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
402
|
+
zoomInButton: 'zoomInButton',
|
|
403
|
+
zoomInBtn: null,
|
|
404
|
+
zoomOutButton: 'zoomOutButton',
|
|
405
|
+
zoomOutBtn: null,
|
|
406
|
+
resetImageTransformButton: 'resetImageTransformButton',
|
|
407
|
+
resetBtn: null,
|
|
408
|
+
undoButton: 'undoButton',
|
|
409
|
+
undoBtn: null,
|
|
410
|
+
redoButton: 'redoButton',
|
|
411
|
+
redoBtn: null,
|
|
393
412
|
imageInput: 'imageInput',
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
413
|
+
enterCropModeButton: 'enterCropModeButton',
|
|
414
|
+
cropBtn: null,
|
|
415
|
+
applyCropButton: 'applyCropButton',
|
|
416
|
+
applyCropBtn: null,
|
|
417
|
+
cancelCropButton: 'cancelCropButton',
|
|
418
|
+
cancelCropBtn: null
|
|
397
419
|
};
|
|
398
420
|
|
|
399
|
-
this.elements = {
|
|
421
|
+
this.elements = this._resolveElementIdMap(idMap || {}, defaults);
|
|
400
422
|
this._elementCache = {};
|
|
401
423
|
|
|
402
424
|
this._initCanvas();
|
|
@@ -413,6 +435,73 @@ function ensureFabric() {
|
|
|
413
435
|
}
|
|
414
436
|
}
|
|
415
437
|
|
|
438
|
+
_resolveElementIdMap(idMap, defaults) {
|
|
439
|
+
const resolved = { ...defaults, ...idMap };
|
|
440
|
+
|
|
441
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'imagePlaceholder', ['imgPlaceholder']);
|
|
442
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'scalePercentageInput', ['scaleRate']);
|
|
443
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'rotateLeftDegreesInput', ['rotationLeftInput']);
|
|
444
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'rotateRightDegreesInput', ['rotationRightInput']);
|
|
445
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'rotateLeftButton', 'rotateLeftBtn');
|
|
446
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'rotateRightButton', 'rotateRightBtn');
|
|
447
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'createMaskButton', 'addMaskBtn');
|
|
448
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'removeSelectedMaskButton', ['removeMaskBtn']);
|
|
449
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'removeAllMasksButton', 'removeAllMasksBtn');
|
|
450
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'mergeMasksButton', 'mergeBtn');
|
|
451
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'downloadImageButton', ['downloadBtn']);
|
|
452
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'zoomInButton', 'zoomInBtn');
|
|
453
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'zoomOutButton', 'zoomOutBtn');
|
|
454
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'resetImageTransformButton', 'resetBtn');
|
|
455
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'undoButton', 'undoBtn');
|
|
456
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'redoButton', 'redoBtn');
|
|
457
|
+
this._resolveElementAliases(resolved, idMap, defaults, 'enterCropModeButton', ['cropBtn']);
|
|
458
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'applyCropButton', 'applyCropBtn');
|
|
459
|
+
this._resolveElementAlias(resolved, idMap, defaults, 'cancelCropButton', 'cancelCropBtn');
|
|
460
|
+
|
|
461
|
+
return resolved;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
_resolveElementAlias(resolved, idMap, defaults, canonicalKey, deprecatedKey) {
|
|
465
|
+
this._resolveElementAliases(resolved, idMap, defaults, canonicalKey, [deprecatedKey]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
_resolveElementAliases(resolved, idMap, defaults, canonicalKey, deprecatedKeys) {
|
|
469
|
+
const hasCanonicalKey = Object.prototype.hasOwnProperty.call(idMap, canonicalKey);
|
|
470
|
+
|
|
471
|
+
if (hasCanonicalKey) {
|
|
472
|
+
resolved[canonicalKey] = idMap[canonicalKey];
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let deprecatedValue;
|
|
477
|
+
let hasDeprecatedValue = false;
|
|
478
|
+
for (const deprecatedKey of deprecatedKeys) {
|
|
479
|
+
if (Object.prototype.hasOwnProperty.call(idMap, deprecatedKey)) {
|
|
480
|
+
if (!hasDeprecatedValue) {
|
|
481
|
+
deprecatedValue = idMap[deprecatedKey];
|
|
482
|
+
hasDeprecatedValue = true;
|
|
483
|
+
}
|
|
484
|
+
this._warnDeprecatedElementIdKey(deprecatedKey, canonicalKey);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (hasDeprecatedValue) {
|
|
489
|
+
resolved[canonicalKey] = deprecatedValue;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
resolved[canonicalKey] = defaults[canonicalKey];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_warnDeprecatedElementIdKey(deprecatedKey, canonicalKey) {
|
|
497
|
+
if (!this._deprecatedElementKeyWarnings) this._deprecatedElementKeyWarnings = new Set();
|
|
498
|
+
if (this._deprecatedElementKeyWarnings.has(deprecatedKey)) return;
|
|
499
|
+
this._deprecatedElementKeyWarnings.add(deprecatedKey);
|
|
500
|
+
this._reportWarning(
|
|
501
|
+
`ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
416
505
|
_reportError(message, error = null) {
|
|
417
506
|
const handler = this.options && this.options.onError;
|
|
418
507
|
if (typeof handler !== 'function') return;
|
|
@@ -435,6 +524,12 @@ function ensureFabric() {
|
|
|
435
524
|
}
|
|
436
525
|
}
|
|
437
526
|
|
|
527
|
+
_notifyImageLoaded() {
|
|
528
|
+
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
529
|
+
const callback = typeof optionsCallback === 'function' ? optionsCallback : this.onImageLoaded;
|
|
530
|
+
if (typeof callback === 'function') callback();
|
|
531
|
+
}
|
|
532
|
+
|
|
438
533
|
/**
|
|
439
534
|
* Initializes the Fabric canvas, viewport elements, and selection event handlers.
|
|
440
535
|
*
|
|
@@ -460,7 +555,7 @@ function ensureFabric() {
|
|
|
460
555
|
this.containerElement = canvasElement.parentElement;
|
|
461
556
|
}
|
|
462
557
|
|
|
463
|
-
this.placeholderElement = this._getElement('
|
|
558
|
+
this.placeholderElement = this._getElement('imagePlaceholder') || null;
|
|
464
559
|
|
|
465
560
|
// Prefer a measured container size when it is available.
|
|
466
561
|
let initialWidth = this.options.canvasWidth;
|
|
@@ -628,23 +723,23 @@ function ensureFabric() {
|
|
|
628
723
|
}
|
|
629
724
|
});
|
|
630
725
|
// Zoom & reset
|
|
631
|
-
this._bindIfExists('
|
|
632
|
-
this._bindIfExists('
|
|
633
|
-
this._bindIfExists('
|
|
726
|
+
this._bindIfExists('zoomInButton', 'click', () => this.scaleImage(this.currentScale + this.options.scaleStep).catch(error => this._reportError('scaleImage failed', error)));
|
|
727
|
+
this._bindIfExists('zoomOutButton', 'click', () => this.scaleImage(this.currentScale - this.options.scaleStep).catch(error => this._reportError('scaleImage failed', error)));
|
|
728
|
+
this._bindIfExists('resetImageTransformButton', 'click', () => { this.resetImageTransform().catch(error => this._reportError('resetImageTransform failed', error)); });
|
|
634
729
|
// Mask management
|
|
635
|
-
this._bindIfExists('
|
|
636
|
-
this._bindIfExists('
|
|
637
|
-
this._bindIfExists('
|
|
730
|
+
this._bindIfExists('createMaskButton', 'click', () => this.createMask());
|
|
731
|
+
this._bindIfExists('removeSelectedMaskButton', 'click', () => this.removeSelectedMask());
|
|
732
|
+
this._bindIfExists('removeAllMasksButton', 'click', () => this.removeAllMasks());
|
|
638
733
|
// Merge + download
|
|
639
|
-
this._bindIfExists('
|
|
640
|
-
this._bindIfExists('
|
|
734
|
+
this._bindIfExists('mergeMasksButton', 'click', () => this.mergeMasks().catch(error => this._reportError('merge error', error)));
|
|
735
|
+
this._bindIfExists('downloadImageButton', 'click', () => this.downloadImage());
|
|
641
736
|
// Undo + Redo
|
|
642
|
-
this._bindIfExists('
|
|
643
|
-
this._bindIfExists('
|
|
737
|
+
this._bindIfExists('undoButton', 'click', () => this.undo().catch(error => this._reportError('undo failed', error)));
|
|
738
|
+
this._bindIfExists('redoButton', 'click', () => this.redo().catch(error => this._reportError('redo failed', error)));
|
|
644
739
|
|
|
645
740
|
// Rotation buttons (step can be overridden by two input fields)
|
|
646
|
-
this._bindIfExists('
|
|
647
|
-
const rotationInputElement = this._getElement('
|
|
741
|
+
this._bindIfExists('rotateLeftButton', 'click', () => {
|
|
742
|
+
const rotationInputElement = this._getElement('rotateLeftDegreesInput');
|
|
648
743
|
let step = this.options.rotationStep;
|
|
649
744
|
if (rotationInputElement) {
|
|
650
745
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -652,8 +747,8 @@ function ensureFabric() {
|
|
|
652
747
|
}
|
|
653
748
|
this.rotateImage(this.currentRotation - step).catch(error => this._reportError('rotateImage failed', error));
|
|
654
749
|
});
|
|
655
|
-
this._bindIfExists('
|
|
656
|
-
const rotationInputElement = this._getElement('
|
|
750
|
+
this._bindIfExists('rotateRightButton', 'click', () => {
|
|
751
|
+
const rotationInputElement = this._getElement('rotateRightDegreesInput');
|
|
657
752
|
let step = this.options.rotationStep;
|
|
658
753
|
if (rotationInputElement) {
|
|
659
754
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -663,9 +758,9 @@ function ensureFabric() {
|
|
|
663
758
|
});
|
|
664
759
|
|
|
665
760
|
// Crop bindings (optional: bound only if element IDs exist in elements)
|
|
666
|
-
this._bindIfExists('
|
|
667
|
-
this._bindIfExists('
|
|
668
|
-
this._bindIfExists('
|
|
761
|
+
this._bindIfExists('enterCropModeButton', 'click', () => this.enterCropMode());
|
|
762
|
+
this._bindIfExists('applyCropButton', 'click', () => { this.applyCrop().catch(error => this._reportError('applyCrop failed', error)); });
|
|
763
|
+
this._bindIfExists('cancelCropButton', 'click', () => this.cancelCrop());
|
|
669
764
|
this._bindIfExists('maskList', 'click', (event) => this._handleMaskListClick(event));
|
|
670
765
|
}
|
|
671
766
|
|
|
@@ -768,14 +863,16 @@ function ensureFabric() {
|
|
|
768
863
|
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
769
864
|
|
|
770
865
|
let loadSource = imageBase64;
|
|
771
|
-
|
|
866
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
867
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
868
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
772
869
|
const shouldResize =
|
|
773
|
-
imageElement.naturalWidth >
|
|
774
|
-
imageElement.naturalHeight >
|
|
870
|
+
imageElement.naturalWidth > downsampleMaxWidth ||
|
|
871
|
+
imageElement.naturalHeight > downsampleMaxHeight;
|
|
775
872
|
if (shouldResize) {
|
|
776
873
|
const ratio = Math.min(
|
|
777
|
-
|
|
778
|
-
|
|
874
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
875
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
779
876
|
);
|
|
780
877
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
781
878
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -787,6 +884,8 @@ function ensureFabric() {
|
|
|
787
884
|
imageBase64
|
|
788
885
|
);
|
|
789
886
|
}
|
|
887
|
+
} else if (this.options.downsampleOnLoad) {
|
|
888
|
+
this._reportWarning('loadImage: downsample limits must be positive numbers; using the original image');
|
|
790
889
|
}
|
|
791
890
|
|
|
792
891
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
@@ -856,9 +955,7 @@ function ensureFabric() {
|
|
|
856
955
|
this.canvas.renderAll();
|
|
857
956
|
this._lastSnapshot = this._captureCanvasStateOrThrow('loadImage');
|
|
858
957
|
|
|
859
|
-
|
|
860
|
-
this.onImageLoaded();
|
|
861
|
-
}
|
|
958
|
+
this._notifyImageLoaded();
|
|
862
959
|
} catch (error) {
|
|
863
960
|
await this._rollbackLoadImageTransaction(transaction);
|
|
864
961
|
throw error;
|
|
@@ -883,6 +980,22 @@ function ensureFabric() {
|
|
|
883
980
|
);
|
|
884
981
|
}
|
|
885
982
|
|
|
983
|
+
/**
|
|
984
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
985
|
+
*
|
|
986
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
987
|
+
* @public
|
|
988
|
+
*/
|
|
989
|
+
isBusy() {
|
|
990
|
+
return !!(
|
|
991
|
+
this.isAnimating ||
|
|
992
|
+
this._cropMode ||
|
|
993
|
+
this._isLoading ||
|
|
994
|
+
this._activeOperationToken ||
|
|
995
|
+
(this.animationQueue && this.animationQueue.isBusy())
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
886
999
|
/**
|
|
887
1000
|
* Creates an HTMLImageElement from a given data URL.
|
|
888
1001
|
*
|
|
@@ -909,7 +1022,7 @@ function ensureFabric() {
|
|
|
909
1022
|
};
|
|
910
1023
|
timerId = setTimeout(() => {
|
|
911
1024
|
settle(() => reject(new Error('Image load timed out')));
|
|
912
|
-
try { imageElement.src = ''; } catch (error) {
|
|
1025
|
+
try { imageElement.src = ''; } catch (error) { this._reportWarning('Image timeout cleanup failed', error); }
|
|
913
1026
|
}, safeTimeoutMs);
|
|
914
1027
|
imageElement.onload = () => settle(() => resolve(imageElement));
|
|
915
1028
|
imageElement.onerror = (error) => settle(() => reject(error));
|
|
@@ -957,7 +1070,6 @@ function ensureFabric() {
|
|
|
957
1070
|
_captureLoadImageTransaction() {
|
|
958
1071
|
return {
|
|
959
1072
|
canvasState: this._serializeCanvasState(),
|
|
960
|
-
originalImage: this.originalImage,
|
|
961
1073
|
baseImageScale: this.baseImageScale,
|
|
962
1074
|
currentScale: this.currentScale,
|
|
963
1075
|
currentRotation: this.currentRotation,
|
|
@@ -983,6 +1095,7 @@ function ensureFabric() {
|
|
|
983
1095
|
async _rollbackLoadImageTransaction(transaction) {
|
|
984
1096
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
985
1097
|
let didRestoreCanvasState = false;
|
|
1098
|
+
let didFailCanvasRestore = false;
|
|
986
1099
|
try {
|
|
987
1100
|
if (transaction.canvasState) {
|
|
988
1101
|
await this.loadFromState(transaction.canvasState);
|
|
@@ -990,23 +1103,28 @@ function ensureFabric() {
|
|
|
990
1103
|
}
|
|
991
1104
|
} catch (error) {
|
|
992
1105
|
this._lastMask = null;
|
|
1106
|
+
didFailCanvasRestore = true;
|
|
993
1107
|
this._reportError('loadImage rollback failed', error);
|
|
994
1108
|
}
|
|
995
1109
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
this.currentRotation = transaction.currentRotation;
|
|
999
|
-
this.maskCounter = transaction.maskCounter;
|
|
1000
|
-
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
1001
|
-
this._lastSnapshot = transaction.lastSnapshot;
|
|
1002
|
-
if (didRestoreCanvasState) {
|
|
1003
|
-
this._restoreLastMaskReference(transaction.lastMask);
|
|
1110
|
+
if (didFailCanvasRestore) {
|
|
1111
|
+
this._reconcileEditorStateFromCanvas();
|
|
1004
1112
|
} else {
|
|
1005
|
-
this.
|
|
1113
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
1114
|
+
this.currentScale = transaction.currentScale;
|
|
1115
|
+
this.currentRotation = transaction.currentRotation;
|
|
1116
|
+
this.maskCounter = transaction.maskCounter;
|
|
1117
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
1118
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
1119
|
+
if (didRestoreCanvasState) {
|
|
1120
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
1121
|
+
} else {
|
|
1122
|
+
this._lastMask = null;
|
|
1123
|
+
}
|
|
1124
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
1125
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
1126
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
1006
1127
|
}
|
|
1007
|
-
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
1008
|
-
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
1009
|
-
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
1010
1128
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
1011
1129
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
1012
1130
|
if (this.containerElement) {
|
|
@@ -1020,6 +1138,49 @@ function ensureFabric() {
|
|
|
1020
1138
|
if (this.canvas) this.canvas.renderAll();
|
|
1021
1139
|
}
|
|
1022
1140
|
|
|
1141
|
+
_reconcileEditorStateFromCanvas() {
|
|
1142
|
+
if (!this.canvas) {
|
|
1143
|
+
this.originalImage = null;
|
|
1144
|
+
this.baseImageScale = 1;
|
|
1145
|
+
this.currentScale = 1;
|
|
1146
|
+
this.currentRotation = 0;
|
|
1147
|
+
this.maskCounter = 0;
|
|
1148
|
+
this.isImageLoadedToCanvas = false;
|
|
1149
|
+
this._lastSnapshot = null;
|
|
1150
|
+
this._clearMaskPlacementMemory();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
const canvasObjects = this.canvas.getObjects();
|
|
1155
|
+
this.originalImage = canvasObjects.find(object => object.type === 'image' && !object.maskId) || null;
|
|
1156
|
+
if (this.originalImage) {
|
|
1157
|
+
const imageScale = Number(this.originalImage.scaleX) || 1;
|
|
1158
|
+
this.baseImageScale = imageScale;
|
|
1159
|
+
this.currentScale = 1;
|
|
1160
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1161
|
+
} else {
|
|
1162
|
+
this.baseImageScale = 1;
|
|
1163
|
+
this.currentScale = 1;
|
|
1164
|
+
this.currentRotation = 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const masks = canvasObjects.filter(object => object.maskId);
|
|
1168
|
+
this.maskCounter = masks.reduce((max, mask) => Math.max(max, Number(mask.maskId) || 0), 0);
|
|
1169
|
+
this._lastMask = masks[masks.length - 1] || null;
|
|
1170
|
+
if (!this._lastMask) {
|
|
1171
|
+
this._lastMaskInitialLeft = null;
|
|
1172
|
+
this._lastMaskInitialTop = null;
|
|
1173
|
+
this._lastMaskInitialWidth = null;
|
|
1174
|
+
}
|
|
1175
|
+
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
1176
|
+
try {
|
|
1177
|
+
this._lastSnapshot = this._serializeCanvasState();
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
this._lastSnapshot = null;
|
|
1180
|
+
this._reportWarning('loadImage rollback: failed to reconcile canvas snapshot', error);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1023
1184
|
_restoreLastMaskReference(previousLastMask) {
|
|
1024
1185
|
if (!this.canvas) {
|
|
1025
1186
|
this._lastMask = null;
|
|
@@ -1048,12 +1209,20 @@ function ensureFabric() {
|
|
|
1048
1209
|
* @private
|
|
1049
1210
|
*/
|
|
1050
1211
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
1212
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
1213
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
1214
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
1215
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
1216
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
1217
|
+
throw new Error('Invalid image resample target dimensions');
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1051
1220
|
const offscreenCanvas = document.createElement('canvas');
|
|
1052
|
-
offscreenCanvas.width =
|
|
1053
|
-
offscreenCanvas.height =
|
|
1221
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
1222
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
1054
1223
|
const context = offscreenCanvas.getContext('2d');
|
|
1055
1224
|
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1056
|
-
context.drawImage(imageElement, 0, 0,
|
|
1225
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
1057
1226
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
1058
1227
|
}
|
|
1059
1228
|
|
|
@@ -1089,6 +1258,7 @@ function ensureFabric() {
|
|
|
1089
1258
|
* @private
|
|
1090
1259
|
*/
|
|
1091
1260
|
_setCanvasSizeInt(width, height) {
|
|
1261
|
+
if (!this.canvas) return;
|
|
1092
1262
|
const integerWidth = Math.max(1, Math.round(Number(width) || 1));
|
|
1093
1263
|
const integerHeight = Math.max(1, Math.round(Number(height) || 1));
|
|
1094
1264
|
// Set fabric internal and also style attributes to keep DOM consistent
|
|
@@ -1400,7 +1570,7 @@ function ensureFabric() {
|
|
|
1400
1570
|
/**
|
|
1401
1571
|
* Captures editor-owned runtime state that Fabric does not include in canvas JSON.
|
|
1402
1572
|
*
|
|
1403
|
-
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number}} Serializable editor metadata.
|
|
1573
|
+
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number, canvasWidth:number, canvasHeight:number}} Serializable editor metadata.
|
|
1404
1574
|
* @private
|
|
1405
1575
|
*/
|
|
1406
1576
|
_serializeEditorMetadata() {
|
|
@@ -1408,13 +1578,17 @@ function ensureFabric() {
|
|
|
1408
1578
|
const currentScale = Number(this.currentScale);
|
|
1409
1579
|
const currentRotation = Number(this.currentRotation);
|
|
1410
1580
|
const maskCounter = Number(this.maskCounter);
|
|
1581
|
+
const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
|
|
1582
|
+
const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
|
|
1411
1583
|
|
|
1412
1584
|
return {
|
|
1413
1585
|
version: 1,
|
|
1414
1586
|
baseImageScale: Number.isFinite(baseImageScale) && baseImageScale > 0 ? baseImageScale : 1,
|
|
1415
1587
|
currentScale: Number.isFinite(currentScale) && currentScale > 0 ? currentScale : 1,
|
|
1416
1588
|
currentRotation: Number.isFinite(currentRotation) ? currentRotation : 0,
|
|
1417
|
-
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0
|
|
1589
|
+
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0,
|
|
1590
|
+
canvasWidth: Number.isFinite(canvasWidth) && canvasWidth > 0 ? Math.round(canvasWidth) : 1,
|
|
1591
|
+
canvasHeight: Number.isFinite(canvasHeight) && canvasHeight > 0 ? Math.round(canvasHeight) : 1
|
|
1418
1592
|
};
|
|
1419
1593
|
}
|
|
1420
1594
|
|
|
@@ -1617,11 +1791,50 @@ function ensureFabric() {
|
|
|
1617
1791
|
|
|
1618
1792
|
_getJpegBackgroundColor() {
|
|
1619
1793
|
const backgroundColor = String(this.options.backgroundColor || '').trim();
|
|
1620
|
-
if (!backgroundColor || backgroundColor
|
|
1621
|
-
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return '#ffffff';
|
|
1794
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
|
|
1622
1795
|
return backgroundColor;
|
|
1623
1796
|
}
|
|
1624
1797
|
|
|
1798
|
+
_isTransparentCssColor(color) {
|
|
1799
|
+
const normalizedColor = String(color || '').trim().toLowerCase();
|
|
1800
|
+
if (!normalizedColor || normalizedColor === 'transparent') return true;
|
|
1801
|
+
|
|
1802
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1803
|
+
if (hexAlphaMatch) {
|
|
1804
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1805
|
+
return alpha === '0' || alpha === '00';
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1809
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1810
|
+
|
|
1811
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1812
|
+
if (commaAlphaMatch) {
|
|
1813
|
+
const parts = commaAlphaMatch[1].split(',');
|
|
1814
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
return false;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1821
|
+
const normalizedAlpha = String(alphaValue || '').trim();
|
|
1822
|
+
if (!normalizedAlpha) return false;
|
|
1823
|
+
if (normalizedAlpha.endsWith('%')) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1824
|
+
return Number(normalizedAlpha) === 0;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
_decodeBase64Payload(base64Payload) {
|
|
1828
|
+
const payload = String(base64Payload || '');
|
|
1829
|
+
if (typeof atob === 'function') {
|
|
1830
|
+
return Uint8Array.from(atob(payload), char => char.charCodeAt(0));
|
|
1831
|
+
}
|
|
1832
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
1833
|
+
return new Uint8Array(Buffer.from(payload, 'base64'));
|
|
1834
|
+
}
|
|
1835
|
+
throw new Error('Base64 decoding is unavailable');
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1625
1838
|
/**
|
|
1626
1839
|
* Gets the top-left corner coordinates of the given object.
|
|
1627
1840
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1747,19 +1960,14 @@ function ensureFabric() {
|
|
|
1747
1960
|
requiredWidth = Math.max(requiredWidth, Math.ceil(boundingRect.left + boundingRect.width + padding));
|
|
1748
1961
|
requiredHeight = Math.max(requiredHeight, Math.ceil(boundingRect.top + boundingRect.height + padding));
|
|
1749
1962
|
});
|
|
1750
|
-
const shouldUseScrollSafeViewport = this.options.fitImageToCanvas || this.options.coverImageToCanvas;
|
|
1751
|
-
|
|
1752
1963
|
let minWidth = 0;
|
|
1753
1964
|
let minHeight = 0;
|
|
1754
|
-
if (
|
|
1965
|
+
if (this.containerElement) {
|
|
1755
1966
|
const viewport = this._getContainerViewportSize();
|
|
1756
1967
|
const safetyMargin = this._getScrollSafetyMargin();
|
|
1757
1968
|
|
|
1758
1969
|
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1759
1970
|
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1760
|
-
} else if (this.containerElement) {
|
|
1761
|
-
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1762
|
-
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1763
1971
|
}
|
|
1764
1972
|
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1765
1973
|
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
@@ -1838,9 +2046,16 @@ function ensureFabric() {
|
|
|
1838
2046
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1839
2047
|
}
|
|
1840
2048
|
|
|
2049
|
+
_isCropModeAllowedOperation(operationName) {
|
|
2050
|
+
return operationName === 'applyCrop' || operationName === 'cancelCrop';
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1841
2053
|
_assertIdleForOperation(operationName, options = {}) {
|
|
1842
2054
|
this._assertEditorAvailable(operationName);
|
|
1843
2055
|
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
2056
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
2057
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
2058
|
+
}
|
|
1844
2059
|
if (this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) {
|
|
1845
2060
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1846
2061
|
}
|
|
@@ -1854,10 +2069,14 @@ function ensureFabric() {
|
|
|
1854
2069
|
|
|
1855
2070
|
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1856
2071
|
this._assertEditorAvailable(operationName);
|
|
1857
|
-
|
|
2072
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
2073
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
2074
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
2075
|
+
}
|
|
2076
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1858
2077
|
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1859
2078
|
}
|
|
1860
|
-
if (this._activeOperationToken && !
|
|
2079
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1861
2080
|
throw new Error(`${operationName} cannot run while ${this._activeOperationName || 'another operation'} is running`);
|
|
1862
2081
|
}
|
|
1863
2082
|
}
|
|
@@ -2064,10 +2283,19 @@ function ensureFabric() {
|
|
|
2064
2283
|
|
|
2065
2284
|
return this.animationQueue.add(async () => {
|
|
2066
2285
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow('resetImageTransform');
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2286
|
+
try {
|
|
2287
|
+
await this._scaleImageImpl(1, { saveHistory: false });
|
|
2288
|
+
await this._rotateImageImpl(0, { saveHistory: false });
|
|
2289
|
+
const after = this._captureCanvasStateOrThrow('resetImageTransform');
|
|
2290
|
+
this._pushStateTransition(before, after);
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
try {
|
|
2293
|
+
await this.loadFromState(before);
|
|
2294
|
+
} catch (restoreError) {
|
|
2295
|
+
this._reportError('resetImageTransform rollback failed', restoreError);
|
|
2296
|
+
}
|
|
2297
|
+
throw error;
|
|
2298
|
+
}
|
|
2071
2299
|
}).finally(() => {
|
|
2072
2300
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
2073
2301
|
}).catch(error => {
|
|
@@ -2111,8 +2339,22 @@ function ensureFabric() {
|
|
|
2111
2339
|
? JSON.parse(serializedState)
|
|
2112
2340
|
: serializedState;
|
|
2113
2341
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
2342
|
+
const restoredCanvasWidth = Number(editorMetadata && editorMetadata.canvasWidth);
|
|
2343
|
+
const restoredCanvasHeight = Number(editorMetadata && editorMetadata.canvasHeight);
|
|
2344
|
+
const hasRestoredCanvasSize =
|
|
2345
|
+
Number.isFinite(restoredCanvasWidth) &&
|
|
2346
|
+
restoredCanvasWidth > 0 &&
|
|
2347
|
+
Number.isFinite(restoredCanvasHeight) &&
|
|
2348
|
+
restoredCanvasHeight > 0;
|
|
2349
|
+
if (
|
|
2350
|
+
editorMetadata &&
|
|
2351
|
+
Object.prototype.hasOwnProperty.call(editorMetadata, 'version') &&
|
|
2352
|
+
Number(editorMetadata.version) !== 1
|
|
2353
|
+
) {
|
|
2354
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
2355
|
+
}
|
|
2114
2356
|
|
|
2115
|
-
|
|
2357
|
+
const finishLoad = async () => {
|
|
2116
2358
|
try {
|
|
2117
2359
|
if (this._disposed || !this.canvas) {
|
|
2118
2360
|
reject(new Error('Editor was disposed while loading state'));
|
|
@@ -2155,6 +2397,12 @@ function ensureFabric() {
|
|
|
2155
2397
|
this.currentRotation = 0;
|
|
2156
2398
|
}
|
|
2157
2399
|
|
|
2400
|
+
if (hasRestoredCanvasSize) {
|
|
2401
|
+
this._setCanvasSizeInt(restoredCanvasWidth, restoredCanvasHeight);
|
|
2402
|
+
} else if (this.originalImage && this._shouldResizeCanvasToContentBounds()) {
|
|
2403
|
+
this._updateCanvasSizeToImageBounds();
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2158
2406
|
const masks = canvasObjects.filter(object => object.maskId);
|
|
2159
2407
|
masks.forEach(mask => {
|
|
2160
2408
|
this._restoreMaskControls(mask);
|
|
@@ -2186,7 +2434,9 @@ function ensureFabric() {
|
|
|
2186
2434
|
this._reportError('loadFromState() failed', callbackError);
|
|
2187
2435
|
reject(callbackError);
|
|
2188
2436
|
}
|
|
2189
|
-
}
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
this.canvas.loadFromJSON(state, () => { void finishLoad(); });
|
|
2190
2440
|
|
|
2191
2441
|
} catch (error) {
|
|
2192
2442
|
this._reportError('loadFromState() failed', error);
|
|
@@ -2204,12 +2454,13 @@ function ensureFabric() {
|
|
|
2204
2454
|
|
|
2205
2455
|
_waitForImageElementReady(imageElement) {
|
|
2206
2456
|
if (!imageElement) return Promise.resolve();
|
|
2207
|
-
|
|
2457
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
|
|
2458
|
+
(Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2459
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
2460
|
+
if (imageElement.complete) return Promise.reject(new Error('Image could not be loaded while restoring state'));
|
|
2208
2461
|
return new Promise((resolve, reject) => {
|
|
2209
2462
|
let isSettled = false;
|
|
2210
|
-
|
|
2211
|
-
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2212
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2463
|
+
let timerId;
|
|
2213
2464
|
const settle = (callback) => {
|
|
2214
2465
|
if (isSettled) return;
|
|
2215
2466
|
isSettled = true;
|
|
@@ -2223,8 +2474,21 @@ function ensureFabric() {
|
|
|
2223
2474
|
}
|
|
2224
2475
|
callback();
|
|
2225
2476
|
};
|
|
2226
|
-
const handleLoad = () =>
|
|
2227
|
-
|
|
2477
|
+
const handleLoad = () => {
|
|
2478
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
|
|
2479
|
+
(Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2480
|
+
settle(() => {
|
|
2481
|
+
if (didLoad) {
|
|
2482
|
+
resolve();
|
|
2483
|
+
} else {
|
|
2484
|
+
reject(new Error('Image could not be loaded while restoring state'));
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2487
|
+
};
|
|
2488
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error('Image could not be loaded while restoring state')));
|
|
2489
|
+
timerId = setTimeout(() => {
|
|
2490
|
+
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2491
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2228
2492
|
if (typeof imageElement.addEventListener === 'function') {
|
|
2229
2493
|
imageElement.addEventListener('load', handleLoad, { once: true });
|
|
2230
2494
|
imageElement.addEventListener('error', handleError, { once: true });
|
|
@@ -2333,12 +2597,7 @@ function ensureFabric() {
|
|
|
2333
2597
|
|
|
2334
2598
|
_rebindMaskEvents(mask) {
|
|
2335
2599
|
if (!mask) return;
|
|
2336
|
-
|
|
2337
|
-
try {
|
|
2338
|
-
mask.off('mouseover', mask.__imageEditorMaskHandlers.mouseover);
|
|
2339
|
-
mask.off('mouseout', mask.__imageEditorMaskHandlers.mouseout);
|
|
2340
|
-
} catch (error) { void error; }
|
|
2341
|
-
}
|
|
2600
|
+
this._cleanupMaskEvents(mask);
|
|
2342
2601
|
|
|
2343
2602
|
const metadata = {};
|
|
2344
2603
|
if (!Number.isFinite(Number(mask.originalAlpha))) {
|
|
@@ -2369,6 +2628,19 @@ function ensureFabric() {
|
|
|
2369
2628
|
mask.__imageEditorMaskHandlers = { mouseover, mouseout };
|
|
2370
2629
|
}
|
|
2371
2630
|
|
|
2631
|
+
_cleanupMaskEvents(mask) {
|
|
2632
|
+
if (!mask || !mask.__imageEditorMaskHandlers) return;
|
|
2633
|
+
try {
|
|
2634
|
+
if (typeof mask.off === 'function') {
|
|
2635
|
+
mask.off('mouseover', mask.__imageEditorMaskHandlers.mouseover);
|
|
2636
|
+
mask.off('mouseout', mask.__imageEditorMaskHandlers.mouseout);
|
|
2637
|
+
}
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
this._reportWarning('Mask event cleanup failed', error);
|
|
2640
|
+
}
|
|
2641
|
+
try { delete mask.__imageEditorMaskHandlers; } catch (error) { this._reportWarning('Mask event metadata cleanup failed', error); }
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2372
2644
|
/**
|
|
2373
2645
|
* Creates a mask and adds it to the canvas.
|
|
2374
2646
|
*
|
|
@@ -2517,6 +2789,11 @@ function ensureFabric() {
|
|
|
2517
2789
|
}
|
|
2518
2790
|
}
|
|
2519
2791
|
|
|
2792
|
+
if (!mask || typeof mask.set !== 'function' || typeof mask.setCoords !== 'function') {
|
|
2793
|
+
this._reportWarning('fabricGenerator returned an invalid Fabric object');
|
|
2794
|
+
return null;
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2520
2797
|
const styles = maskConfig.styles || {};
|
|
2521
2798
|
const hasStyle = property => Object.prototype.hasOwnProperty.call(styles, property);
|
|
2522
2799
|
const maskSettings = {
|
|
@@ -2594,6 +2871,7 @@ function ensureFabric() {
|
|
|
2594
2871
|
this.canvas.discardActiveObject();
|
|
2595
2872
|
selectedMasks.forEach(mask => {
|
|
2596
2873
|
this._removeLabelForMask(mask);
|
|
2874
|
+
this._cleanupMaskEvents(mask);
|
|
2597
2875
|
this.canvas.remove(mask);
|
|
2598
2876
|
});
|
|
2599
2877
|
|
|
@@ -2620,7 +2898,10 @@ function ensureFabric() {
|
|
|
2620
2898
|
const saveHistory = options.saveHistory !== false;
|
|
2621
2899
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2622
2900
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
2623
|
-
masks.forEach(mask =>
|
|
2901
|
+
masks.forEach(mask => {
|
|
2902
|
+
this._cleanupMaskEvents(mask);
|
|
2903
|
+
this.canvas.remove(mask);
|
|
2904
|
+
});
|
|
2624
2905
|
this.canvas.discardActiveObject();
|
|
2625
2906
|
this._lastMask = null;
|
|
2626
2907
|
this._lastMaskInitialLeft = null;
|
|
@@ -2651,6 +2932,101 @@ function ensureFabric() {
|
|
|
2651
2932
|
}
|
|
2652
2933
|
}
|
|
2653
2934
|
|
|
2935
|
+
_captureMaskLabelBackups(masks) {
|
|
2936
|
+
if (!this.canvas) return [];
|
|
2937
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2938
|
+
return (masks || []).map(mask => {
|
|
2939
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2940
|
+
return {
|
|
2941
|
+
mask,
|
|
2942
|
+
label,
|
|
2943
|
+
hadLabel: !!label,
|
|
2944
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2945
|
+
visible: label ? label.visible : undefined
|
|
2946
|
+
};
|
|
2947
|
+
});
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2951
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2952
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2953
|
+
labelBackups.forEach(backup => {
|
|
2954
|
+
if (!backup || !backup.mask) return;
|
|
2955
|
+
try {
|
|
2956
|
+
if (!backup.hadLabel) {
|
|
2957
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
backup.mask.__label = backup.label;
|
|
2961
|
+
if (!backup.label) return;
|
|
2962
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2963
|
+
this.canvas.add(backup.label);
|
|
2964
|
+
canvasObjects.add(backup.label);
|
|
2965
|
+
}
|
|
2966
|
+
if (backup.visible !== undefined) backup.label.set({ visible: backup.visible });
|
|
2967
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2968
|
+
this._syncMaskLabel(backup.mask);
|
|
2969
|
+
} catch (error) {
|
|
2970
|
+
this._reportWarning('restoreMaskLabelBackups: failed to restore mask label', error);
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
_captureActiveObjectBackup() {
|
|
2976
|
+
if (!this.canvas) return null;
|
|
2977
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2978
|
+
if (!activeObject) return null;
|
|
2979
|
+
const selectedObjects = typeof activeObject.getObjects === 'function'
|
|
2980
|
+
? activeObject.getObjects()
|
|
2981
|
+
: [activeObject];
|
|
2982
|
+
return { activeObject, selectedObjects };
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2986
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2987
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2988
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects)
|
|
2989
|
+
? activeObjectBackup.selectedObjects
|
|
2990
|
+
: [];
|
|
2991
|
+
const canRestore = selectedObjects.length
|
|
2992
|
+
? selectedObjects.every(object => canvasObjects.includes(object))
|
|
2993
|
+
: canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2994
|
+
if (!canRestore) return;
|
|
2995
|
+
try {
|
|
2996
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2997
|
+
} catch (error) { void error; }
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
_captureMaskExportBackups(masks) {
|
|
3001
|
+
return (masks || []).map(mask => ({
|
|
3002
|
+
object: mask,
|
|
3003
|
+
visible: mask.visible,
|
|
3004
|
+
opacity: mask.opacity,
|
|
3005
|
+
fill: mask.fill,
|
|
3006
|
+
strokeWidth: mask.strokeWidth,
|
|
3007
|
+
stroke: mask.stroke,
|
|
3008
|
+
selectable: mask.selectable,
|
|
3009
|
+
lockRotation: mask.lockRotation
|
|
3010
|
+
}));
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
3014
|
+
(maskBackups || []).forEach(backup => {
|
|
3015
|
+
try {
|
|
3016
|
+
backup.object.set({
|
|
3017
|
+
visible: backup.visible,
|
|
3018
|
+
opacity: backup.opacity,
|
|
3019
|
+
fill: backup.fill,
|
|
3020
|
+
strokeWidth: backup.strokeWidth,
|
|
3021
|
+
stroke: backup.stroke,
|
|
3022
|
+
selectable: backup.selectable,
|
|
3023
|
+
lockRotation: backup.lockRotation
|
|
3024
|
+
});
|
|
3025
|
+
backup.object.setCoords();
|
|
3026
|
+
} catch (error) { void error; }
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
|
|
2654
3030
|
/**
|
|
2655
3031
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2656
3032
|
*
|
|
@@ -2728,10 +3104,13 @@ function ensureFabric() {
|
|
|
2728
3104
|
_hideAllMaskLabels() {
|
|
2729
3105
|
if (!this.canvas) return;
|
|
2730
3106
|
const canvasObjects = this.canvas.getObjects();
|
|
3107
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2731
3108
|
const labels = canvasObjects.filter(object => object.maskLabel);
|
|
2732
3109
|
labels.forEach(label => {
|
|
2733
3110
|
try {
|
|
2734
|
-
if (
|
|
3111
|
+
if (canvasObjectSet.has(label)) {
|
|
3112
|
+
this.canvas.remove(label);
|
|
3113
|
+
}
|
|
2735
3114
|
} catch (error) { void error; }
|
|
2736
3115
|
});
|
|
2737
3116
|
canvasObjects.forEach(object => {
|
|
@@ -2908,6 +3287,9 @@ function ensureFabric() {
|
|
|
2908
3287
|
fileType: 'png'
|
|
2909
3288
|
}));
|
|
2910
3289
|
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
3290
|
+
if (this.canvas.getObjects().some(object => object.maskId)) {
|
|
3291
|
+
throw new Error('Masks could not be removed during merge');
|
|
3292
|
+
}
|
|
2911
3293
|
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2912
3294
|
preserveScroll: true,
|
|
2913
3295
|
resetMaskCounter: false
|
|
@@ -2987,7 +3369,11 @@ function ensureFabric() {
|
|
|
2987
3369
|
|
|
2988
3370
|
if (!exportImageArea) {
|
|
2989
3371
|
const masks = this.canvas.getObjects().filter(object => object.maskId || object.maskLabel);
|
|
3372
|
+
const editableMasks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2990
3373
|
const maskVisibilityBackups = masks.map(mask => ({ object: mask, visible: mask.visible }));
|
|
3374
|
+
const maskStyleBackups = this._captureMaskExportBackups(editableMasks);
|
|
3375
|
+
const labelBackups = this._captureMaskLabelBackups(editableMasks);
|
|
3376
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2991
3377
|
|
|
2992
3378
|
try {
|
|
2993
3379
|
masks.forEach(mask => { mask.set({ visible: false }); });
|
|
@@ -3008,23 +3394,19 @@ function ensureFabric() {
|
|
|
3008
3394
|
maskVisibilityBackups.forEach(backup => {
|
|
3009
3395
|
try { backup.object.set({ visible: backup.visible }); } catch (error) { void error; }
|
|
3010
3396
|
});
|
|
3397
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3398
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3399
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3011
3400
|
this.canvas.renderAll();
|
|
3012
3401
|
}
|
|
3013
3402
|
}
|
|
3014
3403
|
|
|
3015
3404
|
// Render masks as export shapes without mutating their editable styles.
|
|
3016
3405
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3017
|
-
const maskStyleBackups =
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
fill: mask.fill,
|
|
3021
|
-
strokeWidth: mask.strokeWidth,
|
|
3022
|
-
stroke: mask.stroke,
|
|
3023
|
-
selectable: mask.selectable,
|
|
3024
|
-
lockRotation: mask.lockRotation
|
|
3025
|
-
}));
|
|
3406
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3407
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3408
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
3026
3409
|
|
|
3027
|
-
let finalBase64;
|
|
3028
3410
|
try {
|
|
3029
3411
|
// Labels are UI overlays and should not be part of the flattened export.
|
|
3030
3412
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
@@ -3043,8 +3425,7 @@ function ensureFabric() {
|
|
|
3043
3425
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3044
3426
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3045
3427
|
|
|
3046
|
-
|
|
3047
|
-
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
3428
|
+
return await this._exportCanvasRegionToDataURL({
|
|
3048
3429
|
...exportRegion,
|
|
3049
3430
|
multiplier,
|
|
3050
3431
|
quality,
|
|
@@ -3052,24 +3433,11 @@ function ensureFabric() {
|
|
|
3052
3433
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3053
3434
|
});
|
|
3054
3435
|
} finally {
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
opacity: backup.opacity,
|
|
3059
|
-
fill: backup.fill,
|
|
3060
|
-
strokeWidth: backup.strokeWidth,
|
|
3061
|
-
stroke: backup.stroke,
|
|
3062
|
-
selectable: backup.selectable,
|
|
3063
|
-
lockRotation: backup.lockRotation
|
|
3064
|
-
});
|
|
3065
|
-
backup.object.setCoords();
|
|
3066
|
-
} catch (error) { void error; }
|
|
3067
|
-
});
|
|
3068
|
-
|
|
3436
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3437
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3438
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3069
3439
|
this.canvas.renderAll();
|
|
3070
3440
|
}
|
|
3071
|
-
|
|
3072
|
-
return finalBase64;
|
|
3073
3441
|
}
|
|
3074
3442
|
|
|
3075
3443
|
/**
|
|
@@ -3158,13 +3526,8 @@ function ensureFabric() {
|
|
|
3158
3526
|
}
|
|
3159
3527
|
|
|
3160
3528
|
// Convert the final data URL to a File with the requested MIME type.
|
|
3161
|
-
const
|
|
3529
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(',')[1]);
|
|
3162
3530
|
const mime = `image/${safeFileType}`;
|
|
3163
|
-
let byteIndex = binaryString.length;
|
|
3164
|
-
const bytes = new Uint8Array(byteIndex);
|
|
3165
|
-
while (byteIndex--) {
|
|
3166
|
-
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
3167
|
-
}
|
|
3168
3531
|
return new File([bytes], fileName, { type: mime });
|
|
3169
3532
|
}
|
|
3170
3533
|
|
|
@@ -3214,20 +3577,21 @@ function ensureFabric() {
|
|
|
3214
3577
|
}
|
|
3215
3578
|
|
|
3216
3579
|
_removeCropRect() {
|
|
3217
|
-
if (
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
targetHandlers.handlers.forEach(handlerRecord => {
|
|
3580
|
+
if (this._cropHandlers && this._cropHandlers.length) {
|
|
3581
|
+
this._cropHandlers.forEach(targetHandlers => {
|
|
3582
|
+
(targetHandlers.handlers || []).forEach(handlerRecord => {
|
|
3583
|
+
try {
|
|
3222
3584
|
if (targetHandlers.target && typeof targetHandlers.target.off === 'function') {
|
|
3223
3585
|
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
3224
3586
|
}
|
|
3225
|
-
})
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
this._reportWarning('Crop handler cleanup failed', error);
|
|
3589
|
+
}
|
|
3226
3590
|
});
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3229
3593
|
|
|
3230
|
-
try { if (this.canvas) this.canvas.remove(this._cropRect); } catch (error) { void error; }
|
|
3594
|
+
try { if (this.canvas && this._cropRect) this.canvas.remove(this._cropRect); } catch (error) { void error; }
|
|
3231
3595
|
this._cropRect = null;
|
|
3232
3596
|
this._cropHandlers = [];
|
|
3233
3597
|
}
|
|
@@ -3325,6 +3689,30 @@ function ensureFabric() {
|
|
|
3325
3689
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3326
3690
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3327
3691
|
cropRect.setCoords();
|
|
3692
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3693
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3694
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3695
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3696
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3697
|
+
let deltaX = 0;
|
|
3698
|
+
let deltaY = 0;
|
|
3699
|
+
if (cropBounds.left < imageLeft) {
|
|
3700
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3701
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3702
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3703
|
+
}
|
|
3704
|
+
if (cropBounds.top < imageTop) {
|
|
3705
|
+
deltaY = imageTop - cropBounds.top;
|
|
3706
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3707
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3708
|
+
}
|
|
3709
|
+
if (deltaX || deltaY) {
|
|
3710
|
+
cropRect.set({
|
|
3711
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3712
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3713
|
+
});
|
|
3714
|
+
cropRect.setCoords();
|
|
3715
|
+
}
|
|
3328
3716
|
this.canvas.requestRenderAll();
|
|
3329
3717
|
} catch (error) { void error; }
|
|
3330
3718
|
};
|
|
@@ -3394,9 +3782,13 @@ function ensureFabric() {
|
|
|
3394
3782
|
try {
|
|
3395
3783
|
beforeJson = this._serializeCanvasState();
|
|
3396
3784
|
} catch (error) {
|
|
3397
|
-
this.
|
|
3785
|
+
this._reportError('applyCrop: failed to capture rollback state', error);
|
|
3398
3786
|
beforeJson = null;
|
|
3399
3787
|
}
|
|
3788
|
+
if (!beforeJson) {
|
|
3789
|
+
this.cancelCrop();
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3400
3792
|
|
|
3401
3793
|
const preservedMasks = [];
|
|
3402
3794
|
|
|
@@ -3404,23 +3796,20 @@ function ensureFabric() {
|
|
|
3404
3796
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3405
3797
|
if (masks && masks.length) {
|
|
3406
3798
|
masks.forEach(mask => {
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
}
|
|
3422
|
-
} catch (error) {
|
|
3423
|
-
this._reportWarning('applyCrop: failed to remove mask', error);
|
|
3799
|
+
mask.setCoords();
|
|
3800
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3801
|
+
const intersectsCrop =
|
|
3802
|
+
maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth &&
|
|
3803
|
+
maskBounds.left + maskBounds.width > cropRegion.sourceX &&
|
|
3804
|
+
maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight &&
|
|
3805
|
+
maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3806
|
+
this._removeLabelForMask(mask);
|
|
3807
|
+
this._cleanupMaskEvents(mask);
|
|
3808
|
+
this.canvas.remove(mask);
|
|
3809
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3810
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3811
|
+
mask.set({ visible: true });
|
|
3812
|
+
preservedMasks.push(mask);
|
|
3424
3813
|
}
|
|
3425
3814
|
});
|
|
3426
3815
|
this._clearMaskPlacementMemory();
|
|
@@ -3428,7 +3817,8 @@ function ensureFabric() {
|
|
|
3428
3817
|
this.canvas.renderAll();
|
|
3429
3818
|
}
|
|
3430
3819
|
} catch (error) {
|
|
3431
|
-
this.
|
|
3820
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error);
|
|
3821
|
+
return;
|
|
3432
3822
|
}
|
|
3433
3823
|
|
|
3434
3824
|
this._removeCropRect();
|
|
@@ -3500,7 +3890,7 @@ function ensureFabric() {
|
|
|
3500
3890
|
* @private
|
|
3501
3891
|
*/
|
|
3502
3892
|
_updateInputs() {
|
|
3503
|
-
const scaleInputElement = this._getElement('
|
|
3893
|
+
const scaleInputElement = this._getElement('scalePercentageInput');
|
|
3504
3894
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
3505
3895
|
}
|
|
3506
3896
|
|
|
@@ -3520,14 +3910,14 @@ function ensureFabric() {
|
|
|
3520
3910
|
const canUndo = this.historyManager?.canUndo();
|
|
3521
3911
|
const canRedo = this.historyManager?.canRedo();
|
|
3522
3912
|
const isInCropMode = !!this._cropMode;
|
|
3523
|
-
const isBusy = this.
|
|
3913
|
+
const isBusy = this.isBusy();
|
|
3524
3914
|
|
|
3525
3915
|
if (isInCropMode) {
|
|
3526
3916
|
// Disable all controls except the crop action buttons while crop mode is active.
|
|
3527
3917
|
for (const key of Object.keys(this.elements || {})) {
|
|
3528
3918
|
const element = this._getElement(key);
|
|
3529
3919
|
if (!element) continue;
|
|
3530
|
-
if (key === 'applyCropBtn' || key === 'cancelCropBtn') {
|
|
3920
|
+
if (key === 'applyCropButton' || key === 'cancelCropButton' || key === 'applyCropBtn' || key === 'cancelCropBtn') {
|
|
3531
3921
|
this._setDisabled(key, false);
|
|
3532
3922
|
} else {
|
|
3533
3923
|
this._setDisabled(key, true);
|
|
@@ -3536,21 +3926,25 @@ function ensureFabric() {
|
|
|
3536
3926
|
return;
|
|
3537
3927
|
}
|
|
3538
3928
|
|
|
3539
|
-
this._setDisabled('
|
|
3540
|
-
this._setDisabled('
|
|
3541
|
-
this._setDisabled('
|
|
3542
|
-
this._setDisabled('
|
|
3543
|
-
this._setDisabled('
|
|
3544
|
-
this._setDisabled('
|
|
3545
|
-
this._setDisabled('
|
|
3546
|
-
this._setDisabled('
|
|
3547
|
-
this._setDisabled('
|
|
3548
|
-
this._setDisabled('
|
|
3549
|
-
this._setDisabled('
|
|
3550
|
-
this._setDisabled('
|
|
3551
|
-
this._setDisabled('
|
|
3552
|
-
this._setDisabled('
|
|
3553
|
-
this._setDisabled('
|
|
3929
|
+
this._setDisabled('zoomInButton', !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3930
|
+
this._setDisabled('zoomOutButton', !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3931
|
+
this._setDisabled('rotateLeftButton', !hasImage || isBusy);
|
|
3932
|
+
this._setDisabled('rotateRightButton', !hasImage || isBusy);
|
|
3933
|
+
this._setDisabled('createMaskButton', !hasImage || isBusy);
|
|
3934
|
+
this._setDisabled('removeSelectedMaskButton', !hasSelectedMask || isBusy);
|
|
3935
|
+
this._setDisabled('removeAllMasksButton', !hasMasks || isBusy);
|
|
3936
|
+
this._setDisabled('mergeMasksButton', !hasImage || !hasMasks || isBusy);
|
|
3937
|
+
this._setDisabled('downloadImageButton', !hasImage || isBusy);
|
|
3938
|
+
this._setDisabled('resetImageTransformButton', !hasImage || isDefaultTransform || isBusy);
|
|
3939
|
+
this._setDisabled('undoButton', !hasImage || isBusy || !canUndo);
|
|
3940
|
+
this._setDisabled('redoButton', !hasImage || isBusy || !canRedo);
|
|
3941
|
+
this._setDisabled('enterCropModeButton', !hasImage || isBusy);
|
|
3942
|
+
this._setDisabled('applyCropButton', true);
|
|
3943
|
+
this._setDisabled('cancelCropButton', true);
|
|
3944
|
+
this._setDisabled('scalePercentageInput', !hasImage || isBusy);
|
|
3945
|
+
this._setDisabled('rotateLeftDegreesInput', !hasImage || isBusy);
|
|
3946
|
+
this._setDisabled('rotateRightDegreesInput', !hasImage || isBusy);
|
|
3947
|
+
this._setDisabled('maskList', !hasImage || isBusy);
|
|
3554
3948
|
this._setDisabled('imageInput', isBusy);
|
|
3555
3949
|
this._setDisabled('uploadArea', isBusy);
|
|
3556
3950
|
}
|
|
@@ -3558,7 +3952,7 @@ function ensureFabric() {
|
|
|
3558
3952
|
/**
|
|
3559
3953
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
3560
3954
|
*
|
|
3561
|
-
* @param {string} key - Key of the element in this.elements (e.g. '
|
|
3955
|
+
* @param {string} key - Key of the element in this.elements (e.g. 'zoomInButton').
|
|
3562
3956
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3563
3957
|
* @private
|
|
3564
3958
|
*/
|
|
@@ -3693,10 +4087,7 @@ function ensureFabric() {
|
|
|
3693
4087
|
}
|
|
3694
4088
|
} catch (error) { void error; }
|
|
3695
4089
|
|
|
3696
|
-
if (this._cropRect)
|
|
3697
|
-
try { this.canvas.remove(this._cropRect); } catch (error) { void error; }
|
|
3698
|
-
this._cropRect = null;
|
|
3699
|
-
}
|
|
4090
|
+
if (this._cropRect) this._removeCropRect();
|
|
3700
4091
|
|
|
3701
4092
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3702
4093
|
try { this._restoreContainerOverflowState(); } catch (error) { void error; }
|
|
@@ -3716,10 +4107,16 @@ function ensureFabric() {
|
|
|
3716
4107
|
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3717
4108
|
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3718
4109
|
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
4110
|
+
this.canvasElement.style.maxWidth = this._canvasElementOriginalStyle.maxWidth;
|
|
3719
4111
|
} catch (error) { void error; }
|
|
3720
4112
|
}
|
|
3721
4113
|
|
|
3722
4114
|
if (this.canvas) {
|
|
4115
|
+
try {
|
|
4116
|
+
this.canvas.getObjects().forEach(object => {
|
|
4117
|
+
if (object && object.maskId) this._cleanupMaskEvents(object);
|
|
4118
|
+
});
|
|
4119
|
+
} catch (error) { void error; }
|
|
3723
4120
|
try { this.canvas.dispose(); } catch (error) { void error; }
|
|
3724
4121
|
this.canvas = null;
|
|
3725
4122
|
this.canvasElement = null;
|
|
@@ -3861,7 +4258,7 @@ function ensureFabric() {
|
|
|
3861
4258
|
task.reject(error);
|
|
3862
4259
|
}
|
|
3863
4260
|
} finally {
|
|
3864
|
-
if (
|
|
4261
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3865
4262
|
}
|
|
3866
4263
|
}
|
|
3867
4264
|
} finally {
|
|
@@ -3940,9 +4337,9 @@ function ensureFabric() {
|
|
|
3940
4337
|
execute(command) {
|
|
3941
4338
|
const result = command.execute();
|
|
3942
4339
|
if (result && typeof result.then === 'function') {
|
|
3943
|
-
return Promise.resolve(result).then(() => {
|
|
4340
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3944
4341
|
this.push(command);
|
|
3945
|
-
});
|
|
4342
|
+
}));
|
|
3946
4343
|
}
|
|
3947
4344
|
this.push(command);
|
|
3948
4345
|
return result;
|