@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/src/image-editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.5.
|
|
4
|
+
* @version 1.5.1
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -132,6 +132,7 @@ function ensureFabric() {
|
|
|
132
132
|
* @param {number} [options.downsampleQuality=0.92] - JPEG quality for downsampling/export.
|
|
133
133
|
* @param {number} [options.imageLoadTimeoutMs=30000] - Timeout for image decode operations.
|
|
134
134
|
* @param {number} [options.exportMultiplier=1] - Scale output image by this multiplier on export.
|
|
135
|
+
* @param {number} [options.maxExportPixels=50000000] - Maximum output pixels allowed per export.
|
|
135
136
|
* @param {boolean} [options.exportImageAreaByDefault=true] - Export only the image area (clipped to masks).
|
|
136
137
|
* @param {number} [options.defaultMaskWidth=50] - Default width of new masks.
|
|
137
138
|
* @param {number} [options.defaultMaskHeight=80] - Default height of new masks.
|
|
@@ -202,6 +203,7 @@ function ensureFabric() {
|
|
|
202
203
|
imageLoadTimeoutMs: 30000,
|
|
203
204
|
|
|
204
205
|
exportMultiplier: 1,
|
|
206
|
+
maxExportPixels: 50000000,
|
|
205
207
|
exportImageAreaByDefault: true,
|
|
206
208
|
|
|
207
209
|
defaultMaskWidth: 50,
|
|
@@ -283,6 +285,8 @@ function ensureFabric() {
|
|
|
283
285
|
this._activeAnimationRejectors = new Set();
|
|
284
286
|
this._disposed = false;
|
|
285
287
|
this._initialized = false;
|
|
288
|
+
this._deprecatedElementKeyWarnings = new Set();
|
|
289
|
+
this._cropRotationWarningEmitted = false;
|
|
286
290
|
|
|
287
291
|
this.onImageLoaded = typeof this.options.onImageLoaded === 'function' ? this.options.onImageLoaded : null;
|
|
288
292
|
|
|
@@ -356,7 +360,13 @@ function ensureFabric() {
|
|
|
356
360
|
* });
|
|
357
361
|
*/
|
|
358
362
|
init(idMap = {}) {
|
|
359
|
-
if (!this._fabricLoaded)
|
|
363
|
+
if (!this._fabricLoaded) {
|
|
364
|
+
this._fabricLoaded = !!ensureFabric();
|
|
365
|
+
if (!this._fabricLoaded) {
|
|
366
|
+
this._reportError('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
360
370
|
if (this._initialized || this.canvas) this.dispose();
|
|
361
371
|
this._disposed = false;
|
|
362
372
|
this._initialized = true;
|
|
@@ -371,7 +381,6 @@ function ensureFabric() {
|
|
|
371
381
|
this._containerOriginalOverflow = null;
|
|
372
382
|
this._lastContainerViewportSize = null;
|
|
373
383
|
this._canvasElementOriginalStyle = null;
|
|
374
|
-
this._deprecatedElementKeyWarnings = new Set();
|
|
375
384
|
|
|
376
385
|
const defaults = {
|
|
377
386
|
canvas: 'fabricCanvas',
|
|
@@ -410,6 +419,7 @@ function ensureFabric() {
|
|
|
410
419
|
redoButton: 'redoButton',
|
|
411
420
|
redoBtn: null,
|
|
412
421
|
imageInput: 'imageInput',
|
|
422
|
+
uploadArea: null,
|
|
413
423
|
enterCropModeButton: 'enterCropModeButton',
|
|
414
424
|
cropBtn: null,
|
|
415
425
|
applyCropButton: 'applyCropButton',
|
|
@@ -429,7 +439,8 @@ function ensureFabric() {
|
|
|
429
439
|
|
|
430
440
|
// Auto-load initial image if provided
|
|
431
441
|
if (this.options.initialImageBase64) {
|
|
432
|
-
this.loadImage(this.options.initialImageBase64)
|
|
442
|
+
this.loadImage(this.options.initialImageBase64)
|
|
443
|
+
.catch(error => this._reportError('initialImageBase64 could not be loaded', error));
|
|
433
444
|
} else {
|
|
434
445
|
this._updatePlaceholderStatus();
|
|
435
446
|
}
|
|
@@ -660,13 +671,14 @@ function ensureFabric() {
|
|
|
660
671
|
this._captureContainerOverflowState();
|
|
661
672
|
|
|
662
673
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
663
|
-
|
|
674
|
+
const layoutMode = this._getImageLayoutMode();
|
|
675
|
+
if (layoutMode === 'cover') {
|
|
664
676
|
this.containerElement.style.overflow = 'scroll';
|
|
665
677
|
if (!shouldPreserveScroll) {
|
|
666
678
|
this.containerElement.scrollLeft = 0;
|
|
667
679
|
this.containerElement.scrollTop = 0;
|
|
668
680
|
}
|
|
669
|
-
} else if (
|
|
681
|
+
} else if (layoutMode === 'fit') {
|
|
670
682
|
this.containerElement.style.overflow = 'auto';
|
|
671
683
|
if (!shouldPreserveScroll) {
|
|
672
684
|
this.containerElement.scrollLeft = 0;
|
|
@@ -838,6 +850,13 @@ function ensureFabric() {
|
|
|
838
850
|
);
|
|
839
851
|
}
|
|
840
852
|
|
|
853
|
+
_getImageLayoutMode() {
|
|
854
|
+
if (this.options.fitImageToCanvas) return 'fit';
|
|
855
|
+
if (this.options.coverImageToCanvas) return 'cover';
|
|
856
|
+
if (this.options.expandCanvasToImage) return 'expand';
|
|
857
|
+
return 'contain';
|
|
858
|
+
}
|
|
859
|
+
|
|
841
860
|
/**
|
|
842
861
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
843
862
|
*
|
|
@@ -851,14 +870,21 @@ function ensureFabric() {
|
|
|
851
870
|
if (!this._fabricLoaded) return;
|
|
852
871
|
if (!this.canvas || this._disposed) return;
|
|
853
872
|
if (!imageBase64 || typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) return;
|
|
873
|
+
options = options || {};
|
|
854
874
|
this._assertIdleForOperation('loadImage', options);
|
|
855
875
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
876
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
877
|
+
const operationToken = isNestedOperation
|
|
878
|
+
? this._getInternalOperationToken(options)
|
|
879
|
+
: this._beginBusyOperation('loadImage');
|
|
880
|
+
let transaction = null;
|
|
860
881
|
|
|
861
882
|
try {
|
|
883
|
+
this._isLoading = true;
|
|
884
|
+
this._updateUI();
|
|
885
|
+
this._warnOnImageLayoutOptionConflict();
|
|
886
|
+
transaction = this._captureLoadImageTransaction();
|
|
887
|
+
|
|
862
888
|
const imageElement = await this._createImageElement(imageBase64);
|
|
863
889
|
if (this._disposed || !this.canvas) throw new Error('Editor was disposed while loading image');
|
|
864
890
|
|
|
@@ -906,8 +932,9 @@ function ensureFabric() {
|
|
|
906
932
|
const viewport = this._getContainerViewportSize();
|
|
907
933
|
const minWidth = viewport.width;
|
|
908
934
|
const minHeight = viewport.height;
|
|
935
|
+
const layoutMode = this._getImageLayoutMode();
|
|
909
936
|
|
|
910
|
-
if (
|
|
937
|
+
if (layoutMode === 'fit') {
|
|
911
938
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
912
939
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
913
940
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -915,13 +942,13 @@ function ensureFabric() {
|
|
|
915
942
|
fabricImage.set({ left: 0, top: 0 });
|
|
916
943
|
fabricImage.scale(fitScale);
|
|
917
944
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
918
|
-
} else if (
|
|
945
|
+
} else if (layoutMode === 'cover') {
|
|
919
946
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
920
947
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
921
948
|
fabricImage.set({ left: 0, top: 0 });
|
|
922
949
|
fabricImage.scale(layout.scale);
|
|
923
950
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
924
|
-
} else if (
|
|
951
|
+
} else if (layoutMode === 'expand') {
|
|
925
952
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
926
953
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
927
954
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -957,10 +984,14 @@ function ensureFabric() {
|
|
|
957
984
|
|
|
958
985
|
this._notifyImageLoaded();
|
|
959
986
|
} catch (error) {
|
|
960
|
-
await this._rollbackLoadImageTransaction(
|
|
987
|
+
await this._rollbackLoadImageTransaction(
|
|
988
|
+
transaction,
|
|
989
|
+
this._withInternalOperationOptions(operationToken)
|
|
990
|
+
);
|
|
961
991
|
throw error;
|
|
962
992
|
} finally {
|
|
963
993
|
this._isLoading = false;
|
|
994
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
964
995
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
965
996
|
}
|
|
966
997
|
}
|
|
@@ -1092,13 +1123,13 @@ function ensureFabric() {
|
|
|
1092
1123
|
};
|
|
1093
1124
|
}
|
|
1094
1125
|
|
|
1095
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
1126
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
1096
1127
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
1097
1128
|
let didRestoreCanvasState = false;
|
|
1098
1129
|
let didFailCanvasRestore = false;
|
|
1099
1130
|
try {
|
|
1100
1131
|
if (transaction.canvasState) {
|
|
1101
|
-
await this.loadFromState(transaction.canvasState);
|
|
1132
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
1102
1133
|
didRestoreCanvasState = true;
|
|
1103
1134
|
}
|
|
1104
1135
|
} catch (error) {
|
|
@@ -1413,10 +1444,18 @@ function ensureFabric() {
|
|
|
1413
1444
|
|
|
1414
1445
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1415
1446
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1447
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1448
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1449
|
+
const shouldReserveNoScrollbarMargin = layoutMode === 'fit' || layoutMode === 'cover';
|
|
1450
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1451
|
+
const margin = hasOppositeScrollbar ? safetyMargin : (shouldReserveNoScrollbarMargin ? 1 : 0);
|
|
1452
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1453
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1454
|
+
};
|
|
1416
1455
|
|
|
1417
1456
|
return {
|
|
1418
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1419
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1457
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1458
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1420
1459
|
viewportWidth: effectiveWidth,
|
|
1421
1460
|
viewportHeight: effectiveHeight,
|
|
1422
1461
|
hasHorizontal,
|
|
@@ -1549,6 +1588,50 @@ function ensureFabric() {
|
|
|
1549
1588
|
}
|
|
1550
1589
|
}
|
|
1551
1590
|
|
|
1591
|
+
_getSerializableStateObjects() {
|
|
1592
|
+
if (!this.canvas) return [];
|
|
1593
|
+
return this.canvas.getObjects().filter(object => !object.isCropRect && !object.maskLabel);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1597
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1598
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1599
|
+
const numericProperties = [
|
|
1600
|
+
'left',
|
|
1601
|
+
'top',
|
|
1602
|
+
'width',
|
|
1603
|
+
'height',
|
|
1604
|
+
'scaleX',
|
|
1605
|
+
'scaleY',
|
|
1606
|
+
'angle',
|
|
1607
|
+
'skewX',
|
|
1608
|
+
'skewY',
|
|
1609
|
+
'cropX',
|
|
1610
|
+
'cropY',
|
|
1611
|
+
'radius',
|
|
1612
|
+
'rx',
|
|
1613
|
+
'ry',
|
|
1614
|
+
'strokeWidth'
|
|
1615
|
+
];
|
|
1616
|
+
|
|
1617
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1618
|
+
const fabricObject = fabricObjects[index];
|
|
1619
|
+
if (!serializedObject || !fabricObject) return;
|
|
1620
|
+
|
|
1621
|
+
numericProperties.forEach(property => {
|
|
1622
|
+
const numericValue = Number(fabricObject[property]);
|
|
1623
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1627
|
+
serializedObject.points = fabricObject.points.map(point => ({
|
|
1628
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1629
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1630
|
+
}));
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1552
1635
|
_restoreMaskControls(mask) {
|
|
1553
1636
|
if (!mask) return;
|
|
1554
1637
|
|
|
@@ -1598,6 +1681,7 @@ function ensureFabric() {
|
|
|
1598
1681
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1599
1682
|
if (Array.isArray(jsonObject.objects)) {
|
|
1600
1683
|
jsonObject.objects = jsonObject.objects.filter(object => !object.isCropRect && !object.maskLabel);
|
|
1684
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1601
1685
|
}
|
|
1602
1686
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1603
1687
|
return JSON.stringify(jsonObject);
|
|
@@ -1680,6 +1764,13 @@ function ensureFabric() {
|
|
|
1680
1764
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1681
1765
|
}
|
|
1682
1766
|
|
|
1767
|
+
_hasScaledImageEdge(axis) {
|
|
1768
|
+
if (!this.originalImage) return false;
|
|
1769
|
+
const scale = Number(axis === 'y' ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1770
|
+
if (!Number.isFinite(scale)) return false;
|
|
1771
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1683
1774
|
_getPartialExportEdges(bounds) {
|
|
1684
1775
|
if (!bounds) return null;
|
|
1685
1776
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1689,8 +1780,8 @@ function ensureFabric() {
|
|
|
1689
1780
|
return {
|
|
1690
1781
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1691
1782
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1692
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1693
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1783
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge('x'),
|
|
1784
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge('y')
|
|
1694
1785
|
};
|
|
1695
1786
|
}
|
|
1696
1787
|
|
|
@@ -1756,7 +1847,8 @@ function ensureFabric() {
|
|
|
1756
1847
|
* @private
|
|
1757
1848
|
*/
|
|
1758
1849
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg', sealPartialEdges = null }) {
|
|
1759
|
-
const safeMultiplier =
|
|
1850
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1851
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1760
1852
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1761
1853
|
const exportFormat = safeFormat === 'jpeg' ? 'png' : safeFormat;
|
|
1762
1854
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1774,6 +1866,30 @@ function ensureFabric() {
|
|
|
1774
1866
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1775
1867
|
}
|
|
1776
1868
|
|
|
1869
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1870
|
+
const numericMultiplier = Number(multiplier);
|
|
1871
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1872
|
+
throw new Error('Export multiplier must be a finite positive number');
|
|
1873
|
+
}
|
|
1874
|
+
return Math.max(1, numericMultiplier);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1878
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1879
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1880
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1881
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1882
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1883
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1884
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0
|
|
1885
|
+
? Math.floor(configuredMaxPixels)
|
|
1886
|
+
: 50000000;
|
|
1887
|
+
|
|
1888
|
+
if (outputPixels > maxPixels) {
|
|
1889
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1777
1893
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1778
1894
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1779
1895
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1826,6 +1942,7 @@ function ensureFabric() {
|
|
|
1826
1942
|
|
|
1827
1943
|
_decodeBase64Payload(base64Payload) {
|
|
1828
1944
|
const payload = String(base64Payload || '');
|
|
1945
|
+
if (!payload) throw new Error('Data URL base64 payload is empty');
|
|
1829
1946
|
if (typeof atob === 'function') {
|
|
1830
1947
|
return Uint8Array.from(atob(payload), char => char.charCodeAt(0));
|
|
1831
1948
|
}
|
|
@@ -1835,6 +1952,14 @@ function ensureFabric() {
|
|
|
1835
1952
|
throw new Error('Base64 decoding is unavailable');
|
|
1836
1953
|
}
|
|
1837
1954
|
|
|
1955
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
1956
|
+
const match = String(dataUrl || '').match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
1957
|
+
if (!match || !match[2]) {
|
|
1958
|
+
throw new Error('Export produced an invalid or empty base64 data URL');
|
|
1959
|
+
}
|
|
1960
|
+
return this._decodeBase64Payload(match[2]);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1838
1963
|
/**
|
|
1839
1964
|
* Gets the top-left corner coordinates of the given object.
|
|
1840
1965
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1953,13 +2078,49 @@ function ensureFabric() {
|
|
|
1953
2078
|
const currentHeight = this.canvas.getHeight();
|
|
1954
2079
|
let requiredWidth = currentWidth;
|
|
1955
2080
|
let requiredHeight = currentHeight;
|
|
1956
|
-
|
|
2081
|
+
const layoutMode = this._getImageLayoutMode();
|
|
2082
|
+
const usesScrollableFitBounds = layoutMode === 'fit' || layoutMode === 'cover';
|
|
2083
|
+
let contentWidth = 0;
|
|
2084
|
+
let contentHeight = 0;
|
|
2085
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1957
2086
|
if (!fabricObject) return;
|
|
1958
2087
|
if (typeof fabricObject.setCoords === 'function') fabricObject.setCoords();
|
|
1959
2088
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1960
|
-
|
|
1961
|
-
|
|
2089
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
2090
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
2091
|
+
contentWidth = Math.max(contentWidth, right);
|
|
2092
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
2093
|
+
return { right, bottom };
|
|
2094
|
+
};
|
|
2095
|
+
fabricObjects.forEach(fabricObject => {
|
|
2096
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
2097
|
+
if (!bounds) return;
|
|
2098
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
2099
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1962
2100
|
});
|
|
2101
|
+
if (usesScrollableFitBounds) {
|
|
2102
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
2103
|
+
this.canvas.getObjects().forEach(object => {
|
|
2104
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
2108
|
+
Math.max(1, contentWidth),
|
|
2109
|
+
Math.max(1, contentHeight)
|
|
2110
|
+
);
|
|
2111
|
+
|
|
2112
|
+
const newWidth = contentSize.hasHorizontal
|
|
2113
|
+
? Math.max(currentWidth, contentSize.width)
|
|
2114
|
+
: contentSize.width;
|
|
2115
|
+
const newHeight = contentSize.hasVertical
|
|
2116
|
+
? Math.max(currentHeight, contentSize.height)
|
|
2117
|
+
: contentSize.height;
|
|
2118
|
+
|
|
2119
|
+
if (newWidth !== currentWidth || newHeight !== currentHeight) {
|
|
2120
|
+
this._setCanvasSizeInt(newWidth, newHeight);
|
|
2121
|
+
}
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
1963
2124
|
let minWidth = 0;
|
|
1964
2125
|
let minHeight = 0;
|
|
1965
2126
|
if (this.containerElement) {
|
|
@@ -1979,16 +2140,65 @@ function ensureFabric() {
|
|
|
1979
2140
|
}
|
|
1980
2141
|
}
|
|
1981
2142
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2143
|
+
_captureImageDisplayBounds() {
|
|
2144
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
2145
|
+
this.originalImage.setCoords();
|
|
2146
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
2147
|
+
const width = Number(bounds && bounds.width);
|
|
2148
|
+
const height = Number(bounds && bounds.height);
|
|
2149
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
2150
|
+
|
|
2151
|
+
return {
|
|
2152
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
2153
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
2154
|
+
width,
|
|
2155
|
+
height
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
2160
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
2161
|
+
const imageWidth = Number(this.originalImage.width);
|
|
2162
|
+
const imageHeight = Number(this.originalImage.height);
|
|
2163
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
2164
|
+
|
|
2165
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
2166
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
2167
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
2168
|
+
|
|
2169
|
+
const left = Number(displayBounds.left) || 0;
|
|
2170
|
+
const top = Number(displayBounds.top) || 0;
|
|
2171
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
2172
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
2173
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
2174
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
2175
|
+
const layoutMode = this._getImageLayoutMode();
|
|
2176
|
+
if (layoutMode === 'fit' || layoutMode === 'cover') {
|
|
2177
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
2178
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
2179
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
2180
|
+
}
|
|
2181
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
2182
|
+
this._setCanvasSizeInt(
|
|
2183
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
2184
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
2185
|
+
);
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
this.originalImage.set({
|
|
2189
|
+
originX: 'left',
|
|
2190
|
+
originY: 'top',
|
|
2191
|
+
left,
|
|
2192
|
+
top,
|
|
2193
|
+
scaleX,
|
|
2194
|
+
scaleY
|
|
2195
|
+
});
|
|
2196
|
+
this.originalImage.setCoords();
|
|
2197
|
+
this.baseImageScale = scaleX;
|
|
2198
|
+
this.currentScale = 1;
|
|
2199
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
2200
|
+
this._updateInputs();
|
|
2201
|
+
this.canvas.renderAll();
|
|
1992
2202
|
}
|
|
1993
2203
|
|
|
1994
2204
|
/**
|
|
@@ -2004,7 +2214,14 @@ function ensureFabric() {
|
|
|
2004
2214
|
} catch (error) {
|
|
2005
2215
|
return Promise.reject(error);
|
|
2006
2216
|
}
|
|
2007
|
-
return this.animationQueue.add(() =>
|
|
2217
|
+
return this.animationQueue.add(async () => {
|
|
2218
|
+
const operationToken = this._beginBusyOperation('scaleImage');
|
|
2219
|
+
try {
|
|
2220
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
2221
|
+
} finally {
|
|
2222
|
+
this._endBusyOperation(operationToken);
|
|
2223
|
+
}
|
|
2224
|
+
})
|
|
2008
2225
|
.finally(() => {
|
|
2009
2226
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
2010
2227
|
});
|
|
@@ -2056,7 +2273,7 @@ function ensureFabric() {
|
|
|
2056
2273
|
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
2057
2274
|
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
2058
2275
|
}
|
|
2059
|
-
if (this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) {
|
|
2276
|
+
if ((this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) && !isOwnInternalOperation) {
|
|
2060
2277
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
2061
2278
|
}
|
|
2062
2279
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -2179,7 +2396,7 @@ function ensureFabric() {
|
|
|
2179
2396
|
this.canvas.getObjects().forEach(object => { if (object.maskId) this._syncMaskLabel(object); });
|
|
2180
2397
|
|
|
2181
2398
|
this._updateInputs();
|
|
2182
|
-
if (saveHistory) this.saveState();
|
|
2399
|
+
if (saveHistory) this.saveState(options);
|
|
2183
2400
|
} finally {
|
|
2184
2401
|
if (didStartAnimation) {
|
|
2185
2402
|
this.isAnimating = false;
|
|
@@ -2202,7 +2419,14 @@ function ensureFabric() {
|
|
|
2202
2419
|
} catch (error) {
|
|
2203
2420
|
return Promise.reject(error);
|
|
2204
2421
|
}
|
|
2205
|
-
return this.animationQueue.add(() =>
|
|
2422
|
+
return this.animationQueue.add(async () => {
|
|
2423
|
+
const operationToken = this._beginBusyOperation('rotateImage');
|
|
2424
|
+
try {
|
|
2425
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2426
|
+
} finally {
|
|
2427
|
+
this._endBusyOperation(operationToken);
|
|
2428
|
+
}
|
|
2429
|
+
})
|
|
2206
2430
|
.finally(() => {
|
|
2207
2431
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
2208
2432
|
});
|
|
@@ -2253,7 +2477,7 @@ function ensureFabric() {
|
|
|
2253
2477
|
this.canvas.getObjects().forEach(object => { if (object.maskId) this._syncMaskLabel(object); });
|
|
2254
2478
|
|
|
2255
2479
|
this._updateInputs();
|
|
2256
|
-
if (saveHistory) this.saveState();
|
|
2480
|
+
if (saveHistory) this.saveState(options);
|
|
2257
2481
|
didCompleteRotation = true;
|
|
2258
2482
|
} finally {
|
|
2259
2483
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -2282,19 +2506,22 @@ function ensureFabric() {
|
|
|
2282
2506
|
}
|
|
2283
2507
|
|
|
2284
2508
|
return this.animationQueue.add(async () => {
|
|
2509
|
+
const operationToken = this._beginBusyOperation('resetImageTransform');
|
|
2285
2510
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow('resetImageTransform');
|
|
2286
2511
|
try {
|
|
2287
|
-
await this._scaleImageImpl(1, { saveHistory: false });
|
|
2288
|
-
await this._rotateImageImpl(0, { saveHistory: false });
|
|
2512
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2513
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2289
2514
|
const after = this._captureCanvasStateOrThrow('resetImageTransform');
|
|
2290
2515
|
this._pushStateTransition(before, after);
|
|
2291
2516
|
} catch (error) {
|
|
2292
2517
|
try {
|
|
2293
|
-
await this.loadFromState(before);
|
|
2518
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
2294
2519
|
} catch (restoreError) {
|
|
2295
2520
|
this._reportError('resetImageTransform rollback failed', restoreError);
|
|
2296
2521
|
}
|
|
2297
2522
|
throw error;
|
|
2523
|
+
} finally {
|
|
2524
|
+
this._endBusyOperation(operationToken);
|
|
2298
2525
|
}
|
|
2299
2526
|
}).finally(() => {
|
|
2300
2527
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
@@ -2321,8 +2548,13 @@ function ensureFabric() {
|
|
|
2321
2548
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
2322
2549
|
* @public
|
|
2323
2550
|
*/
|
|
2324
|
-
loadFromState(serializedState) {
|
|
2551
|
+
loadFromState(serializedState, options = {}) {
|
|
2325
2552
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2553
|
+
try {
|
|
2554
|
+
this._assertIdleForOperation('loadFromState', options);
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
return Promise.reject(error);
|
|
2557
|
+
}
|
|
2326
2558
|
if (this._cropMode || this._cropRect) {
|
|
2327
2559
|
this._removeCropRect();
|
|
2328
2560
|
this._restoreCropObjectState();
|
|
@@ -2508,9 +2740,17 @@ function ensureFabric() {
|
|
|
2508
2740
|
* @returns {void}
|
|
2509
2741
|
* @public
|
|
2510
2742
|
*/
|
|
2511
|
-
saveState() {
|
|
2743
|
+
saveState(options = {}) {
|
|
2512
2744
|
if (!this.canvas) return;
|
|
2513
2745
|
|
|
2746
|
+
try {
|
|
2747
|
+
this._assertIdleForOperation('saveState', options);
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
this._reportError('saveState blocked', error);
|
|
2750
|
+
this._updateUI();
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2514
2754
|
try {
|
|
2515
2755
|
const after = this._captureCanvasStateOrThrow('saveState');
|
|
2516
2756
|
const before = this._lastSnapshot || after;
|
|
@@ -2518,14 +2758,14 @@ function ensureFabric() {
|
|
|
2518
2758
|
let executedOnce = false;
|
|
2519
2759
|
|
|
2520
2760
|
const command = new Command(
|
|
2521
|
-
() => {
|
|
2761
|
+
(commandOptions = {}) => {
|
|
2522
2762
|
if (executedOnce) {
|
|
2523
|
-
return this.loadFromState(after);
|
|
2763
|
+
return this.loadFromState(after, commandOptions);
|
|
2524
2764
|
}
|
|
2525
2765
|
executedOnce = true;
|
|
2526
2766
|
return undefined;
|
|
2527
2767
|
},
|
|
2528
|
-
() => this.loadFromState(before)
|
|
2768
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2529
2769
|
);
|
|
2530
2770
|
|
|
2531
2771
|
this.historyManager.execute(command);
|
|
@@ -2557,8 +2797,8 @@ function ensureFabric() {
|
|
|
2557
2797
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2558
2798
|
|
|
2559
2799
|
const command = new Command(
|
|
2560
|
-
() => this.loadFromState(after),
|
|
2561
|
-
() => this.loadFromState(before)
|
|
2800
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2801
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2562
2802
|
);
|
|
2563
2803
|
this.historyManager.push(command);
|
|
2564
2804
|
this._lastSnapshot = after;
|
|
@@ -2572,8 +2812,17 @@ function ensureFabric() {
|
|
|
2572
2812
|
* @public
|
|
2573
2813
|
*/
|
|
2574
2814
|
undo() {
|
|
2575
|
-
|
|
2815
|
+
try {
|
|
2816
|
+
this._assertIdleForOperation('undo');
|
|
2817
|
+
} catch (error) {
|
|
2818
|
+
return Promise.reject(error);
|
|
2819
|
+
}
|
|
2820
|
+
const operationToken = this._beginBusyOperation('undo');
|
|
2821
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken))
|
|
2576
2822
|
.then(() => { this._updateUI(); })
|
|
2823
|
+
.finally(() => {
|
|
2824
|
+
this._endBusyOperation(operationToken);
|
|
2825
|
+
})
|
|
2577
2826
|
.catch(error => {
|
|
2578
2827
|
this._reportError('undo failed', error);
|
|
2579
2828
|
throw error;
|
|
@@ -2587,8 +2836,17 @@ function ensureFabric() {
|
|
|
2587
2836
|
* @public
|
|
2588
2837
|
*/
|
|
2589
2838
|
redo() {
|
|
2590
|
-
|
|
2839
|
+
try {
|
|
2840
|
+
this._assertIdleForOperation('redo');
|
|
2841
|
+
} catch (error) {
|
|
2842
|
+
return Promise.reject(error);
|
|
2843
|
+
}
|
|
2844
|
+
const operationToken = this._beginBusyOperation('redo');
|
|
2845
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken))
|
|
2591
2846
|
.then(() => { this._updateUI(); })
|
|
2847
|
+
.finally(() => {
|
|
2848
|
+
this._endBusyOperation(operationToken);
|
|
2849
|
+
})
|
|
2592
2850
|
.catch(error => {
|
|
2593
2851
|
this._reportError('redo failed', error);
|
|
2594
2852
|
throw error;
|
|
@@ -2712,21 +2970,49 @@ function ensureFabric() {
|
|
|
2712
2970
|
return value != null ? value : fallback;
|
|
2713
2971
|
};
|
|
2714
2972
|
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2973
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
2974
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
2975
|
+
return null;
|
|
2976
|
+
};
|
|
2977
|
+
|
|
2978
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
2979
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
2980
|
+
const numericValue = Number(resolvedValue);
|
|
2981
|
+
if (!Number.isFinite(numericValue)) {
|
|
2982
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
2983
|
+
}
|
|
2984
|
+
if (constraints.positive && numericValue <= 0) {
|
|
2985
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
2986
|
+
}
|
|
2987
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
2988
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
2989
|
+
}
|
|
2990
|
+
return numericValue;
|
|
2991
|
+
};
|
|
2992
|
+
|
|
2993
|
+
try {
|
|
2994
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, 'width', 'gap', { nonNegative: true });
|
|
2995
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, 'width', 'width', { positive: true });
|
|
2996
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, 'height', 'height', { positive: true });
|
|
2997
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, 'width', 'angle');
|
|
2998
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, 'width', 'alpha')));
|
|
2999
|
+
|
|
3000
|
+
if (maskConfig.left === undefined && this._lastMask) {
|
|
3001
|
+
const previousMask = this._lastMask;
|
|
3002
|
+
if (typeof previousMask.setCoords === 'function') previousMask.setCoords();
|
|
3003
|
+
const previousBounds = typeof previousMask.getBoundingRect === 'function'
|
|
3004
|
+
? previousMask.getBoundingRect(true, true)
|
|
3005
|
+
: { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
3006
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
3007
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
3008
|
+
} else {
|
|
3009
|
+
left = resolveNumber(maskConfig.left, firstOffset, 'width', 'left');
|
|
3010
|
+
top = resolveNumber(maskConfig.top, firstOffset, 'height', 'top');
|
|
3011
|
+
}
|
|
3012
|
+
} catch (error) {
|
|
3013
|
+
return rejectInvalidMask('invalid numeric configuration', error);
|
|
2726
3014
|
}
|
|
2727
3015
|
|
|
2728
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, 'width');
|
|
2729
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, 'height');
|
|
2730
3016
|
maskConfig.left = left;
|
|
2731
3017
|
maskConfig.top = top;
|
|
2732
3018
|
|
|
@@ -2736,9 +3022,14 @@ function ensureFabric() {
|
|
|
2736
3022
|
} else {
|
|
2737
3023
|
switch (shapeType) {
|
|
2738
3024
|
case 'circle':
|
|
3025
|
+
try {
|
|
3026
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, 'min', 'radius', { positive: true });
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
return rejectInvalidMask('invalid circle radius', error);
|
|
3029
|
+
}
|
|
2739
3030
|
mask = new fabric.Circle({
|
|
2740
3031
|
left, top,
|
|
2741
|
-
radius:
|
|
3032
|
+
radius: maskConfig.radius,
|
|
2742
3033
|
fill: maskConfig.color,
|
|
2743
3034
|
opacity: maskConfig.alpha,
|
|
2744
3035
|
angle: maskConfig.angle,
|
|
@@ -2746,10 +3037,16 @@ function ensureFabric() {
|
|
|
2746
3037
|
});
|
|
2747
3038
|
break;
|
|
2748
3039
|
case 'ellipse':
|
|
3040
|
+
try {
|
|
3041
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, 'width', 'rx', { positive: true });
|
|
3042
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, 'height', 'ry', { positive: true });
|
|
3043
|
+
} catch (error) {
|
|
3044
|
+
return rejectInvalidMask('invalid ellipse radius', error);
|
|
3045
|
+
}
|
|
2749
3046
|
mask = new fabric.Ellipse({
|
|
2750
3047
|
left, top,
|
|
2751
|
-
rx:
|
|
2752
|
-
ry:
|
|
3048
|
+
rx: maskConfig.rx,
|
|
3049
|
+
ry: maskConfig.ry,
|
|
2753
3050
|
fill: maskConfig.color,
|
|
2754
3051
|
opacity: maskConfig.alpha,
|
|
2755
3052
|
angle: maskConfig.angle,
|
|
@@ -2758,11 +3055,20 @@ function ensureFabric() {
|
|
|
2758
3055
|
break;
|
|
2759
3056
|
case 'polygon': {
|
|
2760
3057
|
let polygonPoints = maskConfig.points || [];
|
|
2761
|
-
if (Array.isArray(polygonPoints)
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
3058
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
3059
|
+
return rejectInvalidMask('polygon masks require at least three points');
|
|
3060
|
+
}
|
|
3061
|
+
try {
|
|
3062
|
+
polygonPoints = polygonPoints.map(point => {
|
|
3063
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
3064
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
3065
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
3066
|
+
throw new Error('polygon point coordinates must be finite numbers');
|
|
3067
|
+
}
|
|
3068
|
+
return { x, y };
|
|
3069
|
+
});
|
|
3070
|
+
} catch (error) {
|
|
3071
|
+
return rejectInvalidMask('invalid polygon points', error);
|
|
2766
3072
|
}
|
|
2767
3073
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2768
3074
|
left, top,
|
|
@@ -2775,10 +3081,16 @@ function ensureFabric() {
|
|
|
2775
3081
|
}
|
|
2776
3082
|
case 'rect':
|
|
2777
3083
|
default:
|
|
3084
|
+
try {
|
|
3085
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, 'width', 'rx', { nonNegative: true });
|
|
3086
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, 'height', 'ry', { nonNegative: true });
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
return rejectInvalidMask('invalid rectangle corner radius', error);
|
|
3089
|
+
}
|
|
2778
3090
|
mask = new fabric.Rect({
|
|
2779
3091
|
left, top,
|
|
2780
|
-
width:
|
|
2781
|
-
height:
|
|
3092
|
+
width: maskConfig.width,
|
|
3093
|
+
height: maskConfig.height,
|
|
2782
3094
|
fill: maskConfig.color,
|
|
2783
3095
|
opacity: maskConfig.alpha,
|
|
2784
3096
|
angle: maskConfig.angle,
|
|
@@ -2819,12 +3131,12 @@ function ensureFabric() {
|
|
|
2819
3131
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2820
3132
|
});
|
|
2821
3133
|
this._rebindMaskEvents(mask);
|
|
2822
|
-
this.
|
|
3134
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2823
3135
|
|
|
2824
3136
|
// Store placement values so the next mask can be positioned beside this one.
|
|
2825
3137
|
this._lastMaskInitialLeft = left;
|
|
2826
3138
|
this._lastMaskInitialTop = top;
|
|
2827
|
-
this._lastMaskInitialWidth =
|
|
3139
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2828
3140
|
|
|
2829
3141
|
const maskId = ++this.maskCounter;
|
|
2830
3142
|
mask.set({
|
|
@@ -3274,6 +3586,7 @@ function ensureFabric() {
|
|
|
3274
3586
|
this._assertIdleForOperation('mergeMasks');
|
|
3275
3587
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3276
3588
|
if (!masks.length) return;
|
|
3589
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
3277
3590
|
const beforeJson = this._serializeCanvasState();
|
|
3278
3591
|
const operationToken = this._beginBusyOperation('mergeMasks');
|
|
3279
3592
|
|
|
@@ -3294,12 +3607,13 @@ function ensureFabric() {
|
|
|
3294
3607
|
preserveScroll: true,
|
|
3295
3608
|
resetMaskCounter: false
|
|
3296
3609
|
}));
|
|
3610
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
3297
3611
|
const afterJson = this._serializeCanvasState();
|
|
3298
3612
|
this._pushStateTransition(beforeJson, afterJson);
|
|
3299
3613
|
} catch (error) {
|
|
3300
3614
|
this._reportError('merge error', error);
|
|
3301
3615
|
try {
|
|
3302
|
-
await this.loadFromState(beforeJson);
|
|
3616
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
3303
3617
|
} catch (restoreError) {
|
|
3304
3618
|
this._reportError('mergeMasks rollback failed', restoreError);
|
|
3305
3619
|
}
|
|
@@ -3361,13 +3675,19 @@ function ensureFabric() {
|
|
|
3361
3675
|
*/
|
|
3362
3676
|
async exportImageBase64(options = {}) {
|
|
3363
3677
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
3678
|
+
options = options || {};
|
|
3364
3679
|
this._assertIdleForOperation('exportImageBase64', options);
|
|
3680
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3681
|
+
const operationToken = isNestedOperation
|
|
3682
|
+
? this._getInternalOperationToken(options)
|
|
3683
|
+
: this._beginBusyOperation('exportImageBase64');
|
|
3365
3684
|
const exportImageArea = typeof options.exportImageArea === 'boolean' ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
3366
3685
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
3367
3686
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
3368
3687
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
3369
3688
|
|
|
3370
|
-
|
|
3689
|
+
try {
|
|
3690
|
+
if (!exportImageArea) {
|
|
3371
3691
|
const masks = this.canvas.getObjects().filter(object => object.maskId || object.maskLabel);
|
|
3372
3692
|
const editableMasks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3373
3693
|
const maskVisibilityBackups = masks.map(mask => ({ object: mask, visible: mask.visible }));
|
|
@@ -3399,15 +3719,15 @@ function ensureFabric() {
|
|
|
3399
3719
|
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3400
3720
|
this.canvas.renderAll();
|
|
3401
3721
|
}
|
|
3402
|
-
|
|
3722
|
+
}
|
|
3403
3723
|
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3724
|
+
// Render masks as export shapes without mutating their editable styles.
|
|
3725
|
+
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
3726
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3727
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3728
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
3409
3729
|
|
|
3410
|
-
|
|
3730
|
+
try {
|
|
3411
3731
|
// Labels are UI overlays and should not be part of the flattened export.
|
|
3412
3732
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
3413
3733
|
this.canvas.discardActiveObject();
|
|
@@ -3438,6 +3758,9 @@ function ensureFabric() {
|
|
|
3438
3758
|
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
3439
3759
|
this.canvas.renderAll();
|
|
3440
3760
|
}
|
|
3761
|
+
} finally {
|
|
3762
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3763
|
+
}
|
|
3441
3764
|
}
|
|
3442
3765
|
|
|
3443
3766
|
/**
|
|
@@ -3471,7 +3794,12 @@ function ensureFabric() {
|
|
|
3471
3794
|
*/
|
|
3472
3795
|
async exportImageFile(options = {}) {
|
|
3473
3796
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
3474
|
-
|
|
3797
|
+
options = options || {};
|
|
3798
|
+
this._assertIdleForOperation('exportImageFile', options);
|
|
3799
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3800
|
+
const operationToken = isNestedOperation
|
|
3801
|
+
? this._getInternalOperationToken(options)
|
|
3802
|
+
: this._beginBusyOperation('exportImageFile');
|
|
3475
3803
|
const {
|
|
3476
3804
|
mergeMask = true,
|
|
3477
3805
|
fileType = 'jpeg',
|
|
@@ -3483,52 +3811,56 @@ function ensureFabric() {
|
|
|
3483
3811
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3484
3812
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
3485
3813
|
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3814
|
+
try {
|
|
3815
|
+
// Generate the data URL in the requested export mode.
|
|
3816
|
+
let imageBase64;
|
|
3817
|
+
if (mergeMask) {
|
|
3818
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3819
|
+
exportImageArea: true,
|
|
3820
|
+
multiplier,
|
|
3821
|
+
quality: normalizedQuality,
|
|
3822
|
+
fileType: safeFileType
|
|
3823
|
+
}));
|
|
3824
|
+
} else {
|
|
3825
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3826
|
+
exportImageArea: false,
|
|
3827
|
+
multiplier,
|
|
3828
|
+
quality: normalizedQuality,
|
|
3829
|
+
fileType: safeFileType
|
|
3830
|
+
}));
|
|
3831
|
+
}
|
|
3503
3832
|
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3833
|
+
// Convert to the required image format
|
|
3834
|
+
let imageDataUrl = imageBase64;
|
|
3835
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3836
|
+
// Redraw the exported data URL when the browser returned a different image format.
|
|
3837
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3838
|
+
const imageElement = new window.Image();
|
|
3839
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3840
|
+
imageElement.onload = () => {
|
|
3841
|
+
try {
|
|
3842
|
+
const offscreenCanvas = document.createElement('canvas');
|
|
3843
|
+
offscreenCanvas.width = imageElement.width;
|
|
3844
|
+
offscreenCanvas.height = imageElement.height;
|
|
3845
|
+
const context = offscreenCanvas.getContext('2d');
|
|
3846
|
+
if (!context) throw new Error('Unable to create 2D canvas context for export conversion');
|
|
3847
|
+
context.drawImage(imageElement, 0, 0);
|
|
3848
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3849
|
+
resolve(convertedDataUrl);
|
|
3850
|
+
} catch (error) { reject(error); }
|
|
3851
|
+
};
|
|
3852
|
+
imageElement.onerror = reject;
|
|
3853
|
+
imageElement.src = imageBase64;
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3527
3856
|
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3857
|
+
// Convert the final data URL to a File with the requested MIME type.
|
|
3858
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3859
|
+
const mime = `image/${safeFileType}`;
|
|
3860
|
+
return new File([bytes], fileName, { type: mime });
|
|
3861
|
+
} finally {
|
|
3862
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
3863
|
+
}
|
|
3532
3864
|
}
|
|
3533
3865
|
|
|
3534
3866
|
_clearMaskPlacementMemory() {
|
|
@@ -3538,7 +3870,7 @@ function ensureFabric() {
|
|
|
3538
3870
|
this._lastMaskInitialWidth = null;
|
|
3539
3871
|
}
|
|
3540
3872
|
|
|
3541
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
3873
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
3542
3874
|
this._reportError(message, error);
|
|
3543
3875
|
|
|
3544
3876
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
@@ -3551,7 +3883,7 @@ function ensureFabric() {
|
|
|
3551
3883
|
|
|
3552
3884
|
if (beforeJson) {
|
|
3553
3885
|
try {
|
|
3554
|
-
await this.loadFromState(beforeJson);
|
|
3886
|
+
await this.loadFromState(beforeJson, options);
|
|
3555
3887
|
} catch (restoreError) {
|
|
3556
3888
|
this._reportError('applyCrop: rollback failed', restoreError);
|
|
3557
3889
|
}
|
|
@@ -3596,6 +3928,18 @@ function ensureFabric() {
|
|
|
3596
3928
|
this._cropHandlers = [];
|
|
3597
3929
|
}
|
|
3598
3930
|
|
|
3931
|
+
_getCropRectContentBounds(cropRect) {
|
|
3932
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
3933
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
3934
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
3935
|
+
return {
|
|
3936
|
+
left: Number(cropRect.left) || 0,
|
|
3937
|
+
top: Number(cropRect.top) || 0,
|
|
3938
|
+
width,
|
|
3939
|
+
height
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3599
3943
|
/**
|
|
3600
3944
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
3601
3945
|
*
|
|
@@ -3626,14 +3970,19 @@ function ensureFabric() {
|
|
|
3626
3970
|
const padding = (this.options.crop && this.options.crop.padding) ? this.options.crop.padding : 10;
|
|
3627
3971
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
3628
3972
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
3629
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
3630
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
3973
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
3974
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
3631
3975
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
3632
3976
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
3633
3977
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3634
3978
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3635
3979
|
const width = minCropWidth;
|
|
3636
3980
|
const height = minCropHeight;
|
|
3981
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
3982
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
3983
|
+
this._cropRotationWarningEmitted = true;
|
|
3984
|
+
this._reportWarning('crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported');
|
|
3985
|
+
}
|
|
3637
3986
|
|
|
3638
3987
|
// Visual style for the temporary crop rectangle.
|
|
3639
3988
|
const cropRect = new fabric.Rect({
|
|
@@ -3645,8 +3994,8 @@ function ensureFabric() {
|
|
|
3645
3994
|
strokeWidth: 1,
|
|
3646
3995
|
strokeUniform: true,
|
|
3647
3996
|
selectable: true,
|
|
3648
|
-
hasRotatingPoint:
|
|
3649
|
-
lockRotation:
|
|
3997
|
+
hasRotatingPoint: false,
|
|
3998
|
+
lockRotation: true,
|
|
3650
3999
|
cornerSize: 8,
|
|
3651
4000
|
objectCaching: false,
|
|
3652
4001
|
originX: 'left',
|
|
@@ -3689,7 +4038,7 @@ function ensureFabric() {
|
|
|
3689
4038
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3690
4039
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3691
4040
|
cropRect.setCoords();
|
|
3692
|
-
const cropBounds =
|
|
4041
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3693
4042
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3694
4043
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3695
4044
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3768,10 +4117,13 @@ function ensureFabric() {
|
|
|
3768
4117
|
async applyCrop() {
|
|
3769
4118
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3770
4119
|
this._assertIdleForOperation('applyCrop');
|
|
4120
|
+
const operationToken = this._beginBusyOperation('applyCrop');
|
|
4121
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3771
4122
|
|
|
4123
|
+
try {
|
|
3772
4124
|
// Fabric does not update control coordinates automatically after programmatic transforms.
|
|
3773
4125
|
this._cropRect.setCoords();
|
|
3774
|
-
const rectBounds = this._cropRect
|
|
4126
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3775
4127
|
|
|
3776
4128
|
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3777
4129
|
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
@@ -3817,7 +4169,7 @@ function ensureFabric() {
|
|
|
3817
4169
|
this.canvas.renderAll();
|
|
3818
4170
|
}
|
|
3819
4171
|
} catch (error) {
|
|
3820
|
-
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error);
|
|
4172
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to prepare masks', error, internalOptions);
|
|
3821
4173
|
return;
|
|
3822
4174
|
}
|
|
3823
4175
|
|
|
@@ -3838,13 +4190,13 @@ function ensureFabric() {
|
|
|
3838
4190
|
format: 'jpeg'
|
|
3839
4191
|
});
|
|
3840
4192
|
} catch (error) {
|
|
3841
|
-
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to create cropped image', error);
|
|
4193
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: failed to create cropped image', error, internalOptions);
|
|
3842
4194
|
return;
|
|
3843
4195
|
}
|
|
3844
4196
|
|
|
3845
4197
|
// Load the cropped image as the new base image.
|
|
3846
4198
|
try {
|
|
3847
|
-
await this.loadImage(croppedBase64, { resetMaskCounter: false });
|
|
4199
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3848
4200
|
if (preservedMasks.length) {
|
|
3849
4201
|
preservedMasks.forEach(mask => {
|
|
3850
4202
|
this._rebindMaskEvents(mask);
|
|
@@ -3857,7 +4209,7 @@ function ensureFabric() {
|
|
|
3857
4209
|
this.canvas.renderAll();
|
|
3858
4210
|
}
|
|
3859
4211
|
} catch (error) {
|
|
3860
|
-
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: loadImage(croppedBase64) failed', error);
|
|
4212
|
+
await this._restoreStateAfterCropFailure(beforeJson, 'applyCrop: loadImage(croppedBase64) failed', error, internalOptions);
|
|
3861
4213
|
return;
|
|
3862
4214
|
}
|
|
3863
4215
|
|
|
@@ -3879,6 +4231,9 @@ function ensureFabric() {
|
|
|
3879
4231
|
// Refresh UI state after crop completion.
|
|
3880
4232
|
this._updateUI();
|
|
3881
4233
|
this.canvas.renderAll();
|
|
4234
|
+
} finally {
|
|
4235
|
+
this._endBusyOperation(operationToken);
|
|
4236
|
+
}
|
|
3882
4237
|
}
|
|
3883
4238
|
|
|
3884
4239
|
|
|
@@ -4168,6 +4523,7 @@ function ensureFabric() {
|
|
|
4168
4523
|
|
|
4169
4524
|
/**
|
|
4170
4525
|
* @callback HistoryTaskCallback
|
|
4526
|
+
* @param {Object} [options] - Internal operation options passed by the editor.
|
|
4171
4527
|
* @returns {void|Promise<void>} Result of a history operation.
|
|
4172
4528
|
*/
|
|
4173
4529
|
|
|
@@ -4389,11 +4745,11 @@ function ensureFabric() {
|
|
|
4389
4745
|
*
|
|
4390
4746
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
4391
4747
|
*/
|
|
4392
|
-
undo() {
|
|
4748
|
+
undo(options = {}) {
|
|
4393
4749
|
return this.enqueue(async () => {
|
|
4394
4750
|
if (this.currentIndex >= 0) {
|
|
4395
4751
|
const index = this.currentIndex;
|
|
4396
|
-
await this.history[index].undo();
|
|
4752
|
+
await this.history[index].undo(options);
|
|
4397
4753
|
this.currentIndex = index - 1;
|
|
4398
4754
|
}
|
|
4399
4755
|
});
|
|
@@ -4404,11 +4760,11 @@ function ensureFabric() {
|
|
|
4404
4760
|
*
|
|
4405
4761
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
4406
4762
|
*/
|
|
4407
|
-
redo() {
|
|
4763
|
+
redo(options = {}) {
|
|
4408
4764
|
return this.enqueue(async () => {
|
|
4409
4765
|
if (this.currentIndex < this.history.length - 1) {
|
|
4410
4766
|
const index = this.currentIndex + 1;
|
|
4411
|
-
await this.history[index].execute();
|
|
4767
|
+
await this.history[index].execute(options);
|
|
4412
4768
|
this.currentIndex = index;
|
|
4413
4769
|
}
|
|
4414
4770
|
});
|