@bensitu/image-editor 1.4.1 → 1.4.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/dist/image-editor.esm.js +239 -66
- 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 +239 -66
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +239 -66
- 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 +1 -0
- package/package.json +2 -1
- package/src/image-editor.js +272 -70
package/image-editor.d.ts
CHANGED
|
@@ -186,6 +186,7 @@ declare module '@bensitu/image-editor' {
|
|
|
186
186
|
init(idMap?: ElementIdMap): void;
|
|
187
187
|
loadImage(imageBase64: string, options?: LoadImageOptions): Promise<void>;
|
|
188
188
|
isImageLoaded(): boolean;
|
|
189
|
+
isBusy(): boolean;
|
|
189
190
|
|
|
190
191
|
/** Public callers should pass only `factor`; internal history control options are intentionally not exposed. */
|
|
191
192
|
scaleImage(factor: number): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bensitu/image-editor",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "Lightweight canvas-based image editor",
|
|
5
5
|
"main": "./dist/image-editor.js",
|
|
6
6
|
"module": "./dist/image-editor.esm.mjs",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"esbuild": "^0.28.0",
|
|
46
46
|
"esbuild-plugin-babel": "^0.2.3",
|
|
47
47
|
"eslint": "^10.4.0",
|
|
48
|
+
"fabric": "^5.5.2",
|
|
48
49
|
"globals": "^17.6.0"
|
|
49
50
|
},
|
|
50
51
|
"browserslist": [
|
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.
|
|
4
|
+
* @version 1.4.2
|
|
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.
|
|
@@ -768,14 +768,16 @@ function ensureFabric() {
|
|
|
768
768
|
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
769
769
|
|
|
770
770
|
let loadSource = imageBase64;
|
|
771
|
-
|
|
771
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
772
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
773
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
772
774
|
const shouldResize =
|
|
773
|
-
imageElement.naturalWidth >
|
|
774
|
-
imageElement.naturalHeight >
|
|
775
|
+
imageElement.naturalWidth > downsampleMaxWidth ||
|
|
776
|
+
imageElement.naturalHeight > downsampleMaxHeight;
|
|
775
777
|
if (shouldResize) {
|
|
776
778
|
const ratio = Math.min(
|
|
777
|
-
|
|
778
|
-
|
|
779
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
780
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
779
781
|
);
|
|
780
782
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
781
783
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -787,6 +789,8 @@ function ensureFabric() {
|
|
|
787
789
|
imageBase64
|
|
788
790
|
);
|
|
789
791
|
}
|
|
792
|
+
} else if (this.options.downsampleOnLoad) {
|
|
793
|
+
this._reportWarning('loadImage: downsample limits must be positive numbers; using the original image');
|
|
790
794
|
}
|
|
791
795
|
|
|
792
796
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
@@ -883,6 +887,22 @@ function ensureFabric() {
|
|
|
883
887
|
);
|
|
884
888
|
}
|
|
885
889
|
|
|
890
|
+
/**
|
|
891
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
892
|
+
*
|
|
893
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
894
|
+
* @public
|
|
895
|
+
*/
|
|
896
|
+
isBusy() {
|
|
897
|
+
return !!(
|
|
898
|
+
this.isAnimating ||
|
|
899
|
+
this._cropMode ||
|
|
900
|
+
this._isLoading ||
|
|
901
|
+
this._activeOperationToken ||
|
|
902
|
+
(this.animationQueue && this.animationQueue.isBusy())
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
886
906
|
/**
|
|
887
907
|
* Creates an HTMLImageElement from a given data URL.
|
|
888
908
|
*
|
|
@@ -957,7 +977,6 @@ function ensureFabric() {
|
|
|
957
977
|
_captureLoadImageTransaction() {
|
|
958
978
|
return {
|
|
959
979
|
canvasState: this._serializeCanvasState(),
|
|
960
|
-
originalImage: this.originalImage,
|
|
961
980
|
baseImageScale: this.baseImageScale,
|
|
962
981
|
currentScale: this.currentScale,
|
|
963
982
|
currentRotation: this.currentRotation,
|
|
@@ -1048,12 +1067,20 @@ function ensureFabric() {
|
|
|
1048
1067
|
* @private
|
|
1049
1068
|
*/
|
|
1050
1069
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
1070
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
1071
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
1072
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
1073
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
1074
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
1075
|
+
throw new Error('Invalid image resample target dimensions');
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1051
1078
|
const offscreenCanvas = document.createElement('canvas');
|
|
1052
|
-
offscreenCanvas.width =
|
|
1053
|
-
offscreenCanvas.height =
|
|
1079
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
1080
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
1054
1081
|
const context = offscreenCanvas.getContext('2d');
|
|
1055
1082
|
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1056
|
-
context.drawImage(imageElement, 0, 0,
|
|
1083
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
1057
1084
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
1058
1085
|
}
|
|
1059
1086
|
|
|
@@ -1617,11 +1644,50 @@ function ensureFabric() {
|
|
|
1617
1644
|
|
|
1618
1645
|
_getJpegBackgroundColor() {
|
|
1619
1646
|
const backgroundColor = String(this.options.backgroundColor || '').trim();
|
|
1620
|
-
if (!backgroundColor || backgroundColor
|
|
1621
|
-
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return '#ffffff';
|
|
1647
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return '#ffffff';
|
|
1622
1648
|
return backgroundColor;
|
|
1623
1649
|
}
|
|
1624
1650
|
|
|
1651
|
+
_isTransparentCssColor(color) {
|
|
1652
|
+
const normalizedColor = String(color || '').trim().toLowerCase();
|
|
1653
|
+
if (!normalizedColor || normalizedColor === 'transparent') return true;
|
|
1654
|
+
|
|
1655
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1656
|
+
if (hexAlphaMatch) {
|
|
1657
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1658
|
+
return alpha === '0' || alpha === '00';
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1662
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1663
|
+
|
|
1664
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1665
|
+
if (commaAlphaMatch) {
|
|
1666
|
+
const parts = commaAlphaMatch[1].split(',');
|
|
1667
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return false;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1674
|
+
const normalizedAlpha = String(alphaValue || '').trim();
|
|
1675
|
+
if (!normalizedAlpha) return false;
|
|
1676
|
+
if (normalizedAlpha.endsWith('%')) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1677
|
+
return Number(normalizedAlpha) === 0;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
_decodeBase64Payload(base64Payload) {
|
|
1681
|
+
const payload = String(base64Payload || '');
|
|
1682
|
+
if (typeof atob === 'function') {
|
|
1683
|
+
return Uint8Array.from(atob(payload), char => char.charCodeAt(0));
|
|
1684
|
+
}
|
|
1685
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
1686
|
+
return new Uint8Array(Buffer.from(payload, 'base64'));
|
|
1687
|
+
}
|
|
1688
|
+
throw new Error('Base64 decoding is unavailable');
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1625
1691
|
/**
|
|
1626
1692
|
* Gets the top-left corner coordinates of the given object.
|
|
1627
1693
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -2111,6 +2177,13 @@ function ensureFabric() {
|
|
|
2111
2177
|
? JSON.parse(serializedState)
|
|
2112
2178
|
: serializedState;
|
|
2113
2179
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
2180
|
+
if (
|
|
2181
|
+
editorMetadata &&
|
|
2182
|
+
Object.prototype.hasOwnProperty.call(editorMetadata, 'version') &&
|
|
2183
|
+
Number(editorMetadata.version) !== 1
|
|
2184
|
+
) {
|
|
2185
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
2186
|
+
}
|
|
2114
2187
|
|
|
2115
2188
|
this.canvas.loadFromJSON(state, async () => {
|
|
2116
2189
|
try {
|
|
@@ -2204,12 +2277,13 @@ function ensureFabric() {
|
|
|
2204
2277
|
|
|
2205
2278
|
_waitForImageElementReady(imageElement) {
|
|
2206
2279
|
if (!imageElement) return Promise.resolve();
|
|
2207
|
-
|
|
2280
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
|
|
2281
|
+
(Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2282
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
2283
|
+
if (imageElement.complete) return Promise.reject(new Error('Image could not be loaded while restoring state'));
|
|
2208
2284
|
return new Promise((resolve, reject) => {
|
|
2209
2285
|
let isSettled = false;
|
|
2210
|
-
|
|
2211
|
-
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2212
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2286
|
+
let timerId;
|
|
2213
2287
|
const settle = (callback) => {
|
|
2214
2288
|
if (isSettled) return;
|
|
2215
2289
|
isSettled = true;
|
|
@@ -2223,8 +2297,21 @@ function ensureFabric() {
|
|
|
2223
2297
|
}
|
|
2224
2298
|
callback();
|
|
2225
2299
|
};
|
|
2226
|
-
const handleLoad = () =>
|
|
2227
|
-
|
|
2300
|
+
const handleLoad = () => {
|
|
2301
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) &&
|
|
2302
|
+
(Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
2303
|
+
settle(() => {
|
|
2304
|
+
if (didLoad) {
|
|
2305
|
+
resolve();
|
|
2306
|
+
} else {
|
|
2307
|
+
reject(new Error('Image could not be loaded while restoring state'));
|
|
2308
|
+
}
|
|
2309
|
+
});
|
|
2310
|
+
};
|
|
2311
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error('Image could not be loaded while restoring state')));
|
|
2312
|
+
timerId = setTimeout(() => {
|
|
2313
|
+
settle(() => reject(new Error('Image load timed out while restoring state')));
|
|
2314
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
2228
2315
|
if (typeof imageElement.addEventListener === 'function') {
|
|
2229
2316
|
imageElement.addEventListener('load', handleLoad, { once: true });
|
|
2230
2317
|
imageElement.addEventListener('error', handleError, { once: true });
|
|
@@ -2517,6 +2604,11 @@ function ensureFabric() {
|
|
|
2517
2604
|
}
|
|
2518
2605
|
}
|
|
2519
2606
|
|
|
2607
|
+
if (!mask || typeof mask.set !== 'function' || typeof mask.setCoords !== 'function') {
|
|
2608
|
+
this._reportWarning('fabricGenerator returned an invalid Fabric object');
|
|
2609
|
+
return null;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2520
2612
|
const styles = maskConfig.styles || {};
|
|
2521
2613
|
const hasStyle = property => Object.prototype.hasOwnProperty.call(styles, property);
|
|
2522
2614
|
const maskSettings = {
|
|
@@ -2651,6 +2743,99 @@ function ensureFabric() {
|
|
|
2651
2743
|
}
|
|
2652
2744
|
}
|
|
2653
2745
|
|
|
2746
|
+
_captureMaskLabelBackups(masks) {
|
|
2747
|
+
if (!this.canvas) return [];
|
|
2748
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2749
|
+
return (masks || []).map(mask => {
|
|
2750
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2751
|
+
return {
|
|
2752
|
+
mask,
|
|
2753
|
+
label,
|
|
2754
|
+
hadLabel: !!label,
|
|
2755
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2756
|
+
visible: label ? label.visible : undefined
|
|
2757
|
+
};
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2762
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2763
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2764
|
+
labelBackups.forEach(backup => {
|
|
2765
|
+
if (!backup || !backup.mask) return;
|
|
2766
|
+
try {
|
|
2767
|
+
if (!backup.hadLabel) {
|
|
2768
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
backup.mask.__label = backup.label;
|
|
2772
|
+
if (!backup.label) return;
|
|
2773
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2774
|
+
this.canvas.add(backup.label);
|
|
2775
|
+
canvasObjects.add(backup.label);
|
|
2776
|
+
}
|
|
2777
|
+
if (backup.visible !== undefined) backup.label.set({ visible: backup.visible });
|
|
2778
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2779
|
+
this._syncMaskLabel(backup.mask);
|
|
2780
|
+
} catch (error) { void error; }
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
_captureActiveObjectBackup() {
|
|
2785
|
+
if (!this.canvas) return null;
|
|
2786
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2787
|
+
if (!activeObject) return null;
|
|
2788
|
+
const selectedObjects = typeof activeObject.getObjects === 'function'
|
|
2789
|
+
? activeObject.getObjects()
|
|
2790
|
+
: [activeObject];
|
|
2791
|
+
return { activeObject, selectedObjects };
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2795
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2796
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2797
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects)
|
|
2798
|
+
? activeObjectBackup.selectedObjects
|
|
2799
|
+
: [];
|
|
2800
|
+
const canRestore = selectedObjects.length
|
|
2801
|
+
? selectedObjects.every(object => canvasObjects.includes(object))
|
|
2802
|
+
: canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2803
|
+
if (!canRestore) return;
|
|
2804
|
+
try {
|
|
2805
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2806
|
+
} catch (error) { void error; }
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
_captureMaskExportBackups(masks) {
|
|
2810
|
+
return (masks || []).map(mask => ({
|
|
2811
|
+
object: mask,
|
|
2812
|
+
visible: mask.visible,
|
|
2813
|
+
opacity: mask.opacity,
|
|
2814
|
+
fill: mask.fill,
|
|
2815
|
+
strokeWidth: mask.strokeWidth,
|
|
2816
|
+
stroke: mask.stroke,
|
|
2817
|
+
selectable: mask.selectable,
|
|
2818
|
+
lockRotation: mask.lockRotation
|
|
2819
|
+
}));
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
2823
|
+
(maskBackups || []).forEach(backup => {
|
|
2824
|
+
try {
|
|
2825
|
+
backup.object.set({
|
|
2826
|
+
visible: backup.visible,
|
|
2827
|
+
opacity: backup.opacity,
|
|
2828
|
+
fill: backup.fill,
|
|
2829
|
+
strokeWidth: backup.strokeWidth,
|
|
2830
|
+
stroke: backup.stroke,
|
|
2831
|
+
selectable: backup.selectable,
|
|
2832
|
+
lockRotation: backup.lockRotation
|
|
2833
|
+
});
|
|
2834
|
+
backup.object.setCoords();
|
|
2835
|
+
} catch (error) { void error; }
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2654
2839
|
/**
|
|
2655
2840
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2656
2841
|
*
|
|
@@ -2728,10 +2913,14 @@ function ensureFabric() {
|
|
|
2728
2913
|
_hideAllMaskLabels() {
|
|
2729
2914
|
if (!this.canvas) return;
|
|
2730
2915
|
const canvasObjects = this.canvas.getObjects();
|
|
2916
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2731
2917
|
const labels = canvasObjects.filter(object => object.maskLabel);
|
|
2732
2918
|
labels.forEach(label => {
|
|
2733
2919
|
try {
|
|
2734
|
-
if (
|
|
2920
|
+
if (canvasObjectSet.has(label)) {
|
|
2921
|
+
this.canvas.remove(label);
|
|
2922
|
+
canvasObjectSet.delete(label);
|
|
2923
|
+
}
|
|
2735
2924
|
} catch (error) { void error; }
|
|
2736
2925
|
});
|
|
2737
2926
|
canvasObjects.forEach(object => {
|
|
@@ -2908,6 +3097,9 @@ function ensureFabric() {
|
|
|
2908
3097
|
fileType: 'png'
|
|
2909
3098
|
}));
|
|
2910
3099
|
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
3100
|
+
if (this.canvas.getObjects().some(object => object.maskId)) {
|
|
3101
|
+
throw new Error('Masks could not be removed during merge');
|
|
3102
|
+
}
|
|
2911
3103
|
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2912
3104
|
preserveScroll: true,
|
|
2913
3105
|
resetMaskCounter: false
|
|
@@ -2987,7 +3179,11 @@ function ensureFabric() {
|
|
|
2987
3179
|
|
|
2988
3180
|
if (!exportImageArea) {
|
|
2989
3181
|
const masks = this.canvas.getObjects().filter(object => object.maskId || object.maskLabel);
|
|
3182
|
+
const editableMasks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2990
3183
|
const maskVisibilityBackups = masks.map(mask => ({ object: mask, visible: mask.visible }));
|
|
3184
|
+
const maskStyleBackups = this._captureMaskExportBackups(editableMasks);
|
|
3185
|
+
const labelBackups = this._captureMaskLabelBackups(editableMasks);
|
|
3186
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2991
3187
|
|
|
2992
3188
|
try {
|
|
2993
3189
|
masks.forEach(mask => { mask.set({ visible: false }); });
|
|
@@ -3008,21 +3204,18 @@ function ensureFabric() {
|
|
|
3008
3204
|
maskVisibilityBackups.forEach(backup => {
|
|
3009
3205
|
try { backup.object.set({ visible: backup.visible }); } catch (error) { void error; }
|
|
3010
3206
|
});
|
|
3207
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3208
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3209
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3011
3210
|
this.canvas.renderAll();
|
|
3012
3211
|
}
|
|
3013
3212
|
}
|
|
3014
3213
|
|
|
3015
3214
|
// Render masks as export shapes without mutating their editable styles.
|
|
3016
3215
|
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
|
-
}));
|
|
3216
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3217
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3218
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
3026
3219
|
|
|
3027
3220
|
let finalBase64;
|
|
3028
3221
|
try {
|
|
@@ -3052,20 +3245,9 @@ function ensureFabric() {
|
|
|
3052
3245
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3053
3246
|
});
|
|
3054
3247
|
} 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
|
-
|
|
3248
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3249
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3250
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3069
3251
|
this.canvas.renderAll();
|
|
3070
3252
|
}
|
|
3071
3253
|
|
|
@@ -3158,13 +3340,8 @@ function ensureFabric() {
|
|
|
3158
3340
|
}
|
|
3159
3341
|
|
|
3160
3342
|
// Convert the final data URL to a File with the requested MIME type.
|
|
3161
|
-
const
|
|
3343
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(',')[1]);
|
|
3162
3344
|
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
3345
|
return new File([bytes], fileName, { type: mime });
|
|
3169
3346
|
}
|
|
3170
3347
|
|
|
@@ -3325,6 +3502,30 @@ function ensureFabric() {
|
|
|
3325
3502
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3326
3503
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3327
3504
|
cropRect.setCoords();
|
|
3505
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3506
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3507
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3508
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3509
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3510
|
+
let deltaX = 0;
|
|
3511
|
+
let deltaY = 0;
|
|
3512
|
+
if (cropBounds.left < imageLeft) {
|
|
3513
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3514
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3515
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3516
|
+
}
|
|
3517
|
+
if (cropBounds.top < imageTop) {
|
|
3518
|
+
deltaY = imageTop - cropBounds.top;
|
|
3519
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3520
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3521
|
+
}
|
|
3522
|
+
if (deltaX || deltaY) {
|
|
3523
|
+
cropRect.set({
|
|
3524
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3525
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3526
|
+
});
|
|
3527
|
+
cropRect.setCoords();
|
|
3528
|
+
}
|
|
3328
3529
|
this.canvas.requestRenderAll();
|
|
3329
3530
|
} catch (error) { void error; }
|
|
3330
3531
|
};
|
|
@@ -3404,23 +3605,19 @@ function ensureFabric() {
|
|
|
3404
3605
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3405
3606
|
if (masks && masks.length) {
|
|
3406
3607
|
masks.forEach(mask => {
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
preservedMasks.push(mask);
|
|
3421
|
-
}
|
|
3422
|
-
} catch (error) {
|
|
3423
|
-
this._reportWarning('applyCrop: failed to remove mask', error);
|
|
3608
|
+
mask.setCoords();
|
|
3609
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3610
|
+
const intersectsCrop =
|
|
3611
|
+
maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth &&
|
|
3612
|
+
maskBounds.left + maskBounds.width > cropRegion.sourceX &&
|
|
3613
|
+
maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight &&
|
|
3614
|
+
maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3615
|
+
this._removeLabelForMask(mask);
|
|
3616
|
+
this.canvas.remove(mask);
|
|
3617
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3618
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3619
|
+
mask.set({ visible: true });
|
|
3620
|
+
preservedMasks.push(mask);
|
|
3424
3621
|
}
|
|
3425
3622
|
});
|
|
3426
3623
|
this._clearMaskPlacementMemory();
|
|
@@ -3428,7 +3625,8 @@ function ensureFabric() {
|
|
|
3428
3625
|
this.canvas.renderAll();
|
|
3429
3626
|
}
|
|
3430
3627
|
} catch (error) {
|
|
3431
|
-
this.
|
|
3628
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error);
|
|
3629
|
+
return;
|
|
3432
3630
|
}
|
|
3433
3631
|
|
|
3434
3632
|
this._removeCropRect();
|
|
@@ -3520,7 +3718,7 @@ function ensureFabric() {
|
|
|
3520
3718
|
const canUndo = this.historyManager?.canUndo();
|
|
3521
3719
|
const canRedo = this.historyManager?.canRedo();
|
|
3522
3720
|
const isInCropMode = !!this._cropMode;
|
|
3523
|
-
const isBusy = this.
|
|
3721
|
+
const isBusy = this.isBusy();
|
|
3524
3722
|
|
|
3525
3723
|
if (isInCropMode) {
|
|
3526
3724
|
// Disable all controls except the crop action buttons while crop mode is active.
|
|
@@ -3551,6 +3749,10 @@ function ensureFabric() {
|
|
|
3551
3749
|
this._setDisabled('cropBtn', !hasImage || isBusy);
|
|
3552
3750
|
this._setDisabled('applyCropBtn', true);
|
|
3553
3751
|
this._setDisabled('cancelCropBtn', true);
|
|
3752
|
+
this._setDisabled('scaleRate', !hasImage || isBusy);
|
|
3753
|
+
this._setDisabled('rotationLeftInput', !hasImage || isBusy);
|
|
3754
|
+
this._setDisabled('rotationRightInput', !hasImage || isBusy);
|
|
3755
|
+
this._setDisabled('maskList', !hasImage || isBusy);
|
|
3554
3756
|
this._setDisabled('imageInput', isBusy);
|
|
3555
3757
|
this._setDisabled('uploadArea', isBusy);
|
|
3556
3758
|
}
|
|
@@ -3940,9 +4142,9 @@ function ensureFabric() {
|
|
|
3940
4142
|
execute(command) {
|
|
3941
4143
|
const result = command.execute();
|
|
3942
4144
|
if (result && typeof result.then === 'function') {
|
|
3943
|
-
return Promise.resolve(result).then(() => {
|
|
4145
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3944
4146
|
this.push(command);
|
|
3945
|
-
});
|
|
4147
|
+
}));
|
|
3946
4148
|
}
|
|
3947
4149
|
this.push(command);
|
|
3948
4150
|
return result;
|