@bensitu/image-editor 1.4.0 → 1.4.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/dist/image-editor.esm.js +319 -126
- package/dist/image-editor.esm.js.map +2 -2
- 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 +319 -126
- package/dist/image-editor.esm.mjs.map +2 -2
- package/dist/image-editor.js +319 -126
- package/dist/image-editor.js.map +2 -2
- package/dist/image-editor.min.js +2 -2
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +23 -12
- package/package.json +2 -4
- package/src/image-editor.js +343 -125
package/src/image-editor.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.4.
|
|
4
|
+
* @version 1.4.1
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
let fabric = null;
|
|
11
|
+
const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Returns the ambient global scope used to discover a globally loaded Fabric.js namespace.
|
|
@@ -252,12 +253,16 @@ function ensureFabric() {
|
|
|
252
253
|
this.currentRotation = 0;
|
|
253
254
|
this.maskCounter = 0;
|
|
254
255
|
this.isAnimating = false;
|
|
256
|
+
this._isLoading = false;
|
|
257
|
+
this._activeOperationName = null;
|
|
258
|
+
this._activeOperationToken = null;
|
|
255
259
|
this.elements = {};
|
|
256
260
|
this.isImageLoadedToCanvas = false;
|
|
257
261
|
this.maxHistorySize = 50;
|
|
258
262
|
|
|
259
263
|
this._handlersByElementKey = {};
|
|
260
264
|
this._elementCache = {};
|
|
265
|
+
this._elementOriginalPointerEvents = new Map();
|
|
261
266
|
|
|
262
267
|
this._lastMask = null;
|
|
263
268
|
this._lastMaskInitialLeft = null;
|
|
@@ -357,6 +362,10 @@ function ensureFabric() {
|
|
|
357
362
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
358
363
|
this._visibilityStateByElement = new WeakMap();
|
|
359
364
|
this._activeAnimationRejectors = new Set();
|
|
365
|
+
this._isLoading = false;
|
|
366
|
+
this._activeOperationName = null;
|
|
367
|
+
this._activeOperationToken = null;
|
|
368
|
+
this._elementOriginalPointerEvents = new Map();
|
|
360
369
|
this._containerOriginalOverflow = null;
|
|
361
370
|
this._lastContainerViewportSize = null;
|
|
362
371
|
this._canvasElementOriginalStyle = null;
|
|
@@ -589,6 +598,13 @@ function ensureFabric() {
|
|
|
589
598
|
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
590
599
|
}
|
|
591
600
|
|
|
601
|
+
_restoreContainerOverflowSnapshot(snapshot) {
|
|
602
|
+
if (!this.containerElement || !this.containerElement.style || !snapshot) return;
|
|
603
|
+
this.containerElement.style.overflow = snapshot.overflow || '';
|
|
604
|
+
this.containerElement.style.overflowX = snapshot.overflowX || '';
|
|
605
|
+
this.containerElement.style.overflowY = snapshot.overflowY || '';
|
|
606
|
+
}
|
|
607
|
+
|
|
592
608
|
/**
|
|
593
609
|
* DOM / UI bindings
|
|
594
610
|
* @private
|
|
@@ -740,8 +756,10 @@ function ensureFabric() {
|
|
|
740
756
|
if (!this._fabricLoaded) return;
|
|
741
757
|
if (!this.canvas || this._disposed) return;
|
|
742
758
|
if (!imageBase64 || typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) return;
|
|
743
|
-
this._assertIdleForOperation('loadImage');
|
|
759
|
+
this._assertIdleForOperation('loadImage', options);
|
|
744
760
|
|
|
761
|
+
this._isLoading = true;
|
|
762
|
+
this._updateUI();
|
|
745
763
|
this._warnOnImageLayoutOptionConflict();
|
|
746
764
|
const transaction = this._captureLoadImageTransaction();
|
|
747
765
|
|
|
@@ -765,7 +783,7 @@ function ensureFabric() {
|
|
|
765
783
|
imageElement,
|
|
766
784
|
targetWidth,
|
|
767
785
|
targetHeight,
|
|
768
|
-
this.options.downsampleQuality,
|
|
786
|
+
this._normalizeQuality(this.options.downsampleQuality),
|
|
769
787
|
imageBase64
|
|
770
788
|
);
|
|
771
789
|
}
|
|
@@ -844,6 +862,9 @@ function ensureFabric() {
|
|
|
844
862
|
} catch (error) {
|
|
845
863
|
await this._rollbackLoadImageTransaction(transaction);
|
|
846
864
|
throw error;
|
|
865
|
+
} finally {
|
|
866
|
+
this._isLoading = false;
|
|
867
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
847
868
|
}
|
|
848
869
|
}
|
|
849
870
|
|
|
@@ -961,9 +982,14 @@ function ensureFabric() {
|
|
|
961
982
|
|
|
962
983
|
async _rollbackLoadImageTransaction(transaction) {
|
|
963
984
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
985
|
+
let didRestoreCanvasState = false;
|
|
964
986
|
try {
|
|
965
|
-
if (transaction.canvasState)
|
|
987
|
+
if (transaction.canvasState) {
|
|
988
|
+
await this.loadFromState(transaction.canvasState);
|
|
989
|
+
didRestoreCanvasState = true;
|
|
990
|
+
}
|
|
966
991
|
} catch (error) {
|
|
992
|
+
this._lastMask = null;
|
|
967
993
|
this._reportError('loadImage rollback failed', error);
|
|
968
994
|
}
|
|
969
995
|
|
|
@@ -973,16 +999,20 @@ function ensureFabric() {
|
|
|
973
999
|
this.maskCounter = transaction.maskCounter;
|
|
974
1000
|
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
975
1001
|
this._lastSnapshot = transaction.lastSnapshot;
|
|
1002
|
+
if (didRestoreCanvasState) {
|
|
1003
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
1004
|
+
} else {
|
|
1005
|
+
this._lastMask = null;
|
|
1006
|
+
}
|
|
976
1007
|
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
977
1008
|
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
978
1009
|
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
979
|
-
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
980
1010
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
981
1011
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
982
1012
|
if (this.containerElement) {
|
|
983
1013
|
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
984
1014
|
this.containerElement.scrollTop = transaction.scrollTop;
|
|
985
|
-
this.
|
|
1015
|
+
this._restoreContainerOverflowSnapshot(transaction.containerOverflow);
|
|
986
1016
|
}
|
|
987
1017
|
this._updateInputs();
|
|
988
1018
|
this._updateMaskList();
|
|
@@ -990,6 +1020,22 @@ function ensureFabric() {
|
|
|
990
1020
|
if (this.canvas) this.canvas.renderAll();
|
|
991
1021
|
}
|
|
992
1022
|
|
|
1023
|
+
_restoreLastMaskReference(previousLastMask) {
|
|
1024
|
+
if (!this.canvas) {
|
|
1025
|
+
this._lastMask = null;
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
1030
|
+
const previousMaskId = previousLastMask && previousLastMask.maskId;
|
|
1031
|
+
this._lastMask = masks.find(mask => mask.maskId === previousMaskId) || masks[masks.length - 1] || null;
|
|
1032
|
+
if (!this._lastMask) {
|
|
1033
|
+
this._lastMaskInitialLeft = null;
|
|
1034
|
+
this._lastMaskInitialTop = null;
|
|
1035
|
+
this._lastMaskInitialWidth = null;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
993
1039
|
/**
|
|
994
1040
|
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
995
1041
|
*
|
|
@@ -1315,7 +1361,11 @@ function ensureFabric() {
|
|
|
1315
1361
|
maskStyleBackups.push(backup);
|
|
1316
1362
|
mask.set(stylePatch);
|
|
1317
1363
|
});
|
|
1318
|
-
|
|
1364
|
+
const result = callback();
|
|
1365
|
+
if (result && typeof result.then === 'function') {
|
|
1366
|
+
throw new Error('_withNormalizedMaskStyles callback must be synchronous');
|
|
1367
|
+
}
|
|
1368
|
+
return result;
|
|
1319
1369
|
} finally {
|
|
1320
1370
|
maskStyleBackups.forEach(backup => {
|
|
1321
1371
|
try {
|
|
@@ -1387,9 +1437,15 @@ function ensureFabric() {
|
|
|
1387
1437
|
* @returns {number} A finite quality value between 0 and 1.
|
|
1388
1438
|
* @private
|
|
1389
1439
|
*/
|
|
1390
|
-
_normalizeQuality(quality) {
|
|
1440
|
+
_normalizeQuality(quality, fallback = undefined) {
|
|
1441
|
+
const fallbackQuality = fallback == null ? this.options.downsampleQuality : fallback;
|
|
1442
|
+
const numericFallback = fallbackQuality == null ? NaN : Number(fallbackQuality);
|
|
1443
|
+
const safeFallback = Number.isFinite(numericFallback)
|
|
1444
|
+
? Math.max(0, Math.min(1, numericFallback))
|
|
1445
|
+
: 0.92;
|
|
1446
|
+
if (quality == null) return safeFallback;
|
|
1391
1447
|
const numericQuality = Number(quality);
|
|
1392
|
-
if (!Number.isFinite(numericQuality)) return
|
|
1448
|
+
if (!Number.isFinite(numericQuality)) return safeFallback;
|
|
1393
1449
|
return Math.max(0, Math.min(1, numericQuality));
|
|
1394
1450
|
}
|
|
1395
1451
|
|
|
@@ -1444,62 +1500,70 @@ function ensureFabric() {
|
|
|
1444
1500
|
};
|
|
1445
1501
|
}
|
|
1446
1502
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
+
_hasFractionalCanvasEdge(value) {
|
|
1504
|
+
const numericValue = Number(value);
|
|
1505
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
1506
|
+
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
_getPartialExportEdges(bounds) {
|
|
1510
|
+
if (!bounds) return null;
|
|
1511
|
+
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
1512
|
+
const isAxisAligned = angle < 0.01 || Math.abs(angle - 90) < 0.01;
|
|
1513
|
+
if (!isAxisAligned) return null;
|
|
1514
|
+
|
|
1515
|
+
return {
|
|
1516
|
+
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1517
|
+
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1518
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1519
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
1524
|
+
if (!edges || !Object.values(edges).some(Boolean)) return dataUrl;
|
|
1525
|
+
|
|
1526
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1527
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1528
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1529
|
+
const offscreenCanvas = document.createElement('canvas');
|
|
1530
|
+
offscreenCanvas.width = width;
|
|
1531
|
+
offscreenCanvas.height = height;
|
|
1532
|
+
const context = offscreenCanvas.getContext('2d');
|
|
1533
|
+
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1534
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1535
|
+
|
|
1536
|
+
const imageData = context.getImageData(0, 0, width, height);
|
|
1537
|
+
const pixels = imageData.data;
|
|
1538
|
+
const sealPixel = (x, y, fallbackX, fallbackY) => {
|
|
1539
|
+
const index = (y * width + x) * 4;
|
|
1540
|
+
const fallbackIndex = (fallbackY * width + fallbackX) * 4;
|
|
1541
|
+
if (pixels[index + 3] === 0 && pixels[fallbackIndex + 3] > 0) {
|
|
1542
|
+
pixels[index] = pixels[fallbackIndex];
|
|
1543
|
+
pixels[index + 1] = pixels[fallbackIndex + 1];
|
|
1544
|
+
pixels[index + 2] = pixels[fallbackIndex + 2];
|
|
1545
|
+
pixels[index + 3] = pixels[fallbackIndex + 3];
|
|
1546
|
+
}
|
|
1547
|
+
if (pixels[index + 3] > 0 && pixels[index + 3] < 255) {
|
|
1548
|
+
pixels[index + 3] = 255;
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
if (edges.left && width > 1) {
|
|
1553
|
+
for (let y = 0; y < height; y += 1) sealPixel(0, y, 1, y);
|
|
1554
|
+
}
|
|
1555
|
+
if (edges.right && width > 1) {
|
|
1556
|
+
for (let y = 0; y < height; y += 1) sealPixel(width - 1, y, width - 2, y);
|
|
1557
|
+
}
|
|
1558
|
+
if (edges.top && height > 1) {
|
|
1559
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, 0, x, 1);
|
|
1560
|
+
}
|
|
1561
|
+
if (edges.bottom && height > 1) {
|
|
1562
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, height - 1, x, height - 2);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
context.putImageData(imageData, 0, 0);
|
|
1566
|
+
return offscreenCanvas.toDataURL('image/png');
|
|
1503
1567
|
}
|
|
1504
1568
|
|
|
1505
1569
|
/**
|
|
@@ -1513,13 +1577,16 @@ function ensureFabric() {
|
|
|
1513
1577
|
* @param {number} [region.multiplier=1] - Export multiplier.
|
|
1514
1578
|
* @param {number} [region.quality=0.92] - Output image quality for lossy formats.
|
|
1515
1579
|
* @param {'jpeg'|'png'|'webp'} [region.format='jpeg'] - Output image format.
|
|
1580
|
+
* @param {Object|null} [region.sealPartialEdges=null] - Fractional canvas edges whose alpha should be sealed.
|
|
1516
1581
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1517
1582
|
* @private
|
|
1518
1583
|
*/
|
|
1519
|
-
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg' }) {
|
|
1584
|
+
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = 'jpeg', sealPartialEdges = null }) {
|
|
1520
1585
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1521
|
-
|
|
1522
|
-
|
|
1586
|
+
const safeFormat = this._normalizeImageFormat(format);
|
|
1587
|
+
const exportFormat = safeFormat === 'jpeg' ? 'png' : safeFormat;
|
|
1588
|
+
let regionDataUrl = this.canvas.toDataURL({
|
|
1589
|
+
format: exportFormat,
|
|
1523
1590
|
quality,
|
|
1524
1591
|
multiplier: safeMultiplier,
|
|
1525
1592
|
left: sourceX,
|
|
@@ -1527,6 +1594,32 @@ function ensureFabric() {
|
|
|
1527
1594
|
width: sourceWidth,
|
|
1528
1595
|
height: sourceHeight
|
|
1529
1596
|
});
|
|
1597
|
+
|
|
1598
|
+
regionDataUrl = await this._sealPartialTransparentEdges(regionDataUrl, sealPartialEdges);
|
|
1599
|
+
if (safeFormat !== 'jpeg') return regionDataUrl;
|
|
1600
|
+
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1604
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1605
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1606
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1607
|
+
const offscreenCanvas = document.createElement('canvas');
|
|
1608
|
+
offscreenCanvas.width = width;
|
|
1609
|
+
offscreenCanvas.height = height;
|
|
1610
|
+
const context = offscreenCanvas.getContext('2d');
|
|
1611
|
+
if (!context) throw new Error('2D canvas context is unavailable');
|
|
1612
|
+
context.fillStyle = this._getJpegBackgroundColor();
|
|
1613
|
+
context.fillRect(0, 0, width, height);
|
|
1614
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1615
|
+
return offscreenCanvas.toDataURL('image/jpeg', this._normalizeQuality(quality));
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
_getJpegBackgroundColor() {
|
|
1619
|
+
const backgroundColor = String(this.options.backgroundColor || '').trim();
|
|
1620
|
+
if (!backgroundColor || backgroundColor === 'transparent') return '#ffffff';
|
|
1621
|
+
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return '#ffffff';
|
|
1622
|
+
return backgroundColor;
|
|
1530
1623
|
}
|
|
1531
1624
|
|
|
1532
1625
|
/**
|
|
@@ -1698,19 +1791,80 @@ function ensureFabric() {
|
|
|
1698
1791
|
* @public
|
|
1699
1792
|
*/
|
|
1700
1793
|
scaleImage(factor, options = {}) {
|
|
1701
|
-
|
|
1794
|
+
try {
|
|
1795
|
+
this._assertCanQueueAnimation('scaleImage', options);
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
return Promise.reject(error);
|
|
1798
|
+
}
|
|
1799
|
+
return this.animationQueue.add(() => this._scaleImageImpl(factor, options))
|
|
1800
|
+
.finally(() => {
|
|
1801
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
_getInternalOperationToken(options) {
|
|
1806
|
+
return options && options[INTERNAL_OPERATION_TOKEN];
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
_isOwnInternalOperation(options) {
|
|
1810
|
+
const token = this._getInternalOperationToken(options);
|
|
1811
|
+
return !!token && token === this._activeOperationToken;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
_beginBusyOperation(operationName) {
|
|
1815
|
+
const token = Symbol(operationName);
|
|
1816
|
+
this._activeOperationName = operationName;
|
|
1817
|
+
this._activeOperationToken = token;
|
|
1818
|
+
this._updateUI();
|
|
1819
|
+
return token;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
_endBusyOperation(token) {
|
|
1823
|
+
if (token && token === this._activeOperationToken) {
|
|
1824
|
+
this._activeOperationName = null;
|
|
1825
|
+
this._activeOperationToken = null;
|
|
1826
|
+
this._updateUI();
|
|
1827
|
+
}
|
|
1702
1828
|
}
|
|
1703
1829
|
|
|
1704
|
-
|
|
1830
|
+
_withInternalOperationOptions(token, options = {}) {
|
|
1831
|
+
return {
|
|
1832
|
+
...options,
|
|
1833
|
+
[INTERNAL_OPERATION_TOKEN]: token
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
_assertEditorAvailable(operationName) {
|
|
1705
1838
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
_assertIdleForOperation(operationName, options = {}) {
|
|
1842
|
+
this._assertEditorAvailable(operationName);
|
|
1843
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1706
1844
|
if (this.isAnimating || (this.animationQueue && this.animationQueue.isBusy())) {
|
|
1707
1845
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1708
1846
|
}
|
|
1847
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1848
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1849
|
+
}
|
|
1850
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1851
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || 'another operation'} is running`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1856
|
+
this._assertEditorAvailable(operationName);
|
|
1857
|
+
if (this._isLoading && !this._isOwnInternalOperation(options)) {
|
|
1858
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1859
|
+
}
|
|
1860
|
+
if (this._activeOperationToken && !this._isOwnInternalOperation(options)) {
|
|
1861
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || 'another operation'} is running`);
|
|
1862
|
+
}
|
|
1709
1863
|
}
|
|
1710
1864
|
|
|
1711
|
-
_canMutateNow(operationName) {
|
|
1865
|
+
_canMutateNow(operationName, options = {}) {
|
|
1712
1866
|
try {
|
|
1713
|
-
this._assertIdleForOperation(operationName);
|
|
1867
|
+
this._assertIdleForOperation(operationName, options);
|
|
1714
1868
|
return true;
|
|
1715
1869
|
} catch (error) {
|
|
1716
1870
|
this._reportError(`${operationName} blocked`, error);
|
|
@@ -1824,7 +1978,15 @@ function ensureFabric() {
|
|
|
1824
1978
|
* @public
|
|
1825
1979
|
*/
|
|
1826
1980
|
rotateImage(degrees, options = {}) {
|
|
1827
|
-
|
|
1981
|
+
try {
|
|
1982
|
+
this._assertCanQueueAnimation('rotateImage', options);
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
return Promise.reject(error);
|
|
1985
|
+
}
|
|
1986
|
+
return this.animationQueue.add(() => this._rotateImageImpl(degrees, options))
|
|
1987
|
+
.finally(() => {
|
|
1988
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1989
|
+
});
|
|
1828
1990
|
}
|
|
1829
1991
|
|
|
1830
1992
|
/**
|
|
@@ -1894,6 +2056,11 @@ function ensureFabric() {
|
|
|
1894
2056
|
*/
|
|
1895
2057
|
resetImageTransform() {
|
|
1896
2058
|
if (!this.originalImage) return Promise.resolve();
|
|
2059
|
+
try {
|
|
2060
|
+
this._assertCanQueueAnimation('resetImageTransform');
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
return Promise.reject(error);
|
|
2063
|
+
}
|
|
1897
2064
|
|
|
1898
2065
|
return this.animationQueue.add(async () => {
|
|
1899
2066
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow('resetImageTransform');
|
|
@@ -1901,6 +2068,8 @@ function ensureFabric() {
|
|
|
1901
2068
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1902
2069
|
const after = this._captureCanvasStateOrThrow('resetImageTransform');
|
|
1903
2070
|
this._pushStateTransition(before, after);
|
|
2071
|
+
}).finally(() => {
|
|
2072
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1904
2073
|
}).catch(error => {
|
|
1905
2074
|
this._reportError('resetImageTransform() failed', error);
|
|
1906
2075
|
throw error;
|
|
@@ -2045,12 +2214,24 @@ function ensureFabric() {
|
|
|
2045
2214
|
if (isSettled) return;
|
|
2046
2215
|
isSettled = true;
|
|
2047
2216
|
clearTimeout(timerId);
|
|
2048
|
-
imageElement.
|
|
2049
|
-
|
|
2217
|
+
if (typeof imageElement.removeEventListener === 'function') {
|
|
2218
|
+
imageElement.removeEventListener('load', handleLoad);
|
|
2219
|
+
imageElement.removeEventListener('error', handleError);
|
|
2220
|
+
} else {
|
|
2221
|
+
imageElement.onload = null;
|
|
2222
|
+
imageElement.onerror = null;
|
|
2223
|
+
}
|
|
2050
2224
|
callback();
|
|
2051
2225
|
};
|
|
2052
|
-
|
|
2053
|
-
|
|
2226
|
+
const handleLoad = () => settle(resolve);
|
|
2227
|
+
const handleError = (error) => settle(() => reject(error));
|
|
2228
|
+
if (typeof imageElement.addEventListener === 'function') {
|
|
2229
|
+
imageElement.addEventListener('load', handleLoad, { once: true });
|
|
2230
|
+
imageElement.addEventListener('error', handleError, { once: true });
|
|
2231
|
+
} else {
|
|
2232
|
+
imageElement.onload = handleLoad;
|
|
2233
|
+
imageElement.onerror = handleError;
|
|
2234
|
+
}
|
|
2054
2235
|
});
|
|
2055
2236
|
}
|
|
2056
2237
|
|
|
@@ -2435,7 +2616,7 @@ function ensureFabric() {
|
|
|
2435
2616
|
*/
|
|
2436
2617
|
removeAllMasks(options = {}) {
|
|
2437
2618
|
if (!this.canvas) return;
|
|
2438
|
-
if (!this._canMutateNow('removeAllMasks')) return;
|
|
2619
|
+
if (!this._canMutateNow('removeAllMasks', options)) return;
|
|
2439
2620
|
const saveHistory = options.saveHistory !== false;
|
|
2440
2621
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2441
2622
|
masks.forEach(mask => this._removeLabelForMask(mask));
|
|
@@ -2714,20 +2895,35 @@ function ensureFabric() {
|
|
|
2714
2895
|
this._assertIdleForOperation('mergeMasks');
|
|
2715
2896
|
const masks = this.canvas.getObjects().filter(object => object.maskId);
|
|
2716
2897
|
if (!masks.length) return;
|
|
2898
|
+
const beforeJson = this._serializeCanvasState();
|
|
2899
|
+
const operationToken = this._beginBusyOperation('mergeMasks');
|
|
2717
2900
|
|
|
2718
2901
|
this.canvas.discardActiveObject();
|
|
2719
2902
|
this.canvas.renderAll();
|
|
2720
2903
|
|
|
2721
2904
|
try {
|
|
2722
|
-
const
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2905
|
+
const merged = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
2906
|
+
exportImageArea: true,
|
|
2907
|
+
multiplier: this.options.exportMultiplier,
|
|
2908
|
+
fileType: 'png'
|
|
2909
|
+
}));
|
|
2910
|
+
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2911
|
+
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2912
|
+
preserveScroll: true,
|
|
2913
|
+
resetMaskCounter: false
|
|
2914
|
+
}));
|
|
2726
2915
|
const afterJson = this._serializeCanvasState();
|
|
2727
2916
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2728
2917
|
} catch (error) {
|
|
2729
2918
|
this._reportError('merge error', error);
|
|
2919
|
+
try {
|
|
2920
|
+
await this.loadFromState(beforeJson);
|
|
2921
|
+
} catch (restoreError) {
|
|
2922
|
+
this._reportError('mergeMasks rollback failed', restoreError);
|
|
2923
|
+
}
|
|
2730
2924
|
throw error;
|
|
2925
|
+
} finally {
|
|
2926
|
+
this._endBusyOperation(operationToken);
|
|
2731
2927
|
}
|
|
2732
2928
|
}
|
|
2733
2929
|
|
|
@@ -2783,7 +2979,7 @@ function ensureFabric() {
|
|
|
2783
2979
|
*/
|
|
2784
2980
|
async exportImageBase64(options = {}) {
|
|
2785
2981
|
if (!this.originalImage) throw new Error('No image loaded');
|
|
2786
|
-
this._assertIdleForOperation('exportImageBase64');
|
|
2982
|
+
this._assertIdleForOperation('exportImageBase64', options);
|
|
2787
2983
|
const exportImageArea = typeof options.exportImageArea === 'boolean' ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2788
2984
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2789
2985
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2800,12 +2996,13 @@ function ensureFabric() {
|
|
|
2800
2996
|
|
|
2801
2997
|
this.originalImage.setCoords();
|
|
2802
2998
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2803
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2804
|
-
return this._exportCanvasRegionToDataURL({
|
|
2999
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3000
|
+
return await this._exportCanvasRegionToDataURL({
|
|
2805
3001
|
...exportRegion,
|
|
2806
3002
|
multiplier,
|
|
2807
3003
|
quality,
|
|
2808
|
-
format
|
|
3004
|
+
format,
|
|
3005
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2809
3006
|
});
|
|
2810
3007
|
} finally {
|
|
2811
3008
|
maskVisibilityBackups.forEach(backup => {
|
|
@@ -2844,14 +3041,15 @@ function ensureFabric() {
|
|
|
2844
3041
|
// Compute an integer canvas region for the base image.
|
|
2845
3042
|
this.originalImage.setCoords();
|
|
2846
3043
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2847
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
3044
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2848
3045
|
|
|
2849
3046
|
// Crop precisely in offscreen canvas
|
|
2850
|
-
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
3047
|
+
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2851
3048
|
...exportRegion,
|
|
2852
3049
|
multiplier,
|
|
2853
3050
|
quality,
|
|
2854
|
-
format
|
|
3051
|
+
format,
|
|
3052
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2855
3053
|
});
|
|
2856
3054
|
} finally {
|
|
2857
3055
|
maskStyleBackups.forEach(backup => {
|
|
@@ -2915,6 +3113,7 @@ function ensureFabric() {
|
|
|
2915
3113
|
} = options;
|
|
2916
3114
|
|
|
2917
3115
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
3116
|
+
const normalizedQuality = this._normalizeQuality(quality);
|
|
2918
3117
|
|
|
2919
3118
|
// Generate the data URL in the requested export mode.
|
|
2920
3119
|
let imageBase64;
|
|
@@ -2922,14 +3121,14 @@ function ensureFabric() {
|
|
|
2922
3121
|
imageBase64 = await this.exportImageBase64({
|
|
2923
3122
|
exportImageArea: true,
|
|
2924
3123
|
multiplier,
|
|
2925
|
-
quality,
|
|
3124
|
+
quality: normalizedQuality,
|
|
2926
3125
|
fileType: safeFileType
|
|
2927
3126
|
});
|
|
2928
3127
|
} else {
|
|
2929
3128
|
imageBase64 = await this.exportImageBase64({
|
|
2930
3129
|
exportImageArea: false,
|
|
2931
3130
|
multiplier,
|
|
2932
|
-
quality,
|
|
3131
|
+
quality: normalizedQuality,
|
|
2933
3132
|
fileType: safeFileType
|
|
2934
3133
|
});
|
|
2935
3134
|
}
|
|
@@ -2949,7 +3148,7 @@ function ensureFabric() {
|
|
|
2949
3148
|
const context = offscreenCanvas.getContext('2d');
|
|
2950
3149
|
if (!context) throw new Error('Unable to create 2D canvas context for export conversion');
|
|
2951
3150
|
context.drawImage(imageElement, 0, 0);
|
|
2952
|
-
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`,
|
|
3151
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
2953
3152
|
resolve(convertedDataUrl);
|
|
2954
3153
|
} catch (error) { reject(error); }
|
|
2955
3154
|
};
|
|
@@ -3321,6 +3520,7 @@ function ensureFabric() {
|
|
|
3321
3520
|
const canUndo = this.historyManager?.canUndo();
|
|
3322
3521
|
const canRedo = this.historyManager?.canRedo();
|
|
3323
3522
|
const isInCropMode = !!this._cropMode;
|
|
3523
|
+
const isBusy = this.isAnimating || this._isLoading || !!this._activeOperationToken || !!(this.animationQueue && this.animationQueue.isBusy());
|
|
3324
3524
|
|
|
3325
3525
|
if (isInCropMode) {
|
|
3326
3526
|
// Disable all controls except the crop action buttons while crop mode is active.
|
|
@@ -3336,23 +3536,23 @@ function ensureFabric() {
|
|
|
3336
3536
|
return;
|
|
3337
3537
|
}
|
|
3338
3538
|
|
|
3339
|
-
this._setDisabled('zoomInBtn', !hasImage ||
|
|
3340
|
-
this._setDisabled('zoomOutBtn', !hasImage ||
|
|
3341
|
-
this._setDisabled('rotateLeftBtn', !hasImage ||
|
|
3342
|
-
this._setDisabled('rotateRightBtn', !hasImage ||
|
|
3343
|
-
this._setDisabled('addMaskBtn', !hasImage ||
|
|
3344
|
-
this._setDisabled('removeMaskBtn', !hasSelectedMask ||
|
|
3345
|
-
this._setDisabled('removeAllMasksBtn', !hasMasks ||
|
|
3346
|
-
this._setDisabled('mergeBtn', !hasImage || !hasMasks ||
|
|
3347
|
-
this._setDisabled('downloadBtn', !hasImage ||
|
|
3348
|
-
this._setDisabled('resetBtn', !hasImage || isDefaultTransform ||
|
|
3349
|
-
this._setDisabled('undoBtn', !hasImage ||
|
|
3350
|
-
this._setDisabled('redoBtn', !hasImage ||
|
|
3351
|
-
this._setDisabled('cropBtn', !hasImage ||
|
|
3539
|
+
this._setDisabled('zoomInBtn', !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3540
|
+
this._setDisabled('zoomOutBtn', !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3541
|
+
this._setDisabled('rotateLeftBtn', !hasImage || isBusy);
|
|
3542
|
+
this._setDisabled('rotateRightBtn', !hasImage || isBusy);
|
|
3543
|
+
this._setDisabled('addMaskBtn', !hasImage || isBusy);
|
|
3544
|
+
this._setDisabled('removeMaskBtn', !hasSelectedMask || isBusy);
|
|
3545
|
+
this._setDisabled('removeAllMasksBtn', !hasMasks || isBusy);
|
|
3546
|
+
this._setDisabled('mergeBtn', !hasImage || !hasMasks || isBusy);
|
|
3547
|
+
this._setDisabled('downloadBtn', !hasImage || isBusy);
|
|
3548
|
+
this._setDisabled('resetBtn', !hasImage || isDefaultTransform || isBusy);
|
|
3549
|
+
this._setDisabled('undoBtn', !hasImage || isBusy || !canUndo);
|
|
3550
|
+
this._setDisabled('redoBtn', !hasImage || isBusy || !canRedo);
|
|
3551
|
+
this._setDisabled('cropBtn', !hasImage || isBusy);
|
|
3352
3552
|
this._setDisabled('applyCropBtn', true);
|
|
3353
3553
|
this._setDisabled('cancelCropBtn', true);
|
|
3354
|
-
this._setDisabled('imageInput',
|
|
3355
|
-
this._setDisabled('uploadArea',
|
|
3554
|
+
this._setDisabled('imageInput', isBusy);
|
|
3555
|
+
this._setDisabled('uploadArea', isBusy);
|
|
3356
3556
|
}
|
|
3357
3557
|
|
|
3358
3558
|
/**
|
|
@@ -3369,13 +3569,17 @@ function ensureFabric() {
|
|
|
3369
3569
|
element.disabled = !!disabled;
|
|
3370
3570
|
return;
|
|
3371
3571
|
}
|
|
3572
|
+
if (!this._elementOriginalPointerEvents) this._elementOriginalPointerEvents = new Map();
|
|
3573
|
+
if (!this._elementOriginalPointerEvents.has(key)) {
|
|
3574
|
+
this._elementOriginalPointerEvents.set(key, element.style.pointerEvents || '');
|
|
3575
|
+
}
|
|
3372
3576
|
|
|
3373
3577
|
if (disabled) {
|
|
3374
3578
|
element.setAttribute('aria-disabled', 'true');
|
|
3375
3579
|
element.style.pointerEvents = 'none';
|
|
3376
3580
|
} else {
|
|
3377
3581
|
element.removeAttribute('aria-disabled');
|
|
3378
|
-
element.style.pointerEvents = '';
|
|
3582
|
+
element.style.pointerEvents = this._elementOriginalPointerEvents.get(key) ?? '';
|
|
3379
3583
|
}
|
|
3380
3584
|
}
|
|
3381
3585
|
|
|
@@ -3474,6 +3678,9 @@ function ensureFabric() {
|
|
|
3474
3678
|
if (this.animationQueue) {
|
|
3475
3679
|
this.animationQueue.cancelAll(new Error('Editor disposed'));
|
|
3476
3680
|
}
|
|
3681
|
+
this._isLoading = false;
|
|
3682
|
+
this._activeOperationName = null;
|
|
3683
|
+
this._activeOperationToken = null;
|
|
3477
3684
|
|
|
3478
3685
|
// Remove bound DOM event listeners
|
|
3479
3686
|
try {
|
|
@@ -3520,17 +3727,20 @@ function ensureFabric() {
|
|
|
3520
3727
|
}
|
|
3521
3728
|
this._handlersByElementKey = {};
|
|
3522
3729
|
this._elementCache = {};
|
|
3730
|
+
this._elementOriginalPointerEvents = new Map();
|
|
3523
3731
|
this._clearMaskPlacementMemory();
|
|
3524
3732
|
this.originalImage = null;
|
|
3525
3733
|
this.baseImageScale = 1;
|
|
3526
3734
|
this.currentScale = 1;
|
|
3527
3735
|
this.currentRotation = 0;
|
|
3528
3736
|
this.isAnimating = false;
|
|
3737
|
+
this._isLoading = false;
|
|
3529
3738
|
this._cropMode = false;
|
|
3530
3739
|
this._cropRect = null;
|
|
3531
3740
|
this._cropHandlers = [];
|
|
3532
3741
|
this._cropPrevEvented = null;
|
|
3533
3742
|
this._prevSelectionSetting = undefined;
|
|
3743
|
+
this._lastContainerViewportSize = null;
|
|
3534
3744
|
this._initialized = false;
|
|
3535
3745
|
}
|
|
3536
3746
|
}
|
|
@@ -3585,6 +3795,7 @@ function ensureFabric() {
|
|
|
3585
3795
|
*/
|
|
3586
3796
|
this.isRunning = false;
|
|
3587
3797
|
this.currentTask = null;
|
|
3798
|
+
this._generation = 0;
|
|
3588
3799
|
}
|
|
3589
3800
|
|
|
3590
3801
|
/**
|
|
@@ -3607,6 +3818,7 @@ function ensureFabric() {
|
|
|
3607
3818
|
}
|
|
3608
3819
|
|
|
3609
3820
|
cancelAll(reason = new Error('Animation queue cancelled')) {
|
|
3821
|
+
this._generation += 1;
|
|
3610
3822
|
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3611
3823
|
const tasks = [
|
|
3612
3824
|
...(this.currentTask ? [this.currentTask] : []),
|
|
@@ -3629,29 +3841,35 @@ function ensureFabric() {
|
|
|
3629
3841
|
*/
|
|
3630
3842
|
async _drainQueue() {
|
|
3631
3843
|
if (this.isRunning) return;
|
|
3844
|
+
const generation = this._generation;
|
|
3632
3845
|
this.isRunning = true;
|
|
3633
3846
|
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3847
|
+
try {
|
|
3848
|
+
while (this.animationTasks.length > 0 && generation === this._generation) {
|
|
3849
|
+
const task = this.animationTasks.shift();
|
|
3850
|
+
this.currentTask = task;
|
|
3637
3851
|
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3852
|
+
try {
|
|
3853
|
+
const result = await task.animationFn();
|
|
3854
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3855
|
+
task.isSettled = true;
|
|
3856
|
+
task.resolve(result);
|
|
3857
|
+
}
|
|
3858
|
+
} catch (error) {
|
|
3859
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3860
|
+
task.isSettled = true;
|
|
3861
|
+
task.reject(error);
|
|
3862
|
+
}
|
|
3863
|
+
} finally {
|
|
3864
|
+
if (generation === this._generation && this.currentTask === task) this.currentTask = null;
|
|
3648
3865
|
}
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3866
|
+
}
|
|
3867
|
+
} finally {
|
|
3868
|
+
if (generation === this._generation) {
|
|
3869
|
+
this.isRunning = false;
|
|
3870
|
+
this.currentTask = null;
|
|
3651
3871
|
}
|
|
3652
3872
|
}
|
|
3653
|
-
|
|
3654
|
-
this.isRunning = false;
|
|
3655
3873
|
}
|
|
3656
3874
|
}
|
|
3657
3875
|
|