@bensitu/image-editor 1.5.0 → 1.5.1
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 +67 -32
- package/dist/image-editor.cjs +4185 -0
- package/dist/image-editor.cjs.map +7 -0
- package/dist/image-editor.esm.js +576 -259
- 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 +576 -259
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +576 -259
- 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 +4 -3
- package/package.json +4 -3
- package/src/image-editor.js +502 -146
package/dist/image-editor.esm.js
CHANGED
|
@@ -5,7 +5,7 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.5.
|
|
8
|
+
* @version 1.5.1
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -77,6 +77,7 @@ var ImageEditor = class {
|
|
|
77
77
|
downsampleMimeType: null,
|
|
78
78
|
imageLoadTimeoutMs: 3e4,
|
|
79
79
|
exportMultiplier: 1,
|
|
80
|
+
maxExportPixels: 5e7,
|
|
80
81
|
exportImageAreaByDefault: true,
|
|
81
82
|
defaultMaskWidth: 50,
|
|
82
83
|
defaultMaskHeight: 80,
|
|
@@ -146,6 +147,8 @@ var ImageEditor = class {
|
|
|
146
147
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
147
148
|
this._disposed = false;
|
|
148
149
|
this._initialized = false;
|
|
150
|
+
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
151
|
+
this._cropRotationWarningEmitted = false;
|
|
149
152
|
this.onImageLoaded = typeof this.options.onImageLoaded === "function" ? this.options.onImageLoaded : null;
|
|
150
153
|
this.animationQueue = new AnimationQueue();
|
|
151
154
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
@@ -210,7 +213,13 @@ var ImageEditor = class {
|
|
|
210
213
|
* });
|
|
211
214
|
*/
|
|
212
215
|
init(idMap = {}) {
|
|
213
|
-
if (!this._fabricLoaded)
|
|
216
|
+
if (!this._fabricLoaded) {
|
|
217
|
+
this._fabricLoaded = !!ensureFabric();
|
|
218
|
+
if (!this._fabricLoaded) {
|
|
219
|
+
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
214
223
|
if (this._initialized || this.canvas) this.dispose();
|
|
215
224
|
this._disposed = false;
|
|
216
225
|
this._initialized = true;
|
|
@@ -225,7 +234,6 @@ var ImageEditor = class {
|
|
|
225
234
|
this._containerOriginalOverflow = null;
|
|
226
235
|
this._lastContainerViewportSize = null;
|
|
227
236
|
this._canvasElementOriginalStyle = null;
|
|
228
|
-
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
229
237
|
const defaults = {
|
|
230
238
|
canvas: "fabricCanvas",
|
|
231
239
|
canvasContainer: null,
|
|
@@ -264,6 +272,7 @@ var ImageEditor = class {
|
|
|
264
272
|
redoButton: "redoButton",
|
|
265
273
|
redoBtn: null,
|
|
266
274
|
imageInput: "imageInput",
|
|
275
|
+
uploadArea: null,
|
|
267
276
|
enterCropModeButton: "enterCropModeButton",
|
|
268
277
|
cropBtn: null,
|
|
269
278
|
applyCropButton: "applyCropButton",
|
|
@@ -279,7 +288,7 @@ var ImageEditor = class {
|
|
|
279
288
|
this._updateMaskList();
|
|
280
289
|
this._updateUI();
|
|
281
290
|
if (this.options.initialImageBase64) {
|
|
282
|
-
this.loadImage(this.options.initialImageBase64);
|
|
291
|
+
this.loadImage(this.options.initialImageBase64).catch((error) => this._reportError("initialImageBase64 could not be loaded", error));
|
|
283
292
|
} else {
|
|
284
293
|
this._updatePlaceholderStatus();
|
|
285
294
|
}
|
|
@@ -480,13 +489,14 @@ var ImageEditor = class {
|
|
|
480
489
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
481
490
|
this._captureContainerOverflowState();
|
|
482
491
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
483
|
-
|
|
492
|
+
const layoutMode = this._getImageLayoutMode();
|
|
493
|
+
if (layoutMode === "cover") {
|
|
484
494
|
this.containerElement.style.overflow = "scroll";
|
|
485
495
|
if (!shouldPreserveScroll) {
|
|
486
496
|
this.containerElement.scrollLeft = 0;
|
|
487
497
|
this.containerElement.scrollTop = 0;
|
|
488
498
|
}
|
|
489
|
-
} else if (
|
|
499
|
+
} else if (layoutMode === "fit") {
|
|
490
500
|
this.containerElement.style.overflow = "auto";
|
|
491
501
|
if (!shouldPreserveScroll) {
|
|
492
502
|
this.containerElement.scrollLeft = 0;
|
|
@@ -637,6 +647,12 @@ var ImageEditor = class {
|
|
|
637
647
|
`Only one image layout mode should be enabled. Active modes: ${activeModes.join(", ")}.`
|
|
638
648
|
);
|
|
639
649
|
}
|
|
650
|
+
_getImageLayoutMode() {
|
|
651
|
+
if (this.options.fitImageToCanvas) return "fit";
|
|
652
|
+
if (this.options.coverImageToCanvas) return "cover";
|
|
653
|
+
if (this.options.expandCanvasToImage) return "expand";
|
|
654
|
+
return "contain";
|
|
655
|
+
}
|
|
640
656
|
/**
|
|
641
657
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
642
658
|
*
|
|
@@ -650,12 +666,16 @@ var ImageEditor = class {
|
|
|
650
666
|
if (!this._fabricLoaded) return;
|
|
651
667
|
if (!this.canvas || this._disposed) return;
|
|
652
668
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
669
|
+
options = options || {};
|
|
653
670
|
this._assertIdleForOperation("loadImage", options);
|
|
654
|
-
|
|
655
|
-
this.
|
|
656
|
-
|
|
657
|
-
const transaction = this._captureLoadImageTransaction();
|
|
671
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
672
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
|
|
673
|
+
let transaction = null;
|
|
658
674
|
try {
|
|
675
|
+
this._isLoading = true;
|
|
676
|
+
this._updateUI();
|
|
677
|
+
this._warnOnImageLayoutOptionConflict();
|
|
678
|
+
transaction = this._captureLoadImageTransaction();
|
|
659
679
|
const imageElement = await this._createImageElement(imageBase64);
|
|
660
680
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
661
681
|
let loadSource = imageBase64;
|
|
@@ -695,7 +715,8 @@ var ImageEditor = class {
|
|
|
695
715
|
const viewport = this._getContainerViewportSize();
|
|
696
716
|
const minWidth = viewport.width;
|
|
697
717
|
const minHeight = viewport.height;
|
|
698
|
-
|
|
718
|
+
const layoutMode = this._getImageLayoutMode();
|
|
719
|
+
if (layoutMode === "fit") {
|
|
699
720
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
700
721
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
701
722
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -703,13 +724,13 @@ var ImageEditor = class {
|
|
|
703
724
|
fabricImage.set({ left: 0, top: 0 });
|
|
704
725
|
fabricImage.scale(fitScale);
|
|
705
726
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
706
|
-
} else if (
|
|
727
|
+
} else if (layoutMode === "cover") {
|
|
707
728
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
708
729
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
709
730
|
fabricImage.set({ left: 0, top: 0 });
|
|
710
731
|
fabricImage.scale(layout.scale);
|
|
711
732
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
712
|
-
} else if (
|
|
733
|
+
} else if (layoutMode === "expand") {
|
|
713
734
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
714
735
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
715
736
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -740,10 +761,14 @@ var ImageEditor = class {
|
|
|
740
761
|
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
741
762
|
this._notifyImageLoaded();
|
|
742
763
|
} catch (error) {
|
|
743
|
-
await this._rollbackLoadImageTransaction(
|
|
764
|
+
await this._rollbackLoadImageTransaction(
|
|
765
|
+
transaction,
|
|
766
|
+
this._withInternalOperationOptions(operationToken)
|
|
767
|
+
);
|
|
744
768
|
throw error;
|
|
745
769
|
} finally {
|
|
746
770
|
this._isLoading = false;
|
|
771
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
747
772
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
748
773
|
}
|
|
749
774
|
}
|
|
@@ -856,13 +881,13 @@ var ImageEditor = class {
|
|
|
856
881
|
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
857
882
|
};
|
|
858
883
|
}
|
|
859
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
884
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
860
885
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
861
886
|
let didRestoreCanvasState = false;
|
|
862
887
|
let didFailCanvasRestore = false;
|
|
863
888
|
try {
|
|
864
889
|
if (transaction.canvasState) {
|
|
865
|
-
await this.loadFromState(transaction.canvasState);
|
|
890
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
866
891
|
didRestoreCanvasState = true;
|
|
867
892
|
}
|
|
868
893
|
} catch (error) {
|
|
@@ -1112,9 +1137,9 @@ var ImageEditor = class {
|
|
|
1112
1137
|
}
|
|
1113
1138
|
_getScrollableCanvasSize(contentWidth, contentHeight, viewport = this._getContainerViewportSize()) {
|
|
1114
1139
|
if (this._hasFixedContainerScrollbars()) {
|
|
1115
|
-
const
|
|
1116
|
-
const safeWidth = Math.max(1, viewport.width -
|
|
1117
|
-
const safeHeight = Math.max(1, viewport.height -
|
|
1140
|
+
const safetyMargin2 = this._getScrollSafetyMargin();
|
|
1141
|
+
const safeWidth = Math.max(1, viewport.width - safetyMargin2);
|
|
1142
|
+
const safeHeight = Math.max(1, viewport.height - safetyMargin2);
|
|
1118
1143
|
return {
|
|
1119
1144
|
width: contentWidth > viewport.width + 0.5 ? this._ceilCanvasDimension(contentWidth) : safeWidth,
|
|
1120
1145
|
height: contentHeight > viewport.height + 0.5 ? this._ceilCanvasDimension(contentHeight) : safeHeight,
|
|
@@ -1140,9 +1165,17 @@ var ImageEditor = class {
|
|
|
1140
1165
|
}
|
|
1141
1166
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1142
1167
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1168
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1169
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1170
|
+
const shouldReserveNoScrollbarMargin = layoutMode === "fit" || layoutMode === "cover";
|
|
1171
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1172
|
+
const margin = hasOppositeScrollbar ? safetyMargin : shouldReserveNoScrollbarMargin ? 1 : 0;
|
|
1173
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1174
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1175
|
+
};
|
|
1143
1176
|
return {
|
|
1144
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1145
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1177
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1178
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1146
1179
|
viewportWidth: effectiveWidth,
|
|
1147
1180
|
viewportHeight: effectiveHeight,
|
|
1148
1181
|
hasHorizontal,
|
|
@@ -1264,6 +1297,45 @@ var ImageEditor = class {
|
|
|
1264
1297
|
});
|
|
1265
1298
|
}
|
|
1266
1299
|
}
|
|
1300
|
+
_getSerializableStateObjects() {
|
|
1301
|
+
if (!this.canvas) return [];
|
|
1302
|
+
return this.canvas.getObjects().filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1303
|
+
}
|
|
1304
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1305
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1306
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1307
|
+
const numericProperties = [
|
|
1308
|
+
"left",
|
|
1309
|
+
"top",
|
|
1310
|
+
"width",
|
|
1311
|
+
"height",
|
|
1312
|
+
"scaleX",
|
|
1313
|
+
"scaleY",
|
|
1314
|
+
"angle",
|
|
1315
|
+
"skewX",
|
|
1316
|
+
"skewY",
|
|
1317
|
+
"cropX",
|
|
1318
|
+
"cropY",
|
|
1319
|
+
"radius",
|
|
1320
|
+
"rx",
|
|
1321
|
+
"ry",
|
|
1322
|
+
"strokeWidth"
|
|
1323
|
+
];
|
|
1324
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1325
|
+
const fabricObject = fabricObjects[index];
|
|
1326
|
+
if (!serializedObject || !fabricObject) return;
|
|
1327
|
+
numericProperties.forEach((property) => {
|
|
1328
|
+
const numericValue = Number(fabricObject[property]);
|
|
1329
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1330
|
+
});
|
|
1331
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1332
|
+
serializedObject.points = fabricObject.points.map((point) => ({
|
|
1333
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1334
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1335
|
+
}));
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1267
1339
|
_restoreMaskControls(mask) {
|
|
1268
1340
|
if (!mask) return;
|
|
1269
1341
|
const cornerSize = Number(mask.cornerSize);
|
|
@@ -1309,6 +1381,7 @@ var ImageEditor = class {
|
|
|
1309
1381
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1310
1382
|
if (Array.isArray(jsonObject.objects)) {
|
|
1311
1383
|
jsonObject.objects = jsonObject.objects.filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1384
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1312
1385
|
}
|
|
1313
1386
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1314
1387
|
return JSON.stringify(jsonObject);
|
|
@@ -1383,6 +1456,12 @@ var ImageEditor = class {
|
|
|
1383
1456
|
if (!Number.isFinite(numericValue)) return false;
|
|
1384
1457
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1385
1458
|
}
|
|
1459
|
+
_hasScaledImageEdge(axis) {
|
|
1460
|
+
if (!this.originalImage) return false;
|
|
1461
|
+
const scale = Number(axis === "y" ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1462
|
+
if (!Number.isFinite(scale)) return false;
|
|
1463
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1464
|
+
}
|
|
1386
1465
|
_getPartialExportEdges(bounds) {
|
|
1387
1466
|
if (!bounds) return null;
|
|
1388
1467
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1391,8 +1470,8 @@ var ImageEditor = class {
|
|
|
1391
1470
|
return {
|
|
1392
1471
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1393
1472
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1394
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1395
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1473
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge("x"),
|
|
1474
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge("y")
|
|
1396
1475
|
};
|
|
1397
1476
|
}
|
|
1398
1477
|
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
@@ -1452,7 +1531,8 @@ var ImageEditor = class {
|
|
|
1452
1531
|
* @private
|
|
1453
1532
|
*/
|
|
1454
1533
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1455
|
-
const safeMultiplier =
|
|
1534
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1535
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1456
1536
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1457
1537
|
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1458
1538
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1468,6 +1548,25 @@ var ImageEditor = class {
|
|
|
1468
1548
|
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1469
1549
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1470
1550
|
}
|
|
1551
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1552
|
+
const numericMultiplier = Number(multiplier);
|
|
1553
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1554
|
+
throw new Error("Export multiplier must be a finite positive number");
|
|
1555
|
+
}
|
|
1556
|
+
return Math.max(1, numericMultiplier);
|
|
1557
|
+
}
|
|
1558
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1559
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1560
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1561
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1562
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1563
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1564
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1565
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0 ? Math.floor(configuredMaxPixels) : 5e7;
|
|
1566
|
+
if (outputPixels > maxPixels) {
|
|
1567
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1471
1570
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1472
1571
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1473
1572
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1512,6 +1611,7 @@ var ImageEditor = class {
|
|
|
1512
1611
|
}
|
|
1513
1612
|
_decodeBase64Payload(base64Payload) {
|
|
1514
1613
|
const payload = String(base64Payload || "");
|
|
1614
|
+
if (!payload) throw new Error("Data URL base64 payload is empty");
|
|
1515
1615
|
if (typeof atob === "function") {
|
|
1516
1616
|
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1517
1617
|
}
|
|
@@ -1520,6 +1620,13 @@ var ImageEditor = class {
|
|
|
1520
1620
|
}
|
|
1521
1621
|
throw new Error("Base64 decoding is unavailable");
|
|
1522
1622
|
}
|
|
1623
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
1624
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
1625
|
+
if (!match || !match[2]) {
|
|
1626
|
+
throw new Error("Export produced an invalid or empty base64 data URL");
|
|
1627
|
+
}
|
|
1628
|
+
return this._decodeBase64Payload(match[2]);
|
|
1629
|
+
}
|
|
1523
1630
|
/**
|
|
1524
1631
|
* Gets the top-left corner coordinates of the given object.
|
|
1525
1632
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1629,13 +1736,42 @@ var ImageEditor = class {
|
|
|
1629
1736
|
const currentHeight = this.canvas.getHeight();
|
|
1630
1737
|
let requiredWidth = currentWidth;
|
|
1631
1738
|
let requiredHeight = currentHeight;
|
|
1632
|
-
|
|
1739
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1740
|
+
const usesScrollableFitBounds = layoutMode === "fit" || layoutMode === "cover";
|
|
1741
|
+
let contentWidth = 0;
|
|
1742
|
+
let contentHeight = 0;
|
|
1743
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1633
1744
|
if (!fabricObject) return;
|
|
1634
1745
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
1635
1746
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1636
|
-
|
|
1637
|
-
|
|
1747
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
1748
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
1749
|
+
contentWidth = Math.max(contentWidth, right);
|
|
1750
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
1751
|
+
return { right, bottom };
|
|
1752
|
+
};
|
|
1753
|
+
fabricObjects.forEach((fabricObject) => {
|
|
1754
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
1755
|
+
if (!bounds) return;
|
|
1756
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
1757
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1638
1758
|
});
|
|
1759
|
+
if (usesScrollableFitBounds) {
|
|
1760
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
1761
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1762
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
1763
|
+
});
|
|
1764
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
1765
|
+
Math.max(1, contentWidth),
|
|
1766
|
+
Math.max(1, contentHeight)
|
|
1767
|
+
);
|
|
1768
|
+
const newWidth2 = contentSize.hasHorizontal ? Math.max(currentWidth, contentSize.width) : contentSize.width;
|
|
1769
|
+
const newHeight2 = contentSize.hasVertical ? Math.max(currentHeight, contentSize.height) : contentSize.height;
|
|
1770
|
+
if (newWidth2 !== currentWidth || newHeight2 !== currentHeight) {
|
|
1771
|
+
this._setCanvasSizeInt(newWidth2, newHeight2);
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1639
1775
|
let minWidth = 0;
|
|
1640
1776
|
let minHeight = 0;
|
|
1641
1777
|
if (this.containerElement) {
|
|
@@ -1653,16 +1789,60 @@ var ImageEditor = class {
|
|
|
1653
1789
|
this._reportWarning("expandCanvasToFitObjects: failed to expand canvas", error);
|
|
1654
1790
|
}
|
|
1655
1791
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1792
|
+
_captureImageDisplayBounds() {
|
|
1793
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
1794
|
+
this.originalImage.setCoords();
|
|
1795
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
1796
|
+
const width = Number(bounds && bounds.width);
|
|
1797
|
+
const height = Number(bounds && bounds.height);
|
|
1798
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
1799
|
+
return {
|
|
1800
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
1801
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
1802
|
+
width,
|
|
1803
|
+
height
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
1807
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
1808
|
+
const imageWidth = Number(this.originalImage.width);
|
|
1809
|
+
const imageHeight = Number(this.originalImage.height);
|
|
1810
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
1811
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
1812
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
1813
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
1814
|
+
const left = Number(displayBounds.left) || 0;
|
|
1815
|
+
const top = Number(displayBounds.top) || 0;
|
|
1816
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
1817
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
1818
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
1819
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
1820
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1821
|
+
if (layoutMode === "fit" || layoutMode === "cover") {
|
|
1822
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
1823
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
1824
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
1825
|
+
}
|
|
1826
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
1827
|
+
this._setCanvasSizeInt(
|
|
1828
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
1829
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
this.originalImage.set({
|
|
1833
|
+
originX: "left",
|
|
1834
|
+
originY: "top",
|
|
1835
|
+
left,
|
|
1836
|
+
top,
|
|
1837
|
+
scaleX,
|
|
1838
|
+
scaleY
|
|
1839
|
+
});
|
|
1840
|
+
this.originalImage.setCoords();
|
|
1841
|
+
this.baseImageScale = scaleX;
|
|
1842
|
+
this.currentScale = 1;
|
|
1843
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1844
|
+
this._updateInputs();
|
|
1845
|
+
this.canvas.renderAll();
|
|
1666
1846
|
}
|
|
1667
1847
|
/**
|
|
1668
1848
|
* Scales the original image by a given factor, with animation.
|
|
@@ -1677,7 +1857,14 @@ var ImageEditor = class {
|
|
|
1677
1857
|
} catch (error) {
|
|
1678
1858
|
return Promise.reject(error);
|
|
1679
1859
|
}
|
|
1680
|
-
return this.animationQueue.add(
|
|
1860
|
+
return this.animationQueue.add(async () => {
|
|
1861
|
+
const operationToken = this._beginBusyOperation("scaleImage");
|
|
1862
|
+
try {
|
|
1863
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
1864
|
+
} finally {
|
|
1865
|
+
this._endBusyOperation(operationToken);
|
|
1866
|
+
}
|
|
1867
|
+
}).finally(() => {
|
|
1681
1868
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1682
1869
|
});
|
|
1683
1870
|
}
|
|
@@ -1720,7 +1907,7 @@ var ImageEditor = class {
|
|
|
1720
1907
|
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1721
1908
|
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1722
1909
|
}
|
|
1723
|
-
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1910
|
+
if ((this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) && !isOwnInternalOperation) {
|
|
1724
1911
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1725
1912
|
}
|
|
1726
1913
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -1833,7 +2020,7 @@ var ImageEditor = class {
|
|
|
1833
2020
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1834
2021
|
});
|
|
1835
2022
|
this._updateInputs();
|
|
1836
|
-
if (saveHistory) this.saveState();
|
|
2023
|
+
if (saveHistory) this.saveState(options);
|
|
1837
2024
|
} finally {
|
|
1838
2025
|
if (didStartAnimation) {
|
|
1839
2026
|
this.isAnimating = false;
|
|
@@ -1855,7 +2042,14 @@ var ImageEditor = class {
|
|
|
1855
2042
|
} catch (error) {
|
|
1856
2043
|
return Promise.reject(error);
|
|
1857
2044
|
}
|
|
1858
|
-
return this.animationQueue.add(
|
|
2045
|
+
return this.animationQueue.add(async () => {
|
|
2046
|
+
const operationToken = this._beginBusyOperation("rotateImage");
|
|
2047
|
+
try {
|
|
2048
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2049
|
+
} finally {
|
|
2050
|
+
this._endBusyOperation(operationToken);
|
|
2051
|
+
}
|
|
2052
|
+
}).finally(() => {
|
|
1859
2053
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1860
2054
|
});
|
|
1861
2055
|
}
|
|
@@ -1898,7 +2092,7 @@ var ImageEditor = class {
|
|
|
1898
2092
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1899
2093
|
});
|
|
1900
2094
|
this._updateInputs();
|
|
1901
|
-
if (saveHistory) this.saveState();
|
|
2095
|
+
if (saveHistory) this.saveState(options);
|
|
1902
2096
|
didCompleteRotation = true;
|
|
1903
2097
|
} finally {
|
|
1904
2098
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -1925,19 +2119,22 @@ var ImageEditor = class {
|
|
|
1925
2119
|
return Promise.reject(error);
|
|
1926
2120
|
}
|
|
1927
2121
|
return this.animationQueue.add(async () => {
|
|
2122
|
+
const operationToken = this._beginBusyOperation("resetImageTransform");
|
|
1928
2123
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1929
2124
|
try {
|
|
1930
|
-
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1931
|
-
await this._rotateImageImpl(0, { saveHistory: false });
|
|
2125
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2126
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
1932
2127
|
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1933
2128
|
this._pushStateTransition(before, after);
|
|
1934
2129
|
} catch (error) {
|
|
1935
2130
|
try {
|
|
1936
|
-
await this.loadFromState(before);
|
|
2131
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
1937
2132
|
} catch (restoreError) {
|
|
1938
2133
|
this._reportError("resetImageTransform rollback failed", restoreError);
|
|
1939
2134
|
}
|
|
1940
2135
|
throw error;
|
|
2136
|
+
} finally {
|
|
2137
|
+
this._endBusyOperation(operationToken);
|
|
1941
2138
|
}
|
|
1942
2139
|
}).finally(() => {
|
|
1943
2140
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
@@ -1962,8 +2159,13 @@ var ImageEditor = class {
|
|
|
1962
2159
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
1963
2160
|
* @public
|
|
1964
2161
|
*/
|
|
1965
|
-
loadFromState(serializedState) {
|
|
2162
|
+
loadFromState(serializedState, options = {}) {
|
|
1966
2163
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2164
|
+
try {
|
|
2165
|
+
this._assertIdleForOperation("loadFromState", options);
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
return Promise.reject(error);
|
|
2168
|
+
}
|
|
1967
2169
|
if (this._cropMode || this._cropRect) {
|
|
1968
2170
|
this._removeCropRect();
|
|
1969
2171
|
this._restoreCropObjectState();
|
|
@@ -2120,22 +2322,29 @@ var ImageEditor = class {
|
|
|
2120
2322
|
* @returns {void}
|
|
2121
2323
|
* @public
|
|
2122
2324
|
*/
|
|
2123
|
-
saveState() {
|
|
2325
|
+
saveState(options = {}) {
|
|
2124
2326
|
if (!this.canvas) return;
|
|
2327
|
+
try {
|
|
2328
|
+
this._assertIdleForOperation("saveState", options);
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
this._reportError("saveState blocked", error);
|
|
2331
|
+
this._updateUI();
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2125
2334
|
try {
|
|
2126
2335
|
const after = this._captureCanvasStateOrThrow("saveState");
|
|
2127
2336
|
const before = this._lastSnapshot || after;
|
|
2128
2337
|
if (after === before) return;
|
|
2129
2338
|
let executedOnce = false;
|
|
2130
2339
|
const command = new Command(
|
|
2131
|
-
() => {
|
|
2340
|
+
(commandOptions = {}) => {
|
|
2132
2341
|
if (executedOnce) {
|
|
2133
|
-
return this.loadFromState(after);
|
|
2342
|
+
return this.loadFromState(after, commandOptions);
|
|
2134
2343
|
}
|
|
2135
2344
|
executedOnce = true;
|
|
2136
2345
|
return void 0;
|
|
2137
2346
|
},
|
|
2138
|
-
() => this.loadFromState(before)
|
|
2347
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2139
2348
|
);
|
|
2140
2349
|
this.historyManager.execute(command);
|
|
2141
2350
|
this._lastSnapshot = after;
|
|
@@ -2164,8 +2373,8 @@ var ImageEditor = class {
|
|
|
2164
2373
|
if (before === after) return;
|
|
2165
2374
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2166
2375
|
const command = new Command(
|
|
2167
|
-
() => this.loadFromState(after),
|
|
2168
|
-
() => this.loadFromState(before)
|
|
2376
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2377
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2169
2378
|
);
|
|
2170
2379
|
this.historyManager.push(command);
|
|
2171
2380
|
this._lastSnapshot = after;
|
|
@@ -2178,8 +2387,16 @@ var ImageEditor = class {
|
|
|
2178
2387
|
* @public
|
|
2179
2388
|
*/
|
|
2180
2389
|
undo() {
|
|
2181
|
-
|
|
2390
|
+
try {
|
|
2391
|
+
this._assertIdleForOperation("undo");
|
|
2392
|
+
} catch (error) {
|
|
2393
|
+
return Promise.reject(error);
|
|
2394
|
+
}
|
|
2395
|
+
const operationToken = this._beginBusyOperation("undo");
|
|
2396
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2182
2397
|
this._updateUI();
|
|
2398
|
+
}).finally(() => {
|
|
2399
|
+
this._endBusyOperation(operationToken);
|
|
2183
2400
|
}).catch((error) => {
|
|
2184
2401
|
this._reportError("undo failed", error);
|
|
2185
2402
|
throw error;
|
|
@@ -2192,8 +2409,16 @@ var ImageEditor = class {
|
|
|
2192
2409
|
* @public
|
|
2193
2410
|
*/
|
|
2194
2411
|
redo() {
|
|
2195
|
-
|
|
2412
|
+
try {
|
|
2413
|
+
this._assertIdleForOperation("redo");
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
return Promise.reject(error);
|
|
2416
|
+
}
|
|
2417
|
+
const operationToken = this._beginBusyOperation("redo");
|
|
2418
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2196
2419
|
this._updateUI();
|
|
2420
|
+
}).finally(() => {
|
|
2421
|
+
this._endBusyOperation(operationToken);
|
|
2197
2422
|
}).catch((error) => {
|
|
2198
2423
|
this._reportError("redo failed", error);
|
|
2199
2424
|
throw error;
|
|
@@ -2309,18 +2534,43 @@ var ImageEditor = class {
|
|
|
2309
2534
|
}
|
|
2310
2535
|
return value != null ? value : fallback;
|
|
2311
2536
|
};
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2537
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
2538
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
2539
|
+
return null;
|
|
2540
|
+
};
|
|
2541
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
2542
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
2543
|
+
const numericValue = Number(resolvedValue);
|
|
2544
|
+
if (!Number.isFinite(numericValue)) {
|
|
2545
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
2546
|
+
}
|
|
2547
|
+
if (constraints.positive && numericValue <= 0) {
|
|
2548
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
2549
|
+
}
|
|
2550
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
2551
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
2552
|
+
}
|
|
2553
|
+
return numericValue;
|
|
2554
|
+
};
|
|
2555
|
+
try {
|
|
2556
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, "width", "gap", { nonNegative: true });
|
|
2557
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, "width", "width", { positive: true });
|
|
2558
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, "height", "height", { positive: true });
|
|
2559
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, "width", "angle");
|
|
2560
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, "width", "alpha")));
|
|
2561
|
+
if (maskConfig.left === void 0 && this._lastMask) {
|
|
2562
|
+
const previousMask = this._lastMask;
|
|
2563
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
2564
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2565
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2566
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
2567
|
+
} else {
|
|
2568
|
+
left = resolveNumber(maskConfig.left, firstOffset, "width", "left");
|
|
2569
|
+
top = resolveNumber(maskConfig.top, firstOffset, "height", "top");
|
|
2570
|
+
}
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
return rejectInvalidMask("invalid numeric configuration", error);
|
|
2321
2573
|
}
|
|
2322
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
2323
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height");
|
|
2324
2574
|
maskConfig.left = left;
|
|
2325
2575
|
maskConfig.top = top;
|
|
2326
2576
|
let mask;
|
|
@@ -2329,10 +2579,15 @@ var ImageEditor = class {
|
|
|
2329
2579
|
} else {
|
|
2330
2580
|
switch (shapeType) {
|
|
2331
2581
|
case "circle":
|
|
2582
|
+
try {
|
|
2583
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, "min", "radius", { positive: true });
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return rejectInvalidMask("invalid circle radius", error);
|
|
2586
|
+
}
|
|
2332
2587
|
mask = new fabric.Circle({
|
|
2333
2588
|
left,
|
|
2334
2589
|
top,
|
|
2335
|
-
radius:
|
|
2590
|
+
radius: maskConfig.radius,
|
|
2336
2591
|
fill: maskConfig.color,
|
|
2337
2592
|
opacity: maskConfig.alpha,
|
|
2338
2593
|
angle: maskConfig.angle,
|
|
@@ -2340,11 +2595,17 @@ var ImageEditor = class {
|
|
|
2340
2595
|
});
|
|
2341
2596
|
break;
|
|
2342
2597
|
case "ellipse":
|
|
2598
|
+
try {
|
|
2599
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, "width", "rx", { positive: true });
|
|
2600
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, "height", "ry", { positive: true });
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
return rejectInvalidMask("invalid ellipse radius", error);
|
|
2603
|
+
}
|
|
2343
2604
|
mask = new fabric.Ellipse({
|
|
2344
2605
|
left,
|
|
2345
2606
|
top,
|
|
2346
|
-
rx:
|
|
2347
|
-
ry:
|
|
2607
|
+
rx: maskConfig.rx,
|
|
2608
|
+
ry: maskConfig.ry,
|
|
2348
2609
|
fill: maskConfig.color,
|
|
2349
2610
|
opacity: maskConfig.alpha,
|
|
2350
2611
|
angle: maskConfig.angle,
|
|
@@ -2353,8 +2614,20 @@ var ImageEditor = class {
|
|
|
2353
2614
|
break;
|
|
2354
2615
|
case "polygon": {
|
|
2355
2616
|
let polygonPoints = maskConfig.points || [];
|
|
2356
|
-
if (Array.isArray(polygonPoints)
|
|
2357
|
-
|
|
2617
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
2618
|
+
return rejectInvalidMask("polygon masks require at least three points");
|
|
2619
|
+
}
|
|
2620
|
+
try {
|
|
2621
|
+
polygonPoints = polygonPoints.map((point) => {
|
|
2622
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
2623
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
2624
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
2625
|
+
throw new Error("polygon point coordinates must be finite numbers");
|
|
2626
|
+
}
|
|
2627
|
+
return { x, y };
|
|
2628
|
+
});
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
return rejectInvalidMask("invalid polygon points", error);
|
|
2358
2631
|
}
|
|
2359
2632
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2360
2633
|
left,
|
|
@@ -2368,11 +2641,17 @@ var ImageEditor = class {
|
|
|
2368
2641
|
}
|
|
2369
2642
|
case "rect":
|
|
2370
2643
|
default:
|
|
2644
|
+
try {
|
|
2645
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, "width", "rx", { nonNegative: true });
|
|
2646
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, "height", "ry", { nonNegative: true });
|
|
2647
|
+
} catch (error) {
|
|
2648
|
+
return rejectInvalidMask("invalid rectangle corner radius", error);
|
|
2649
|
+
}
|
|
2371
2650
|
mask = new fabric.Rect({
|
|
2372
2651
|
left,
|
|
2373
2652
|
top,
|
|
2374
|
-
width:
|
|
2375
|
-
height:
|
|
2653
|
+
width: maskConfig.width,
|
|
2654
|
+
height: maskConfig.height,
|
|
2376
2655
|
fill: maskConfig.color,
|
|
2377
2656
|
opacity: maskConfig.alpha,
|
|
2378
2657
|
angle: maskConfig.angle,
|
|
@@ -2410,10 +2689,10 @@ var ImageEditor = class {
|
|
|
2410
2689
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2411
2690
|
});
|
|
2412
2691
|
this._rebindMaskEvents(mask);
|
|
2413
|
-
this.
|
|
2692
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2414
2693
|
this._lastMaskInitialLeft = left;
|
|
2415
2694
|
this._lastMaskInitialTop = top;
|
|
2416
|
-
this._lastMaskInitialWidth =
|
|
2695
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2417
2696
|
const maskId = ++this.maskCounter;
|
|
2418
2697
|
mask.set({
|
|
2419
2698
|
maskId,
|
|
@@ -2841,6 +3120,7 @@ var ImageEditor = class {
|
|
|
2841
3120
|
this._assertIdleForOperation("mergeMasks");
|
|
2842
3121
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2843
3122
|
if (!masks.length) return;
|
|
3123
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
2844
3124
|
const beforeJson = this._serializeCanvasState();
|
|
2845
3125
|
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2846
3126
|
this.canvas.discardActiveObject();
|
|
@@ -2859,12 +3139,13 @@ var ImageEditor = class {
|
|
|
2859
3139
|
preserveScroll: true,
|
|
2860
3140
|
resetMaskCounter: false
|
|
2861
3141
|
}));
|
|
3142
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
2862
3143
|
const afterJson = this._serializeCanvasState();
|
|
2863
3144
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2864
3145
|
} catch (error) {
|
|
2865
3146
|
this._reportError("merge error", error);
|
|
2866
3147
|
try {
|
|
2867
|
-
await this.loadFromState(beforeJson);
|
|
3148
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
2868
3149
|
} catch (restoreError) {
|
|
2869
3150
|
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2870
3151
|
}
|
|
@@ -2921,24 +3202,65 @@ var ImageEditor = class {
|
|
|
2921
3202
|
*/
|
|
2922
3203
|
async exportImageBase64(options = {}) {
|
|
2923
3204
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3205
|
+
options = options || {};
|
|
2924
3206
|
this._assertIdleForOperation("exportImageBase64", options);
|
|
3207
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3208
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageBase64");
|
|
2925
3209
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2926
3210
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2927
3211
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2928
3212
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3213
|
+
try {
|
|
3214
|
+
if (!exportImageArea) {
|
|
3215
|
+
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
3216
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3217
|
+
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
3218
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
3219
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
3220
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
3221
|
+
try {
|
|
3222
|
+
masks2.forEach((mask) => {
|
|
3223
|
+
mask.set({ visible: false });
|
|
3224
|
+
});
|
|
3225
|
+
this.canvas.discardActiveObject();
|
|
3226
|
+
this.canvas.renderAll();
|
|
3227
|
+
this.originalImage.setCoords();
|
|
3228
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3229
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3230
|
+
return await this._exportCanvasRegionToDataURL({
|
|
3231
|
+
...exportRegion,
|
|
3232
|
+
multiplier,
|
|
3233
|
+
quality,
|
|
3234
|
+
format,
|
|
3235
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3236
|
+
});
|
|
3237
|
+
} finally {
|
|
3238
|
+
maskVisibilityBackups.forEach((backup) => {
|
|
3239
|
+
try {
|
|
3240
|
+
backup.object.set({ visible: backup.visible });
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
void error;
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
3246
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
3247
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3248
|
+
this.canvas.renderAll();
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3252
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3253
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3254
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2936
3255
|
try {
|
|
2937
|
-
|
|
2938
|
-
mask.set({ visible: false });
|
|
2939
|
-
});
|
|
3256
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2940
3257
|
this.canvas.discardActiveObject();
|
|
2941
3258
|
this.canvas.renderAll();
|
|
3259
|
+
masks.forEach((mask) => {
|
|
3260
|
+
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
3261
|
+
mask.setCoords();
|
|
3262
|
+
});
|
|
3263
|
+
this.canvas.renderAll();
|
|
2942
3264
|
this.originalImage.setCoords();
|
|
2943
3265
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2944
3266
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
@@ -2950,47 +3272,13 @@ var ImageEditor = class {
|
|
|
2950
3272
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2951
3273
|
});
|
|
2952
3274
|
} finally {
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
} catch (error) {
|
|
2957
|
-
void error;
|
|
2958
|
-
}
|
|
2959
|
-
});
|
|
2960
|
-
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2961
|
-
this._restoreMaskLabelBackups(labelBackups2);
|
|
2962
|
-
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3275
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3276
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3277
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2963
3278
|
this.canvas.renderAll();
|
|
2964
3279
|
}
|
|
2965
|
-
}
|
|
2966
|
-
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2967
|
-
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2968
|
-
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2969
|
-
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2970
|
-
try {
|
|
2971
|
-
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2972
|
-
this.canvas.discardActiveObject();
|
|
2973
|
-
this.canvas.renderAll();
|
|
2974
|
-
masks.forEach((mask) => {
|
|
2975
|
-
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
2976
|
-
mask.setCoords();
|
|
2977
|
-
});
|
|
2978
|
-
this.canvas.renderAll();
|
|
2979
|
-
this.originalImage.setCoords();
|
|
2980
|
-
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2981
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2982
|
-
return await this._exportCanvasRegionToDataURL({
|
|
2983
|
-
...exportRegion,
|
|
2984
|
-
multiplier,
|
|
2985
|
-
quality,
|
|
2986
|
-
format,
|
|
2987
|
-
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2988
|
-
});
|
|
2989
3280
|
} finally {
|
|
2990
|
-
this.
|
|
2991
|
-
this._restoreMaskLabelBackups(labelBackups);
|
|
2992
|
-
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2993
|
-
this.canvas.renderAll();
|
|
3281
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2994
3282
|
}
|
|
2995
3283
|
}
|
|
2996
3284
|
/**
|
|
@@ -3023,7 +3311,10 @@ var ImageEditor = class {
|
|
|
3023
3311
|
*/
|
|
3024
3312
|
async exportImageFile(options = {}) {
|
|
3025
3313
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3026
|
-
|
|
3314
|
+
options = options || {};
|
|
3315
|
+
this._assertIdleForOperation("exportImageFile", options);
|
|
3316
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3317
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageFile");
|
|
3027
3318
|
const {
|
|
3028
3319
|
mergeMask = true,
|
|
3029
3320
|
fileType = "jpeg",
|
|
@@ -3033,48 +3324,52 @@ var ImageEditor = class {
|
|
|
3033
3324
|
} = options;
|
|
3034
3325
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3035
3326
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3327
|
+
try {
|
|
3328
|
+
let imageBase64;
|
|
3329
|
+
if (mergeMask) {
|
|
3330
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3331
|
+
exportImageArea: true,
|
|
3332
|
+
multiplier,
|
|
3333
|
+
quality: normalizedQuality,
|
|
3334
|
+
fileType: safeFileType
|
|
3335
|
+
}));
|
|
3336
|
+
} else {
|
|
3337
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3338
|
+
exportImageArea: false,
|
|
3339
|
+
multiplier,
|
|
3340
|
+
quality: normalizedQuality,
|
|
3341
|
+
fileType: safeFileType
|
|
3342
|
+
}));
|
|
3343
|
+
}
|
|
3344
|
+
let imageDataUrl = imageBase64;
|
|
3345
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3346
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3347
|
+
const imageElement = new window.Image();
|
|
3348
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3349
|
+
imageElement.onload = () => {
|
|
3350
|
+
try {
|
|
3351
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
3352
|
+
offscreenCanvas.width = imageElement.width;
|
|
3353
|
+
offscreenCanvas.height = imageElement.height;
|
|
3354
|
+
const context = offscreenCanvas.getContext("2d");
|
|
3355
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
3356
|
+
context.drawImage(imageElement, 0, 0);
|
|
3357
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3358
|
+
resolve(convertedDataUrl);
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
reject(error);
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
imageElement.onerror = reject;
|
|
3364
|
+
imageElement.src = imageBase64;
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3368
|
+
const mime = `image/${safeFileType}`;
|
|
3369
|
+
return new File([bytes], fileName, { type: mime });
|
|
3370
|
+
} finally {
|
|
3371
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3074
3372
|
}
|
|
3075
|
-
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
3076
|
-
const mime = `image/${safeFileType}`;
|
|
3077
|
-
return new File([bytes], fileName, { type: mime });
|
|
3078
3373
|
}
|
|
3079
3374
|
_clearMaskPlacementMemory() {
|
|
3080
3375
|
this._lastMask = null;
|
|
@@ -3082,7 +3377,7 @@ var ImageEditor = class {
|
|
|
3082
3377
|
this._lastMaskInitialTop = null;
|
|
3083
3378
|
this._lastMaskInitialWidth = null;
|
|
3084
3379
|
}
|
|
3085
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
3380
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
3086
3381
|
this._reportError(message, error);
|
|
3087
3382
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
3088
3383
|
this._cropRect = null;
|
|
@@ -3093,7 +3388,7 @@ var ImageEditor = class {
|
|
|
3093
3388
|
this._prevSelectionSetting = void 0;
|
|
3094
3389
|
if (beforeJson) {
|
|
3095
3390
|
try {
|
|
3096
|
-
await this.loadFromState(beforeJson);
|
|
3391
|
+
await this.loadFromState(beforeJson, options);
|
|
3097
3392
|
} catch (restoreError) {
|
|
3098
3393
|
this._reportError("applyCrop: rollback failed", restoreError);
|
|
3099
3394
|
}
|
|
@@ -3139,6 +3434,17 @@ var ImageEditor = class {
|
|
|
3139
3434
|
this._cropRect = null;
|
|
3140
3435
|
this._cropHandlers = [];
|
|
3141
3436
|
}
|
|
3437
|
+
_getCropRectContentBounds(cropRect) {
|
|
3438
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
3439
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
3440
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
3441
|
+
return {
|
|
3442
|
+
left: Number(cropRect.left) || 0,
|
|
3443
|
+
top: Number(cropRect.top) || 0,
|
|
3444
|
+
width,
|
|
3445
|
+
height
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3142
3448
|
/**
|
|
3143
3449
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
3144
3450
|
*
|
|
@@ -3162,14 +3468,19 @@ var ImageEditor = class {
|
|
|
3162
3468
|
const padding = this.options.crop && this.options.crop.padding ? this.options.crop.padding : 10;
|
|
3163
3469
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
3164
3470
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
3165
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
3166
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
3471
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
3472
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
3167
3473
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
3168
3474
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
3169
3475
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3170
3476
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3171
3477
|
const width = minCropWidth;
|
|
3172
3478
|
const height = minCropHeight;
|
|
3479
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
3480
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
3481
|
+
this._cropRotationWarningEmitted = true;
|
|
3482
|
+
this._reportWarning("crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported");
|
|
3483
|
+
}
|
|
3173
3484
|
const cropRect = new fabric.Rect({
|
|
3174
3485
|
left,
|
|
3175
3486
|
top,
|
|
@@ -3181,8 +3492,8 @@ var ImageEditor = class {
|
|
|
3181
3492
|
strokeWidth: 1,
|
|
3182
3493
|
strokeUniform: true,
|
|
3183
3494
|
selectable: true,
|
|
3184
|
-
hasRotatingPoint:
|
|
3185
|
-
lockRotation:
|
|
3495
|
+
hasRotatingPoint: false,
|
|
3496
|
+
lockRotation: true,
|
|
3186
3497
|
cornerSize: 8,
|
|
3187
3498
|
objectCaching: false,
|
|
3188
3499
|
originX: "left",
|
|
@@ -3219,7 +3530,7 @@ var ImageEditor = class {
|
|
|
3219
3530
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3220
3531
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3221
3532
|
cropRect.setCoords();
|
|
3222
|
-
const cropBounds =
|
|
3533
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3223
3534
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3224
3535
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3225
3536
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3293,94 +3604,100 @@ var ImageEditor = class {
|
|
|
3293
3604
|
async applyCrop() {
|
|
3294
3605
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3295
3606
|
this._assertIdleForOperation("applyCrop");
|
|
3296
|
-
this.
|
|
3297
|
-
const
|
|
3298
|
-
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3299
|
-
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3300
|
-
this._restoreCropObjectState();
|
|
3301
|
-
let beforeJson;
|
|
3302
|
-
try {
|
|
3303
|
-
beforeJson = this._serializeCanvasState();
|
|
3304
|
-
} catch (error) {
|
|
3305
|
-
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3306
|
-
beforeJson = null;
|
|
3307
|
-
}
|
|
3308
|
-
if (!beforeJson) {
|
|
3309
|
-
this.cancelCrop();
|
|
3310
|
-
return;
|
|
3311
|
-
}
|
|
3312
|
-
const preservedMasks = [];
|
|
3607
|
+
const operationToken = this._beginBusyOperation("applyCrop");
|
|
3608
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3313
3609
|
try {
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
mask.set({ visible: true });
|
|
3326
|
-
preservedMasks.push(mask);
|
|
3327
|
-
}
|
|
3328
|
-
});
|
|
3329
|
-
this._clearMaskPlacementMemory();
|
|
3330
|
-
this.canvas.discardActiveObject();
|
|
3331
|
-
this.canvas.renderAll();
|
|
3610
|
+
this._cropRect.setCoords();
|
|
3611
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3612
|
+
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3613
|
+
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3614
|
+
this._restoreCropObjectState();
|
|
3615
|
+
let beforeJson;
|
|
3616
|
+
try {
|
|
3617
|
+
beforeJson = this._serializeCanvasState();
|
|
3618
|
+
} catch (error) {
|
|
3619
|
+
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3620
|
+
beforeJson = null;
|
|
3332
3621
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3622
|
+
if (!beforeJson) {
|
|
3623
|
+
this.cancelCrop();
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
const preservedMasks = [];
|
|
3627
|
+
try {
|
|
3628
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3629
|
+
if (masks && masks.length) {
|
|
3630
|
+
masks.forEach((mask) => {
|
|
3631
|
+
mask.setCoords();
|
|
3632
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3633
|
+
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;
|
|
3634
|
+
this._removeLabelForMask(mask);
|
|
3635
|
+
this._cleanupMaskEvents(mask);
|
|
3636
|
+
this.canvas.remove(mask);
|
|
3637
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3638
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3639
|
+
mask.set({ visible: true });
|
|
3640
|
+
preservedMasks.push(mask);
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
this._clearMaskPlacementMemory();
|
|
3644
|
+
this.canvas.discardActiveObject();
|
|
3645
|
+
this.canvas.renderAll();
|
|
3646
|
+
}
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error, internalOptions);
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
this._removeCropRect();
|
|
3652
|
+
this._cropMode = false;
|
|
3653
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3654
|
+
this._prevSelectionSetting = void 0;
|
|
3655
|
+
let croppedBase64;
|
|
3656
|
+
try {
|
|
3657
|
+
croppedBase64 = await this._exportCanvasRegionToDataURL({
|
|
3658
|
+
...cropRegion,
|
|
3659
|
+
multiplier: 1,
|
|
3660
|
+
quality: this._normalizeQuality(this.options.downsampleQuality),
|
|
3661
|
+
format: "jpeg"
|
|
3360
3662
|
});
|
|
3361
|
-
|
|
3362
|
-
this.
|
|
3363
|
-
|
|
3364
|
-
this.canvas.renderAll();
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to create cropped image", error, internalOptions);
|
|
3665
|
+
return;
|
|
3365
3666
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3667
|
+
try {
|
|
3668
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3669
|
+
if (preservedMasks.length) {
|
|
3670
|
+
preservedMasks.forEach((mask) => {
|
|
3671
|
+
this._rebindMaskEvents(mask);
|
|
3672
|
+
this.canvas.add(mask);
|
|
3673
|
+
this.canvas.bringToFront(mask);
|
|
3674
|
+
});
|
|
3675
|
+
this._lastMask = preservedMasks[preservedMasks.length - 1];
|
|
3676
|
+
this.maskCounter = preservedMasks.reduce((max, mask) => Math.max(max, mask.maskId || 0), this.maskCounter);
|
|
3677
|
+
this._updateMaskList();
|
|
3678
|
+
this.canvas.renderAll();
|
|
3679
|
+
}
|
|
3680
|
+
} catch (error) {
|
|
3681
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", error, internalOptions);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
let afterJson;
|
|
3685
|
+
try {
|
|
3686
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
3687
|
+
} catch (error) {
|
|
3688
|
+
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
3689
|
+
afterJson = null;
|
|
3690
|
+
}
|
|
3691
|
+
try {
|
|
3692
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
3693
|
+
} catch (error) {
|
|
3694
|
+
this._reportWarning("applyCrop: failed to push history command", error);
|
|
3695
|
+
}
|
|
3696
|
+
this._updateUI();
|
|
3697
|
+
this.canvas.renderAll();
|
|
3698
|
+
} finally {
|
|
3699
|
+
this._endBusyOperation(operationToken);
|
|
3381
3700
|
}
|
|
3382
|
-
this._updateUI();
|
|
3383
|
-
this.canvas.renderAll();
|
|
3384
3701
|
}
|
|
3385
3702
|
/* ---------- Misc / UI ---------- */
|
|
3386
3703
|
/**
|
|
@@ -3800,11 +4117,11 @@ var HistoryManager = class {
|
|
|
3800
4117
|
*
|
|
3801
4118
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
3802
4119
|
*/
|
|
3803
|
-
undo() {
|
|
4120
|
+
undo(options = {}) {
|
|
3804
4121
|
return this.enqueue(async () => {
|
|
3805
4122
|
if (this.currentIndex >= 0) {
|
|
3806
4123
|
const index = this.currentIndex;
|
|
3807
|
-
await this.history[index].undo();
|
|
4124
|
+
await this.history[index].undo(options);
|
|
3808
4125
|
this.currentIndex = index - 1;
|
|
3809
4126
|
}
|
|
3810
4127
|
});
|
|
@@ -3814,11 +4131,11 @@ var HistoryManager = class {
|
|
|
3814
4131
|
*
|
|
3815
4132
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
3816
4133
|
*/
|
|
3817
|
-
redo() {
|
|
4134
|
+
redo(options = {}) {
|
|
3818
4135
|
return this.enqueue(async () => {
|
|
3819
4136
|
if (this.currentIndex < this.history.length - 1) {
|
|
3820
4137
|
const index = this.currentIndex + 1;
|
|
3821
|
-
await this.history[index].execute();
|
|
4138
|
+
await this.history[index].execute(options);
|
|
3822
4139
|
this.currentIndex = index;
|
|
3823
4140
|
}
|
|
3824
4141
|
});
|