@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/dist/image-editor.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file image-editor.js
|
|
5
5
|
* @module image-editor
|
|
6
|
-
* @version 1.4.
|
|
6
|
+
* @version 1.4.2
|
|
7
7
|
* @author Ben Situ
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
10
10
|
*/
|
|
11
11
|
var fabric = null;
|
|
12
|
-
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol("ImageEditorInternalOperation");
|
|
12
|
+
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol.for("ImageEditorInternalOperation");
|
|
13
13
|
function getGlobalScope() {
|
|
14
14
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
15
15
|
if (typeof self !== "undefined") return self;
|
|
@@ -573,12 +573,14 @@
|
|
|
573
573
|
const imageElement = await this._createImageElement(imageBase64);
|
|
574
574
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
575
575
|
let loadSource = imageBase64;
|
|
576
|
-
|
|
577
|
-
|
|
576
|
+
const downsampleMaxWidth = Number(this.options.downsampleMaxWidth);
|
|
577
|
+
const downsampleMaxHeight = Number(this.options.downsampleMaxHeight);
|
|
578
|
+
if (this.options.downsampleOnLoad && downsampleMaxWidth > 0 && downsampleMaxHeight > 0) {
|
|
579
|
+
const shouldResize = imageElement.naturalWidth > downsampleMaxWidth || imageElement.naturalHeight > downsampleMaxHeight;
|
|
578
580
|
if (shouldResize) {
|
|
579
581
|
const ratio = Math.min(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
+
downsampleMaxWidth / imageElement.naturalWidth,
|
|
583
|
+
downsampleMaxHeight / imageElement.naturalHeight
|
|
582
584
|
);
|
|
583
585
|
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
584
586
|
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
@@ -590,6 +592,8 @@
|
|
|
590
592
|
imageBase64
|
|
591
593
|
);
|
|
592
594
|
}
|
|
595
|
+
} else if (this.options.downsampleOnLoad) {
|
|
596
|
+
this._reportWarning("loadImage: downsample limits must be positive numbers; using the original image");
|
|
593
597
|
}
|
|
594
598
|
const fabricImage = await this._createFabricImageFromURL(loadSource);
|
|
595
599
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
@@ -667,6 +671,15 @@
|
|
|
667
671
|
const fabricInstance = ensureFabric();
|
|
668
672
|
return !!(this.originalImage && fabricInstance && this.originalImage instanceof fabricInstance.Image && this.originalImage.width > 0 && this.originalImage.height > 0);
|
|
669
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Checks whether the editor is in a temporary non-mutating state.
|
|
676
|
+
*
|
|
677
|
+
* @returns {boolean} True while loading, animating, cropping, or running a compound operation.
|
|
678
|
+
* @public
|
|
679
|
+
*/
|
|
680
|
+
isBusy() {
|
|
681
|
+
return !!(this.isAnimating || this._cropMode || this._isLoading || this._activeOperationToken || this.animationQueue && this.animationQueue.isBusy());
|
|
682
|
+
}
|
|
670
683
|
/**
|
|
671
684
|
* Creates an HTMLImageElement from a given data URL.
|
|
672
685
|
*
|
|
@@ -738,7 +751,6 @@
|
|
|
738
751
|
_captureLoadImageTransaction() {
|
|
739
752
|
return {
|
|
740
753
|
canvasState: this._serializeCanvasState(),
|
|
741
|
-
originalImage: this.originalImage,
|
|
742
754
|
baseImageScale: this.baseImageScale,
|
|
743
755
|
currentScale: this.currentScale,
|
|
744
756
|
currentRotation: this.currentRotation,
|
|
@@ -824,12 +836,19 @@
|
|
|
824
836
|
* @private
|
|
825
837
|
*/
|
|
826
838
|
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92, sourceDataUrl = null) {
|
|
839
|
+
const sourceWidth = Math.max(1, Number(imageElement && (imageElement.naturalWidth || imageElement.width)) || 0);
|
|
840
|
+
const sourceHeight = Math.max(1, Number(imageElement && (imageElement.naturalHeight || imageElement.height)) || 0);
|
|
841
|
+
const safeTargetWidth = Math.round(Number(targetWidth));
|
|
842
|
+
const safeTargetHeight = Math.round(Number(targetHeight));
|
|
843
|
+
if (!Number.isFinite(safeTargetWidth) || !Number.isFinite(safeTargetHeight) || safeTargetWidth <= 0 || safeTargetHeight <= 0) {
|
|
844
|
+
throw new Error("Invalid image resample target dimensions");
|
|
845
|
+
}
|
|
827
846
|
const offscreenCanvas = document.createElement("canvas");
|
|
828
|
-
offscreenCanvas.width =
|
|
829
|
-
offscreenCanvas.height =
|
|
847
|
+
offscreenCanvas.width = safeTargetWidth;
|
|
848
|
+
offscreenCanvas.height = safeTargetHeight;
|
|
830
849
|
const context = offscreenCanvas.getContext("2d");
|
|
831
850
|
if (!context) throw new Error("2D canvas context is unavailable");
|
|
832
|
-
context.drawImage(imageElement, 0, 0,
|
|
851
|
+
context.drawImage(imageElement, 0, 0, sourceWidth, sourceHeight, 0, 0, safeTargetWidth, safeTargetHeight);
|
|
833
852
|
return offscreenCanvas.toDataURL(this._getDownsampleMimeType(sourceDataUrl), quality);
|
|
834
853
|
}
|
|
835
854
|
_getDataUrlMimeType(dataUrl) {
|
|
@@ -1330,10 +1349,42 @@
|
|
|
1330
1349
|
}
|
|
1331
1350
|
_getJpegBackgroundColor() {
|
|
1332
1351
|
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1333
|
-
if (!backgroundColor || backgroundColor
|
|
1334
|
-
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return "#ffffff";
|
|
1352
|
+
if (!backgroundColor || this._isTransparentCssColor(backgroundColor)) return "#ffffff";
|
|
1335
1353
|
return backgroundColor;
|
|
1336
1354
|
}
|
|
1355
|
+
_isTransparentCssColor(color) {
|
|
1356
|
+
const normalizedColor = String(color || "").trim().toLowerCase();
|
|
1357
|
+
if (!normalizedColor || normalizedColor === "transparent") return true;
|
|
1358
|
+
const hexAlphaMatch = normalizedColor.match(/^#(?:[0-9a-f]{3}([0-9a-f])|[0-9a-f]{6}([0-9a-f]{2}))$/i);
|
|
1359
|
+
if (hexAlphaMatch) {
|
|
1360
|
+
const alpha = hexAlphaMatch[1] || hexAlphaMatch[2];
|
|
1361
|
+
return alpha === "0" || alpha === "00";
|
|
1362
|
+
}
|
|
1363
|
+
const slashAlphaMatch = normalizedColor.match(/^(?:rgba?|hsla?)\([^)]*\/\s*([^)]+)\)$/i);
|
|
1364
|
+
if (slashAlphaMatch) return this._isZeroCssAlpha(slashAlphaMatch[1]);
|
|
1365
|
+
const commaAlphaMatch = normalizedColor.match(/^(?:rgba|hsla)\((.*)\)$/i);
|
|
1366
|
+
if (commaAlphaMatch) {
|
|
1367
|
+
const parts = commaAlphaMatch[1].split(",");
|
|
1368
|
+
if (parts.length >= 4) return this._isZeroCssAlpha(parts[parts.length - 1]);
|
|
1369
|
+
}
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
_isZeroCssAlpha(alphaValue) {
|
|
1373
|
+
const normalizedAlpha = String(alphaValue || "").trim();
|
|
1374
|
+
if (!normalizedAlpha) return false;
|
|
1375
|
+
if (normalizedAlpha.endsWith("%")) return Number.parseFloat(normalizedAlpha) === 0;
|
|
1376
|
+
return Number(normalizedAlpha) === 0;
|
|
1377
|
+
}
|
|
1378
|
+
_decodeBase64Payload(base64Payload) {
|
|
1379
|
+
const payload = String(base64Payload || "");
|
|
1380
|
+
if (typeof atob === "function") {
|
|
1381
|
+
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1382
|
+
}
|
|
1383
|
+
if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
|
|
1384
|
+
return new Uint8Array(Buffer.from(payload, "base64"));
|
|
1385
|
+
}
|
|
1386
|
+
throw new Error("Base64 decoding is unavailable");
|
|
1387
|
+
}
|
|
1337
1388
|
/**
|
|
1338
1389
|
* Gets the top-left corner coordinates of the given object.
|
|
1339
1390
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1776,6 +1827,9 @@
|
|
|
1776
1827
|
try {
|
|
1777
1828
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1778
1829
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
1830
|
+
if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
|
|
1831
|
+
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
1832
|
+
}
|
|
1779
1833
|
this.canvas.loadFromJSON(state, async () => {
|
|
1780
1834
|
try {
|
|
1781
1835
|
if (this._disposed || !this.canvas) {
|
|
@@ -1854,12 +1908,12 @@
|
|
|
1854
1908
|
}
|
|
1855
1909
|
_waitForImageElementReady(imageElement) {
|
|
1856
1910
|
if (!imageElement) return Promise.resolve();
|
|
1857
|
-
|
|
1911
|
+
const hasLoadedDimensions = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
1912
|
+
if (hasLoadedDimensions) return Promise.resolve();
|
|
1913
|
+
if (imageElement.complete) return Promise.reject(new Error("Image could not be loaded while restoring state"));
|
|
1858
1914
|
return new Promise((resolve, reject) => {
|
|
1859
1915
|
let isSettled = false;
|
|
1860
|
-
|
|
1861
|
-
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1862
|
-
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1916
|
+
let timerId;
|
|
1863
1917
|
const settle = (callback) => {
|
|
1864
1918
|
if (isSettled) return;
|
|
1865
1919
|
isSettled = true;
|
|
@@ -1873,8 +1927,20 @@
|
|
|
1873
1927
|
}
|
|
1874
1928
|
callback();
|
|
1875
1929
|
};
|
|
1876
|
-
const handleLoad = () =>
|
|
1877
|
-
|
|
1930
|
+
const handleLoad = () => {
|
|
1931
|
+
const didLoad = (Number(imageElement.naturalWidth) > 0 || Number(imageElement.width) > 0) && (Number(imageElement.naturalHeight) > 0 || Number(imageElement.height) > 0);
|
|
1932
|
+
settle(() => {
|
|
1933
|
+
if (didLoad) {
|
|
1934
|
+
resolve();
|
|
1935
|
+
} else {
|
|
1936
|
+
reject(new Error("Image could not be loaded while restoring state"));
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
};
|
|
1940
|
+
const handleError = (error) => settle(() => reject(error instanceof Error ? error : new Error("Image could not be loaded while restoring state")));
|
|
1941
|
+
timerId = setTimeout(() => {
|
|
1942
|
+
settle(() => reject(new Error("Image load timed out while restoring state")));
|
|
1943
|
+
}, this._getSafeTimeoutMs(this.options.imageLoadTimeoutMs));
|
|
1878
1944
|
if (typeof imageElement.addEventListener === "function") {
|
|
1879
1945
|
imageElement.addEventListener("load", handleLoad, { once: true });
|
|
1880
1946
|
imageElement.addEventListener("error", handleError, { once: true });
|
|
@@ -2146,6 +2212,10 @@
|
|
|
2146
2212
|
});
|
|
2147
2213
|
}
|
|
2148
2214
|
}
|
|
2215
|
+
if (!mask || typeof mask.set !== "function" || typeof mask.setCoords !== "function") {
|
|
2216
|
+
this._reportWarning("fabricGenerator returned an invalid Fabric object");
|
|
2217
|
+
return null;
|
|
2218
|
+
}
|
|
2149
2219
|
const styles = maskConfig.styles || {};
|
|
2150
2220
|
const hasStyle = (property) => Object.prototype.hasOwnProperty.call(styles, property);
|
|
2151
2221
|
const maskSettings = {
|
|
@@ -2273,6 +2343,93 @@
|
|
|
2273
2343
|
}
|
|
2274
2344
|
}
|
|
2275
2345
|
}
|
|
2346
|
+
_captureMaskLabelBackups(masks) {
|
|
2347
|
+
if (!this.canvas) return [];
|
|
2348
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2349
|
+
return (masks || []).map((mask) => {
|
|
2350
|
+
const label = mask && mask.__label ? mask.__label : null;
|
|
2351
|
+
return {
|
|
2352
|
+
mask,
|
|
2353
|
+
label,
|
|
2354
|
+
hadLabel: !!label,
|
|
2355
|
+
labelInCanvas: !!label && canvasObjects.has(label),
|
|
2356
|
+
visible: label ? label.visible : void 0
|
|
2357
|
+
};
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
_restoreMaskLabelBackups(labelBackups) {
|
|
2361
|
+
if (!this.canvas || !Array.isArray(labelBackups)) return;
|
|
2362
|
+
const canvasObjects = new Set(this.canvas.getObjects());
|
|
2363
|
+
labelBackups.forEach((backup) => {
|
|
2364
|
+
if (!backup || !backup.mask) return;
|
|
2365
|
+
try {
|
|
2366
|
+
if (!backup.hadLabel) {
|
|
2367
|
+
if (backup.mask.__label) this._removeLabelForMask(backup.mask);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
backup.mask.__label = backup.label;
|
|
2371
|
+
if (!backup.label) return;
|
|
2372
|
+
if (backup.labelInCanvas && !canvasObjects.has(backup.label)) {
|
|
2373
|
+
this.canvas.add(backup.label);
|
|
2374
|
+
canvasObjects.add(backup.label);
|
|
2375
|
+
}
|
|
2376
|
+
if (backup.visible !== void 0) backup.label.set({ visible: backup.visible });
|
|
2377
|
+
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2378
|
+
this._syncMaskLabel(backup.mask);
|
|
2379
|
+
} catch (error) {
|
|
2380
|
+
void error;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
_captureActiveObjectBackup() {
|
|
2385
|
+
if (!this.canvas) return null;
|
|
2386
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2387
|
+
if (!activeObject) return null;
|
|
2388
|
+
const selectedObjects = typeof activeObject.getObjects === "function" ? activeObject.getObjects() : [activeObject];
|
|
2389
|
+
return { activeObject, selectedObjects };
|
|
2390
|
+
}
|
|
2391
|
+
_restoreActiveObjectBackup(activeObjectBackup) {
|
|
2392
|
+
if (!this.canvas || !activeObjectBackup || !activeObjectBackup.activeObject) return;
|
|
2393
|
+
const canvasObjects = this.canvas.getObjects();
|
|
2394
|
+
const selectedObjects = Array.isArray(activeObjectBackup.selectedObjects) ? activeObjectBackup.selectedObjects : [];
|
|
2395
|
+
const canRestore = selectedObjects.length ? selectedObjects.every((object) => canvasObjects.includes(object)) : canvasObjects.includes(activeObjectBackup.activeObject);
|
|
2396
|
+
if (!canRestore) return;
|
|
2397
|
+
try {
|
|
2398
|
+
this.canvas.setActiveObject(activeObjectBackup.activeObject);
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
void error;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
_captureMaskExportBackups(masks) {
|
|
2404
|
+
return (masks || []).map((mask) => ({
|
|
2405
|
+
object: mask,
|
|
2406
|
+
visible: mask.visible,
|
|
2407
|
+
opacity: mask.opacity,
|
|
2408
|
+
fill: mask.fill,
|
|
2409
|
+
strokeWidth: mask.strokeWidth,
|
|
2410
|
+
stroke: mask.stroke,
|
|
2411
|
+
selectable: mask.selectable,
|
|
2412
|
+
lockRotation: mask.lockRotation
|
|
2413
|
+
}));
|
|
2414
|
+
}
|
|
2415
|
+
_restoreMaskExportBackups(maskBackups) {
|
|
2416
|
+
(maskBackups || []).forEach((backup) => {
|
|
2417
|
+
try {
|
|
2418
|
+
backup.object.set({
|
|
2419
|
+
visible: backup.visible,
|
|
2420
|
+
opacity: backup.opacity,
|
|
2421
|
+
fill: backup.fill,
|
|
2422
|
+
strokeWidth: backup.strokeWidth,
|
|
2423
|
+
stroke: backup.stroke,
|
|
2424
|
+
selectable: backup.selectable,
|
|
2425
|
+
lockRotation: backup.lockRotation
|
|
2426
|
+
});
|
|
2427
|
+
backup.object.setCoords();
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
void error;
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2276
2433
|
/**
|
|
2277
2434
|
* Returns a stable zero-based creation index for label callbacks.
|
|
2278
2435
|
*
|
|
@@ -2345,10 +2502,14 @@
|
|
|
2345
2502
|
_hideAllMaskLabels() {
|
|
2346
2503
|
if (!this.canvas) return;
|
|
2347
2504
|
const canvasObjects = this.canvas.getObjects();
|
|
2505
|
+
const canvasObjectSet = new Set(canvasObjects);
|
|
2348
2506
|
const labels = canvasObjects.filter((object) => object.maskLabel);
|
|
2349
2507
|
labels.forEach((label) => {
|
|
2350
2508
|
try {
|
|
2351
|
-
if (
|
|
2509
|
+
if (canvasObjectSet.has(label)) {
|
|
2510
|
+
this.canvas.remove(label);
|
|
2511
|
+
canvasObjectSet.delete(label);
|
|
2512
|
+
}
|
|
2352
2513
|
} catch (error) {
|
|
2353
2514
|
void error;
|
|
2354
2515
|
}
|
|
@@ -2518,6 +2679,9 @@
|
|
|
2518
2679
|
fileType: "png"
|
|
2519
2680
|
}));
|
|
2520
2681
|
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2682
|
+
if (this.canvas.getObjects().some((object) => object.maskId)) {
|
|
2683
|
+
throw new Error("Masks could not be removed during merge");
|
|
2684
|
+
}
|
|
2521
2685
|
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2522
2686
|
preserveScroll: true,
|
|
2523
2687
|
resetMaskCounter: false
|
|
@@ -2591,7 +2755,11 @@
|
|
|
2591
2755
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2592
2756
|
if (!exportImageArea) {
|
|
2593
2757
|
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
2758
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2594
2759
|
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
2760
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
2761
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
2762
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
2595
2763
|
try {
|
|
2596
2764
|
masks2.forEach((mask) => {
|
|
2597
2765
|
mask.set({ visible: false });
|
|
@@ -2616,19 +2784,16 @@
|
|
|
2616
2784
|
void error;
|
|
2617
2785
|
}
|
|
2618
2786
|
});
|
|
2787
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2788
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
2789
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
2619
2790
|
this.canvas.renderAll();
|
|
2620
2791
|
}
|
|
2621
2792
|
}
|
|
2622
2793
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2623
|
-
const maskStyleBackups =
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
fill: mask.fill,
|
|
2627
|
-
strokeWidth: mask.strokeWidth,
|
|
2628
|
-
stroke: mask.stroke,
|
|
2629
|
-
selectable: mask.selectable,
|
|
2630
|
-
lockRotation: mask.lockRotation
|
|
2631
|
-
}));
|
|
2794
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2795
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2796
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2632
2797
|
let finalBase64;
|
|
2633
2798
|
try {
|
|
2634
2799
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -2650,21 +2815,9 @@
|
|
|
2650
2815
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2651
2816
|
});
|
|
2652
2817
|
} finally {
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
opacity: backup.opacity,
|
|
2657
|
-
fill: backup.fill,
|
|
2658
|
-
strokeWidth: backup.strokeWidth,
|
|
2659
|
-
stroke: backup.stroke,
|
|
2660
|
-
selectable: backup.selectable,
|
|
2661
|
-
lockRotation: backup.lockRotation
|
|
2662
|
-
});
|
|
2663
|
-
backup.object.setCoords();
|
|
2664
|
-
} catch (error) {
|
|
2665
|
-
void error;
|
|
2666
|
-
}
|
|
2667
|
-
});
|
|
2818
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
2819
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
2820
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2668
2821
|
this.canvas.renderAll();
|
|
2669
2822
|
}
|
|
2670
2823
|
return finalBase64;
|
|
@@ -2748,13 +2901,8 @@
|
|
|
2748
2901
|
imageElement.src = imageBase64;
|
|
2749
2902
|
});
|
|
2750
2903
|
}
|
|
2751
|
-
const
|
|
2904
|
+
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
2752
2905
|
const mime = `image/${safeFileType}`;
|
|
2753
|
-
let byteIndex = binaryString.length;
|
|
2754
|
-
const bytes = new Uint8Array(byteIndex);
|
|
2755
|
-
while (byteIndex--) {
|
|
2756
|
-
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
2757
|
-
}
|
|
2758
2906
|
return new File([bytes], fileName, { type: mime });
|
|
2759
2907
|
}
|
|
2760
2908
|
_clearMaskPlacementMemory() {
|
|
@@ -2901,6 +3049,30 @@
|
|
|
2901
3049
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
2902
3050
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
2903
3051
|
cropRect.setCoords();
|
|
3052
|
+
const cropBounds = cropRect.getBoundingRect(true, true);
|
|
3053
|
+
const imageLeft = Number(imageBounds.left) || 0;
|
|
3054
|
+
const imageTop = Number(imageBounds.top) || 0;
|
|
3055
|
+
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
3056
|
+
const imageBottom = imageTop + (Number(imageBounds.height) || 0);
|
|
3057
|
+
let deltaX = 0;
|
|
3058
|
+
let deltaY = 0;
|
|
3059
|
+
if (cropBounds.left < imageLeft) {
|
|
3060
|
+
deltaX = imageLeft - cropBounds.left;
|
|
3061
|
+
} else if (cropBounds.left + cropBounds.width > imageRight) {
|
|
3062
|
+
deltaX = imageRight - (cropBounds.left + cropBounds.width);
|
|
3063
|
+
}
|
|
3064
|
+
if (cropBounds.top < imageTop) {
|
|
3065
|
+
deltaY = imageTop - cropBounds.top;
|
|
3066
|
+
} else if (cropBounds.top + cropBounds.height > imageBottom) {
|
|
3067
|
+
deltaY = imageBottom - (cropBounds.top + cropBounds.height);
|
|
3068
|
+
}
|
|
3069
|
+
if (deltaX || deltaY) {
|
|
3070
|
+
cropRect.set({
|
|
3071
|
+
left: (Number(cropRect.left) || 0) + deltaX,
|
|
3072
|
+
top: (Number(cropRect.top) || 0) + deltaY
|
|
3073
|
+
});
|
|
3074
|
+
cropRect.setCoords();
|
|
3075
|
+
}
|
|
2904
3076
|
this.canvas.requestRenderAll();
|
|
2905
3077
|
} catch (error) {
|
|
2906
3078
|
void error;
|
|
@@ -2968,19 +3140,15 @@
|
|
|
2968
3140
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2969
3141
|
if (masks && masks.length) {
|
|
2970
3142
|
masks.forEach((mask) => {
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
preservedMasks.push(mask);
|
|
2981
|
-
}
|
|
2982
|
-
} catch (error) {
|
|
2983
|
-
this._reportWarning("applyCrop: failed to remove mask", error);
|
|
3143
|
+
mask.setCoords();
|
|
3144
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3145
|
+
const intersectsCrop = maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth && maskBounds.left + maskBounds.width > cropRegion.sourceX && maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight && maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3146
|
+
this._removeLabelForMask(mask);
|
|
3147
|
+
this.canvas.remove(mask);
|
|
3148
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3149
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3150
|
+
mask.set({ visible: true });
|
|
3151
|
+
preservedMasks.push(mask);
|
|
2984
3152
|
}
|
|
2985
3153
|
});
|
|
2986
3154
|
this._clearMaskPlacementMemory();
|
|
@@ -2988,7 +3156,8 @@
|
|
|
2988
3156
|
this.canvas.renderAll();
|
|
2989
3157
|
}
|
|
2990
3158
|
} catch (error) {
|
|
2991
|
-
this.
|
|
3159
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error);
|
|
3160
|
+
return;
|
|
2992
3161
|
}
|
|
2993
3162
|
this._removeCropRect();
|
|
2994
3163
|
this._cropMode = false;
|
|
@@ -3064,7 +3233,7 @@
|
|
|
3064
3233
|
const canUndo = this.historyManager?.canUndo();
|
|
3065
3234
|
const canRedo = this.historyManager?.canRedo();
|
|
3066
3235
|
const isInCropMode = !!this._cropMode;
|
|
3067
|
-
const isBusy = this.
|
|
3236
|
+
const isBusy = this.isBusy();
|
|
3068
3237
|
if (isInCropMode) {
|
|
3069
3238
|
for (const key of Object.keys(this.elements || {})) {
|
|
3070
3239
|
const element = this._getElement(key);
|
|
@@ -3092,6 +3261,10 @@
|
|
|
3092
3261
|
this._setDisabled("cropBtn", !hasImage || isBusy);
|
|
3093
3262
|
this._setDisabled("applyCropBtn", true);
|
|
3094
3263
|
this._setDisabled("cancelCropBtn", true);
|
|
3264
|
+
this._setDisabled("scaleRate", !hasImage || isBusy);
|
|
3265
|
+
this._setDisabled("rotationLeftInput", !hasImage || isBusy);
|
|
3266
|
+
this._setDisabled("rotationRightInput", !hasImage || isBusy);
|
|
3267
|
+
this._setDisabled("maskList", !hasImage || isBusy);
|
|
3095
3268
|
this._setDisabled("imageInput", isBusy);
|
|
3096
3269
|
this._setDisabled("uploadArea", isBusy);
|
|
3097
3270
|
}
|
|
@@ -3406,9 +3579,9 @@
|
|
|
3406
3579
|
execute(command) {
|
|
3407
3580
|
const result = command.execute();
|
|
3408
3581
|
if (result && typeof result.then === "function") {
|
|
3409
|
-
return Promise.resolve(result).then(() => {
|
|
3582
|
+
return this.enqueue(() => Promise.resolve(result).then(() => {
|
|
3410
3583
|
this.push(command);
|
|
3411
|
-
});
|
|
3584
|
+
}));
|
|
3412
3585
|
}
|
|
3413
3586
|
this.push(command);
|
|
3414
3587
|
return result;
|