@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
|
@@ -5,12 +5,13 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.4.
|
|
8
|
+
* @version 1.4.1
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
12
12
|
*/
|
|
13
13
|
var fabric = null;
|
|
14
|
+
var INTERNAL_OPERATION_TOKEN = /* @__PURE__ */ Symbol("ImageEditorInternalOperation");
|
|
14
15
|
function getGlobalScope() {
|
|
15
16
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
16
17
|
if (typeof self !== "undefined") return self;
|
|
@@ -118,11 +119,15 @@ var ImageEditor = class {
|
|
|
118
119
|
this.currentRotation = 0;
|
|
119
120
|
this.maskCounter = 0;
|
|
120
121
|
this.isAnimating = false;
|
|
122
|
+
this._isLoading = false;
|
|
123
|
+
this._activeOperationName = null;
|
|
124
|
+
this._activeOperationToken = null;
|
|
121
125
|
this.elements = {};
|
|
122
126
|
this.isImageLoadedToCanvas = false;
|
|
123
127
|
this.maxHistorySize = 50;
|
|
124
128
|
this._handlersByElementKey = {};
|
|
125
129
|
this._elementCache = {};
|
|
130
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
126
131
|
this._lastMask = null;
|
|
127
132
|
this._lastMaskInitialLeft = null;
|
|
128
133
|
this._lastMaskInitialTop = null;
|
|
@@ -211,6 +216,10 @@ var ImageEditor = class {
|
|
|
211
216
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
212
217
|
this._visibilityStateByElement = /* @__PURE__ */ new WeakMap();
|
|
213
218
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
219
|
+
this._isLoading = false;
|
|
220
|
+
this._activeOperationName = null;
|
|
221
|
+
this._activeOperationToken = null;
|
|
222
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
214
223
|
this._containerOriginalOverflow = null;
|
|
215
224
|
this._lastContainerViewportSize = null;
|
|
216
225
|
this._canvasElementOriginalStyle = null;
|
|
@@ -417,6 +426,12 @@ var ImageEditor = class {
|
|
|
417
426
|
this.containerElement.style.overflowX = this._containerOriginalOverflow.overflowX;
|
|
418
427
|
this.containerElement.style.overflowY = this._containerOriginalOverflow.overflowY;
|
|
419
428
|
}
|
|
429
|
+
_restoreContainerOverflowSnapshot(snapshot) {
|
|
430
|
+
if (!this.containerElement || !this.containerElement.style || !snapshot) return;
|
|
431
|
+
this.containerElement.style.overflow = snapshot.overflow || "";
|
|
432
|
+
this.containerElement.style.overflowX = snapshot.overflowX || "";
|
|
433
|
+
this.containerElement.style.overflowY = snapshot.overflowY || "";
|
|
434
|
+
}
|
|
420
435
|
/**
|
|
421
436
|
* DOM / UI bindings
|
|
422
437
|
* @private
|
|
@@ -551,7 +566,9 @@ var ImageEditor = class {
|
|
|
551
566
|
if (!this._fabricLoaded) return;
|
|
552
567
|
if (!this.canvas || this._disposed) return;
|
|
553
568
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
554
|
-
this._assertIdleForOperation("loadImage");
|
|
569
|
+
this._assertIdleForOperation("loadImage", options);
|
|
570
|
+
this._isLoading = true;
|
|
571
|
+
this._updateUI();
|
|
555
572
|
this._warnOnImageLayoutOptionConflict();
|
|
556
573
|
const transaction = this._captureLoadImageTransaction();
|
|
557
574
|
try {
|
|
@@ -571,7 +588,7 @@ var ImageEditor = class {
|
|
|
571
588
|
imageElement,
|
|
572
589
|
targetWidth,
|
|
573
590
|
targetHeight,
|
|
574
|
-
this.options.downsampleQuality,
|
|
591
|
+
this._normalizeQuality(this.options.downsampleQuality),
|
|
575
592
|
imageBase64
|
|
576
593
|
);
|
|
577
594
|
}
|
|
@@ -639,6 +656,9 @@ var ImageEditor = class {
|
|
|
639
656
|
} catch (error) {
|
|
640
657
|
await this._rollbackLoadImageTransaction(transaction);
|
|
641
658
|
throw error;
|
|
659
|
+
} finally {
|
|
660
|
+
this._isLoading = false;
|
|
661
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
642
662
|
}
|
|
643
663
|
}
|
|
644
664
|
/**
|
|
@@ -744,9 +764,14 @@ var ImageEditor = class {
|
|
|
744
764
|
}
|
|
745
765
|
async _rollbackLoadImageTransaction(transaction) {
|
|
746
766
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
767
|
+
let didRestoreCanvasState = false;
|
|
747
768
|
try {
|
|
748
|
-
if (transaction.canvasState)
|
|
769
|
+
if (transaction.canvasState) {
|
|
770
|
+
await this.loadFromState(transaction.canvasState);
|
|
771
|
+
didRestoreCanvasState = true;
|
|
772
|
+
}
|
|
749
773
|
} catch (error) {
|
|
774
|
+
this._lastMask = null;
|
|
750
775
|
this._reportError("loadImage rollback failed", error);
|
|
751
776
|
}
|
|
752
777
|
this.baseImageScale = transaction.baseImageScale;
|
|
@@ -755,22 +780,40 @@ var ImageEditor = class {
|
|
|
755
780
|
this.maskCounter = transaction.maskCounter;
|
|
756
781
|
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
757
782
|
this._lastSnapshot = transaction.lastSnapshot;
|
|
783
|
+
if (didRestoreCanvasState) {
|
|
784
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
785
|
+
} else {
|
|
786
|
+
this._lastMask = null;
|
|
787
|
+
}
|
|
758
788
|
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
759
789
|
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
760
790
|
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
761
|
-
this._containerOriginalOverflow = transaction.containerOverflow;
|
|
762
791
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
763
792
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
764
793
|
if (this.containerElement) {
|
|
765
794
|
this.containerElement.scrollLeft = transaction.scrollLeft;
|
|
766
795
|
this.containerElement.scrollTop = transaction.scrollTop;
|
|
767
|
-
this.
|
|
796
|
+
this._restoreContainerOverflowSnapshot(transaction.containerOverflow);
|
|
768
797
|
}
|
|
769
798
|
this._updateInputs();
|
|
770
799
|
this._updateMaskList();
|
|
771
800
|
this._updateUI();
|
|
772
801
|
if (this.canvas) this.canvas.renderAll();
|
|
773
802
|
}
|
|
803
|
+
_restoreLastMaskReference(previousLastMask) {
|
|
804
|
+
if (!this.canvas) {
|
|
805
|
+
this._lastMask = null;
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
809
|
+
const previousMaskId = previousLastMask && previousLastMask.maskId;
|
|
810
|
+
this._lastMask = masks.find((mask) => mask.maskId === previousMaskId) || masks[masks.length - 1] || null;
|
|
811
|
+
if (!this._lastMask) {
|
|
812
|
+
this._lastMaskInitialLeft = null;
|
|
813
|
+
this._lastMaskInitialTop = null;
|
|
814
|
+
this._lastMaskInitialWidth = null;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
774
817
|
/**
|
|
775
818
|
* Resamples the given image element to a new width and height and returns the result as a data URL.
|
|
776
819
|
*
|
|
@@ -1054,7 +1097,11 @@ var ImageEditor = class {
|
|
|
1054
1097
|
maskStyleBackups.push(backup);
|
|
1055
1098
|
mask.set(stylePatch);
|
|
1056
1099
|
});
|
|
1057
|
-
|
|
1100
|
+
const result = callback();
|
|
1101
|
+
if (result && typeof result.then === "function") {
|
|
1102
|
+
throw new Error("_withNormalizedMaskStyles callback must be synchronous");
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1058
1105
|
} finally {
|
|
1059
1106
|
maskStyleBackups.forEach((backup) => {
|
|
1060
1107
|
try {
|
|
@@ -1122,9 +1169,13 @@ var ImageEditor = class {
|
|
|
1122
1169
|
* @returns {number} A finite quality value between 0 and 1.
|
|
1123
1170
|
* @private
|
|
1124
1171
|
*/
|
|
1125
|
-
_normalizeQuality(quality) {
|
|
1172
|
+
_normalizeQuality(quality, fallback = void 0) {
|
|
1173
|
+
const fallbackQuality = fallback == null ? this.options.downsampleQuality : fallback;
|
|
1174
|
+
const numericFallback = fallbackQuality == null ? NaN : Number(fallbackQuality);
|
|
1175
|
+
const safeFallback = Number.isFinite(numericFallback) ? Math.max(0, Math.min(1, numericFallback)) : 0.92;
|
|
1176
|
+
if (quality == null) return safeFallback;
|
|
1126
1177
|
const numericQuality = Number(quality);
|
|
1127
|
-
if (!Number.isFinite(numericQuality)) return
|
|
1178
|
+
if (!Number.isFinite(numericQuality)) return safeFallback;
|
|
1128
1179
|
return Math.max(0, Math.min(1, numericQuality));
|
|
1129
1180
|
}
|
|
1130
1181
|
/**
|
|
@@ -1175,64 +1226,63 @@ var ImageEditor = class {
|
|
|
1175
1226
|
sourceHeight: Math.max(1, endY - sourceY)
|
|
1176
1227
|
};
|
|
1177
1228
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
});
|
|
1229
|
+
_hasFractionalCanvasEdge(value) {
|
|
1230
|
+
const numericValue = Number(value);
|
|
1231
|
+
if (!Number.isFinite(numericValue)) return false;
|
|
1232
|
+
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1233
|
+
}
|
|
1234
|
+
_getPartialExportEdges(bounds) {
|
|
1235
|
+
if (!bounds) return null;
|
|
1236
|
+
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
1237
|
+
const isAxisAligned = angle < 0.01 || Math.abs(angle - 90) < 0.01;
|
|
1238
|
+
if (!isAxisAligned) return null;
|
|
1239
|
+
return {
|
|
1240
|
+
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1241
|
+
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1242
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1243
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
1247
|
+
if (!edges || !Object.values(edges).some(Boolean)) return dataUrl;
|
|
1248
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1249
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1250
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1251
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1252
|
+
offscreenCanvas.width = width;
|
|
1253
|
+
offscreenCanvas.height = height;
|
|
1254
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1255
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1256
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1257
|
+
const imageData = context.getImageData(0, 0, width, height);
|
|
1258
|
+
const pixels = imageData.data;
|
|
1259
|
+
const sealPixel = (x, y, fallbackX, fallbackY) => {
|
|
1260
|
+
const index = (y * width + x) * 4;
|
|
1261
|
+
const fallbackIndex = (fallbackY * width + fallbackX) * 4;
|
|
1262
|
+
if (pixels[index + 3] === 0 && pixels[fallbackIndex + 3] > 0) {
|
|
1263
|
+
pixels[index] = pixels[fallbackIndex];
|
|
1264
|
+
pixels[index + 1] = pixels[fallbackIndex + 1];
|
|
1265
|
+
pixels[index + 2] = pixels[fallbackIndex + 2];
|
|
1266
|
+
pixels[index + 3] = pixels[fallbackIndex + 3];
|
|
1267
|
+
}
|
|
1268
|
+
if (pixels[index + 3] > 0 && pixels[index + 3] < 255) {
|
|
1269
|
+
pixels[index + 3] = 255;
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
if (edges.left && width > 1) {
|
|
1273
|
+
for (let y = 0; y < height; y += 1) sealPixel(0, y, 1, y);
|
|
1274
|
+
}
|
|
1275
|
+
if (edges.right && width > 1) {
|
|
1276
|
+
for (let y = 0; y < height; y += 1) sealPixel(width - 1, y, width - 2, y);
|
|
1277
|
+
}
|
|
1278
|
+
if (edges.top && height > 1) {
|
|
1279
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, 0, x, 1);
|
|
1280
|
+
}
|
|
1281
|
+
if (edges.bottom && height > 1) {
|
|
1282
|
+
for (let x = 0; x < width; x += 1) sealPixel(x, height - 1, x, height - 2);
|
|
1283
|
+
}
|
|
1284
|
+
context.putImageData(imageData, 0, 0);
|
|
1285
|
+
return offscreenCanvas.toDataURL("image/png");
|
|
1236
1286
|
}
|
|
1237
1287
|
/**
|
|
1238
1288
|
* Exports a source region directly through Fabric's region export options.
|
|
@@ -1245,13 +1295,16 @@ var ImageEditor = class {
|
|
|
1245
1295
|
* @param {number} [region.multiplier=1] - Export multiplier.
|
|
1246
1296
|
* @param {number} [region.quality=0.92] - Output image quality for lossy formats.
|
|
1247
1297
|
* @param {'jpeg'|'png'|'webp'} [region.format='jpeg'] - Output image format.
|
|
1298
|
+
* @param {Object|null} [region.sealPartialEdges=null] - Fractional canvas edges whose alpha should be sealed.
|
|
1248
1299
|
* @returns {Promise<string>} Resolves with an image data URL for the cropped region.
|
|
1249
1300
|
* @private
|
|
1250
1301
|
*/
|
|
1251
|
-
_exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
1302
|
+
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1252
1303
|
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
1253
|
-
|
|
1254
|
-
|
|
1304
|
+
const safeFormat = this._normalizeImageFormat(format);
|
|
1305
|
+
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1306
|
+
let regionDataUrl = this.canvas.toDataURL({
|
|
1307
|
+
format: exportFormat,
|
|
1255
1308
|
quality,
|
|
1256
1309
|
multiplier: safeMultiplier,
|
|
1257
1310
|
left: sourceX,
|
|
@@ -1259,6 +1312,29 @@ var ImageEditor = class {
|
|
|
1259
1312
|
width: sourceWidth,
|
|
1260
1313
|
height: sourceHeight
|
|
1261
1314
|
});
|
|
1315
|
+
regionDataUrl = await this._sealPartialTransparentEdges(regionDataUrl, sealPartialEdges);
|
|
1316
|
+
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1317
|
+
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1318
|
+
}
|
|
1319
|
+
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1320
|
+
const imageElement = await this._createImageElement(dataUrl);
|
|
1321
|
+
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
1322
|
+
const height = Math.max(1, imageElement.naturalHeight || imageElement.height || 1);
|
|
1323
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1324
|
+
offscreenCanvas.width = width;
|
|
1325
|
+
offscreenCanvas.height = height;
|
|
1326
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1327
|
+
if (!context) throw new Error("2D canvas context is unavailable");
|
|
1328
|
+
context.fillStyle = this._getJpegBackgroundColor();
|
|
1329
|
+
context.fillRect(0, 0, width, height);
|
|
1330
|
+
context.drawImage(imageElement, 0, 0, width, height);
|
|
1331
|
+
return offscreenCanvas.toDataURL("image/jpeg", this._normalizeQuality(quality));
|
|
1332
|
+
}
|
|
1333
|
+
_getJpegBackgroundColor() {
|
|
1334
|
+
const backgroundColor = String(this.options.backgroundColor || "").trim();
|
|
1335
|
+
if (!backgroundColor || backgroundColor === "transparent") return "#ffffff";
|
|
1336
|
+
if (/^rgba\([^)]*,\s*0(?:\.0+)?\s*\)$/i.test(backgroundColor)) return "#ffffff";
|
|
1337
|
+
return backgroundColor;
|
|
1262
1338
|
}
|
|
1263
1339
|
/**
|
|
1264
1340
|
* Gets the top-left corner coordinates of the given object.
|
|
@@ -1416,17 +1492,70 @@ var ImageEditor = class {
|
|
|
1416
1492
|
* @public
|
|
1417
1493
|
*/
|
|
1418
1494
|
scaleImage(factor, options = {}) {
|
|
1419
|
-
|
|
1495
|
+
try {
|
|
1496
|
+
this._assertCanQueueAnimation("scaleImage", options);
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
return Promise.reject(error);
|
|
1499
|
+
}
|
|
1500
|
+
return this.animationQueue.add(() => this._scaleImageImpl(factor, options)).finally(() => {
|
|
1501
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
_getInternalOperationToken(options) {
|
|
1505
|
+
return options && options[INTERNAL_OPERATION_TOKEN];
|
|
1420
1506
|
}
|
|
1421
|
-
|
|
1507
|
+
_isOwnInternalOperation(options) {
|
|
1508
|
+
const token = this._getInternalOperationToken(options);
|
|
1509
|
+
return !!token && token === this._activeOperationToken;
|
|
1510
|
+
}
|
|
1511
|
+
_beginBusyOperation(operationName) {
|
|
1512
|
+
const token = Symbol(operationName);
|
|
1513
|
+
this._activeOperationName = operationName;
|
|
1514
|
+
this._activeOperationToken = token;
|
|
1515
|
+
this._updateUI();
|
|
1516
|
+
return token;
|
|
1517
|
+
}
|
|
1518
|
+
_endBusyOperation(token) {
|
|
1519
|
+
if (token && token === this._activeOperationToken) {
|
|
1520
|
+
this._activeOperationName = null;
|
|
1521
|
+
this._activeOperationToken = null;
|
|
1522
|
+
this._updateUI();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
_withInternalOperationOptions(token, options = {}) {
|
|
1526
|
+
return {
|
|
1527
|
+
...options,
|
|
1528
|
+
[INTERNAL_OPERATION_TOKEN]: token
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
_assertEditorAvailable(operationName) {
|
|
1422
1532
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1533
|
+
}
|
|
1534
|
+
_assertIdleForOperation(operationName, options = {}) {
|
|
1535
|
+
this._assertEditorAvailable(operationName);
|
|
1536
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1423
1537
|
if (this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) {
|
|
1424
1538
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1425
1539
|
}
|
|
1540
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1541
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1542
|
+
}
|
|
1543
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1544
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1548
|
+
this._assertEditorAvailable(operationName);
|
|
1549
|
+
if (this._isLoading && !this._isOwnInternalOperation(options)) {
|
|
1550
|
+
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1551
|
+
}
|
|
1552
|
+
if (this._activeOperationToken && !this._isOwnInternalOperation(options)) {
|
|
1553
|
+
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1554
|
+
}
|
|
1426
1555
|
}
|
|
1427
|
-
_canMutateNow(operationName) {
|
|
1556
|
+
_canMutateNow(operationName, options = {}) {
|
|
1428
1557
|
try {
|
|
1429
|
-
this._assertIdleForOperation(operationName);
|
|
1558
|
+
this._assertIdleForOperation(operationName, options);
|
|
1430
1559
|
return true;
|
|
1431
1560
|
} catch (error) {
|
|
1432
1561
|
this._reportError(`${operationName} blocked`, error);
|
|
@@ -1531,7 +1660,14 @@ var ImageEditor = class {
|
|
|
1531
1660
|
* @public
|
|
1532
1661
|
*/
|
|
1533
1662
|
rotateImage(degrees, options = {}) {
|
|
1534
|
-
|
|
1663
|
+
try {
|
|
1664
|
+
this._assertCanQueueAnimation("rotateImage", options);
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
return Promise.reject(error);
|
|
1667
|
+
}
|
|
1668
|
+
return this.animationQueue.add(() => this._rotateImageImpl(degrees, options)).finally(() => {
|
|
1669
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1670
|
+
});
|
|
1535
1671
|
}
|
|
1536
1672
|
/**
|
|
1537
1673
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -1593,12 +1729,19 @@ var ImageEditor = class {
|
|
|
1593
1729
|
*/
|
|
1594
1730
|
resetImageTransform() {
|
|
1595
1731
|
if (!this.originalImage) return Promise.resolve();
|
|
1732
|
+
try {
|
|
1733
|
+
this._assertCanQueueAnimation("resetImageTransform");
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
return Promise.reject(error);
|
|
1736
|
+
}
|
|
1596
1737
|
return this.animationQueue.add(async () => {
|
|
1597
1738
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1598
1739
|
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1599
1740
|
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1600
1741
|
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1601
1742
|
this._pushStateTransition(before, after);
|
|
1743
|
+
}).finally(() => {
|
|
1744
|
+
if (!this._disposed && this.canvas) this._updateUI();
|
|
1602
1745
|
}).catch((error) => {
|
|
1603
1746
|
this._reportError("resetImageTransform() failed", error);
|
|
1604
1747
|
throw error;
|
|
@@ -1723,12 +1866,24 @@ var ImageEditor = class {
|
|
|
1723
1866
|
if (isSettled) return;
|
|
1724
1867
|
isSettled = true;
|
|
1725
1868
|
clearTimeout(timerId);
|
|
1726
|
-
imageElement.
|
|
1727
|
-
|
|
1869
|
+
if (typeof imageElement.removeEventListener === "function") {
|
|
1870
|
+
imageElement.removeEventListener("load", handleLoad);
|
|
1871
|
+
imageElement.removeEventListener("error", handleError);
|
|
1872
|
+
} else {
|
|
1873
|
+
imageElement.onload = null;
|
|
1874
|
+
imageElement.onerror = null;
|
|
1875
|
+
}
|
|
1728
1876
|
callback();
|
|
1729
1877
|
};
|
|
1730
|
-
|
|
1731
|
-
|
|
1878
|
+
const handleLoad = () => settle(resolve);
|
|
1879
|
+
const handleError = (error) => settle(() => reject(error));
|
|
1880
|
+
if (typeof imageElement.addEventListener === "function") {
|
|
1881
|
+
imageElement.addEventListener("load", handleLoad, { once: true });
|
|
1882
|
+
imageElement.addEventListener("error", handleError, { once: true });
|
|
1883
|
+
} else {
|
|
1884
|
+
imageElement.onload = handleLoad;
|
|
1885
|
+
imageElement.onerror = handleError;
|
|
1886
|
+
}
|
|
1732
1887
|
});
|
|
1733
1888
|
}
|
|
1734
1889
|
/**
|
|
@@ -2081,7 +2236,7 @@ var ImageEditor = class {
|
|
|
2081
2236
|
*/
|
|
2082
2237
|
removeAllMasks(options = {}) {
|
|
2083
2238
|
if (!this.canvas) return;
|
|
2084
|
-
if (!this._canMutateNow("removeAllMasks")) return;
|
|
2239
|
+
if (!this._canMutateNow("removeAllMasks", options)) return;
|
|
2085
2240
|
const saveHistory = options.saveHistory !== false;
|
|
2086
2241
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2087
2242
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
@@ -2354,18 +2509,33 @@ var ImageEditor = class {
|
|
|
2354
2509
|
this._assertIdleForOperation("mergeMasks");
|
|
2355
2510
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2356
2511
|
if (!masks.length) return;
|
|
2512
|
+
const beforeJson = this._serializeCanvasState();
|
|
2513
|
+
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2357
2514
|
this.canvas.discardActiveObject();
|
|
2358
2515
|
this.canvas.renderAll();
|
|
2359
2516
|
try {
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2517
|
+
const merged = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
2518
|
+
exportImageArea: true,
|
|
2519
|
+
multiplier: this.options.exportMultiplier,
|
|
2520
|
+
fileType: "png"
|
|
2521
|
+
}));
|
|
2522
|
+
this.removeAllMasks(this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2523
|
+
await this.loadImage(merged, this._withInternalOperationOptions(operationToken, {
|
|
2524
|
+
preserveScroll: true,
|
|
2525
|
+
resetMaskCounter: false
|
|
2526
|
+
}));
|
|
2364
2527
|
const afterJson = this._serializeCanvasState();
|
|
2365
2528
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2366
2529
|
} catch (error) {
|
|
2367
2530
|
this._reportError("merge error", error);
|
|
2531
|
+
try {
|
|
2532
|
+
await this.loadFromState(beforeJson);
|
|
2533
|
+
} catch (restoreError) {
|
|
2534
|
+
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2535
|
+
}
|
|
2368
2536
|
throw error;
|
|
2537
|
+
} finally {
|
|
2538
|
+
this._endBusyOperation(operationToken);
|
|
2369
2539
|
}
|
|
2370
2540
|
}
|
|
2371
2541
|
/**
|
|
@@ -2416,7 +2586,7 @@ var ImageEditor = class {
|
|
|
2416
2586
|
*/
|
|
2417
2587
|
async exportImageBase64(options = {}) {
|
|
2418
2588
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2419
|
-
this._assertIdleForOperation("exportImageBase64");
|
|
2589
|
+
this._assertIdleForOperation("exportImageBase64", options);
|
|
2420
2590
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2421
2591
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2422
2592
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
@@ -2432,12 +2602,13 @@ var ImageEditor = class {
|
|
|
2432
2602
|
this.canvas.renderAll();
|
|
2433
2603
|
this.originalImage.setCoords();
|
|
2434
2604
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2435
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2436
|
-
return this._exportCanvasRegionToDataURL({
|
|
2605
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2606
|
+
return await this._exportCanvasRegionToDataURL({
|
|
2437
2607
|
...exportRegion,
|
|
2438
2608
|
multiplier,
|
|
2439
2609
|
quality,
|
|
2440
|
-
format
|
|
2610
|
+
format,
|
|
2611
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2441
2612
|
});
|
|
2442
2613
|
} finally {
|
|
2443
2614
|
maskVisibilityBackups.forEach((backup) => {
|
|
@@ -2472,12 +2643,13 @@ var ImageEditor = class {
|
|
|
2472
2643
|
this.canvas.renderAll();
|
|
2473
2644
|
this.originalImage.setCoords();
|
|
2474
2645
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2475
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds
|
|
2476
|
-
finalBase64 = this._exportCanvasRegionToDataURL({
|
|
2646
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2647
|
+
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2477
2648
|
...exportRegion,
|
|
2478
2649
|
multiplier,
|
|
2479
2650
|
quality,
|
|
2480
|
-
format
|
|
2651
|
+
format,
|
|
2652
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2481
2653
|
});
|
|
2482
2654
|
} finally {
|
|
2483
2655
|
maskStyleBackups.forEach((backup) => {
|
|
@@ -2538,19 +2710,20 @@ var ImageEditor = class {
|
|
|
2538
2710
|
fileName = this.options.defaultDownloadFileName ?? "exported_image.jpg"
|
|
2539
2711
|
} = options;
|
|
2540
2712
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
2713
|
+
const normalizedQuality = this._normalizeQuality(quality);
|
|
2541
2714
|
let imageBase64;
|
|
2542
2715
|
if (mergeMask) {
|
|
2543
2716
|
imageBase64 = await this.exportImageBase64({
|
|
2544
2717
|
exportImageArea: true,
|
|
2545
2718
|
multiplier,
|
|
2546
|
-
quality,
|
|
2719
|
+
quality: normalizedQuality,
|
|
2547
2720
|
fileType: safeFileType
|
|
2548
2721
|
});
|
|
2549
2722
|
} else {
|
|
2550
2723
|
imageBase64 = await this.exportImageBase64({
|
|
2551
2724
|
exportImageArea: false,
|
|
2552
2725
|
multiplier,
|
|
2553
|
-
quality,
|
|
2726
|
+
quality: normalizedQuality,
|
|
2554
2727
|
fileType: safeFileType
|
|
2555
2728
|
});
|
|
2556
2729
|
}
|
|
@@ -2567,7 +2740,7 @@ var ImageEditor = class {
|
|
|
2567
2740
|
const context = offscreenCanvas.getContext("2d");
|
|
2568
2741
|
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
2569
2742
|
context.drawImage(imageElement, 0, 0);
|
|
2570
|
-
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`,
|
|
2743
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
2571
2744
|
resolve(convertedDataUrl);
|
|
2572
2745
|
} catch (error) {
|
|
2573
2746
|
reject(error);
|
|
@@ -2893,6 +3066,7 @@ var ImageEditor = class {
|
|
|
2893
3066
|
const canUndo = this.historyManager?.canUndo();
|
|
2894
3067
|
const canRedo = this.historyManager?.canRedo();
|
|
2895
3068
|
const isInCropMode = !!this._cropMode;
|
|
3069
|
+
const isBusy = this.isAnimating || this._isLoading || !!this._activeOperationToken || !!(this.animationQueue && this.animationQueue.isBusy());
|
|
2896
3070
|
if (isInCropMode) {
|
|
2897
3071
|
for (const key of Object.keys(this.elements || {})) {
|
|
2898
3072
|
const element = this._getElement(key);
|
|
@@ -2905,23 +3079,23 @@ var ImageEditor = class {
|
|
|
2905
3079
|
}
|
|
2906
3080
|
return;
|
|
2907
3081
|
}
|
|
2908
|
-
this._setDisabled("zoomInBtn", !hasImage ||
|
|
2909
|
-
this._setDisabled("zoomOutBtn", !hasImage ||
|
|
2910
|
-
this._setDisabled("rotateLeftBtn", !hasImage ||
|
|
2911
|
-
this._setDisabled("rotateRightBtn", !hasImage ||
|
|
2912
|
-
this._setDisabled("addMaskBtn", !hasImage ||
|
|
2913
|
-
this._setDisabled("removeMaskBtn", !hasSelectedMask ||
|
|
2914
|
-
this._setDisabled("removeAllMasksBtn", !hasMasks ||
|
|
2915
|
-
this._setDisabled("mergeBtn", !hasImage || !hasMasks ||
|
|
2916
|
-
this._setDisabled("downloadBtn", !hasImage ||
|
|
2917
|
-
this._setDisabled("resetBtn", !hasImage || isDefaultTransform ||
|
|
2918
|
-
this._setDisabled("undoBtn", !hasImage ||
|
|
2919
|
-
this._setDisabled("redoBtn", !hasImage ||
|
|
2920
|
-
this._setDisabled("cropBtn", !hasImage ||
|
|
3082
|
+
this._setDisabled("zoomInBtn", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3083
|
+
this._setDisabled("zoomOutBtn", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3084
|
+
this._setDisabled("rotateLeftBtn", !hasImage || isBusy);
|
|
3085
|
+
this._setDisabled("rotateRightBtn", !hasImage || isBusy);
|
|
3086
|
+
this._setDisabled("addMaskBtn", !hasImage || isBusy);
|
|
3087
|
+
this._setDisabled("removeMaskBtn", !hasSelectedMask || isBusy);
|
|
3088
|
+
this._setDisabled("removeAllMasksBtn", !hasMasks || isBusy);
|
|
3089
|
+
this._setDisabled("mergeBtn", !hasImage || !hasMasks || isBusy);
|
|
3090
|
+
this._setDisabled("downloadBtn", !hasImage || isBusy);
|
|
3091
|
+
this._setDisabled("resetBtn", !hasImage || isDefaultTransform || isBusy);
|
|
3092
|
+
this._setDisabled("undoBtn", !hasImage || isBusy || !canUndo);
|
|
3093
|
+
this._setDisabled("redoBtn", !hasImage || isBusy || !canRedo);
|
|
3094
|
+
this._setDisabled("cropBtn", !hasImage || isBusy);
|
|
2921
3095
|
this._setDisabled("applyCropBtn", true);
|
|
2922
3096
|
this._setDisabled("cancelCropBtn", true);
|
|
2923
|
-
this._setDisabled("imageInput",
|
|
2924
|
-
this._setDisabled("uploadArea",
|
|
3097
|
+
this._setDisabled("imageInput", isBusy);
|
|
3098
|
+
this._setDisabled("uploadArea", isBusy);
|
|
2925
3099
|
}
|
|
2926
3100
|
/**
|
|
2927
3101
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
@@ -2937,12 +3111,16 @@ var ImageEditor = class {
|
|
|
2937
3111
|
element.disabled = !!disabled;
|
|
2938
3112
|
return;
|
|
2939
3113
|
}
|
|
3114
|
+
if (!this._elementOriginalPointerEvents) this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3115
|
+
if (!this._elementOriginalPointerEvents.has(key)) {
|
|
3116
|
+
this._elementOriginalPointerEvents.set(key, element.style.pointerEvents || "");
|
|
3117
|
+
}
|
|
2940
3118
|
if (disabled) {
|
|
2941
3119
|
element.setAttribute("aria-disabled", "true");
|
|
2942
3120
|
element.style.pointerEvents = "none";
|
|
2943
3121
|
} else {
|
|
2944
3122
|
element.removeAttribute("aria-disabled");
|
|
2945
|
-
element.style.pointerEvents = "";
|
|
3123
|
+
element.style.pointerEvents = this._elementOriginalPointerEvents.get(key) ?? "";
|
|
2946
3124
|
}
|
|
2947
3125
|
}
|
|
2948
3126
|
_isElementDisabled(element) {
|
|
@@ -3028,6 +3206,9 @@ var ImageEditor = class {
|
|
|
3028
3206
|
if (this.animationQueue) {
|
|
3029
3207
|
this.animationQueue.cancelAll(new Error("Editor disposed"));
|
|
3030
3208
|
}
|
|
3209
|
+
this._isLoading = false;
|
|
3210
|
+
this._activeOperationName = null;
|
|
3211
|
+
this._activeOperationToken = null;
|
|
3031
3212
|
try {
|
|
3032
3213
|
for (const [key, handlers] of Object.entries(this._handlersByElementKey || {})) {
|
|
3033
3214
|
const element = this._getElement(key);
|
|
@@ -3089,17 +3270,20 @@ var ImageEditor = class {
|
|
|
3089
3270
|
}
|
|
3090
3271
|
this._handlersByElementKey = {};
|
|
3091
3272
|
this._elementCache = {};
|
|
3273
|
+
this._elementOriginalPointerEvents = /* @__PURE__ */ new Map();
|
|
3092
3274
|
this._clearMaskPlacementMemory();
|
|
3093
3275
|
this.originalImage = null;
|
|
3094
3276
|
this.baseImageScale = 1;
|
|
3095
3277
|
this.currentScale = 1;
|
|
3096
3278
|
this.currentRotation = 0;
|
|
3097
3279
|
this.isAnimating = false;
|
|
3280
|
+
this._isLoading = false;
|
|
3098
3281
|
this._cropMode = false;
|
|
3099
3282
|
this._cropRect = null;
|
|
3100
3283
|
this._cropHandlers = [];
|
|
3101
3284
|
this._cropPrevEvented = null;
|
|
3102
3285
|
this._prevSelectionSetting = void 0;
|
|
3286
|
+
this._lastContainerViewportSize = null;
|
|
3103
3287
|
this._initialized = false;
|
|
3104
3288
|
}
|
|
3105
3289
|
};
|
|
@@ -3111,6 +3295,7 @@ var AnimationQueue = class {
|
|
|
3111
3295
|
this.animationTasks = [];
|
|
3112
3296
|
this.isRunning = false;
|
|
3113
3297
|
this.currentTask = null;
|
|
3298
|
+
this._generation = 0;
|
|
3114
3299
|
}
|
|
3115
3300
|
/**
|
|
3116
3301
|
* Adds an animation function to the queue.
|
|
@@ -3130,6 +3315,7 @@ var AnimationQueue = class {
|
|
|
3130
3315
|
return this.isRunning || this.animationTasks.length > 0;
|
|
3131
3316
|
}
|
|
3132
3317
|
cancelAll(reason = new Error("Animation queue cancelled")) {
|
|
3318
|
+
this._generation += 1;
|
|
3133
3319
|
const cancellationError = reason instanceof Error ? reason : new Error(String(reason));
|
|
3134
3320
|
const tasks = [
|
|
3135
3321
|
...this.currentTask ? [this.currentTask] : [],
|
|
@@ -3151,26 +3337,33 @@ var AnimationQueue = class {
|
|
|
3151
3337
|
*/
|
|
3152
3338
|
async _drainQueue() {
|
|
3153
3339
|
if (this.isRunning) return;
|
|
3340
|
+
const generation = this._generation;
|
|
3154
3341
|
this.isRunning = true;
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
task.isSettled
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
task.isSettled
|
|
3167
|
-
|
|
3342
|
+
try {
|
|
3343
|
+
while (this.animationTasks.length > 0 && generation === this._generation) {
|
|
3344
|
+
const task = this.animationTasks.shift();
|
|
3345
|
+
this.currentTask = task;
|
|
3346
|
+
try {
|
|
3347
|
+
const result = await task.animationFn();
|
|
3348
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3349
|
+
task.isSettled = true;
|
|
3350
|
+
task.resolve(result);
|
|
3351
|
+
}
|
|
3352
|
+
} catch (error) {
|
|
3353
|
+
if (generation === this._generation && !task.isSettled) {
|
|
3354
|
+
task.isSettled = true;
|
|
3355
|
+
task.reject(error);
|
|
3356
|
+
}
|
|
3357
|
+
} finally {
|
|
3358
|
+
if (generation === this._generation && this.currentTask === task) this.currentTask = null;
|
|
3168
3359
|
}
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3360
|
+
}
|
|
3361
|
+
} finally {
|
|
3362
|
+
if (generation === this._generation) {
|
|
3363
|
+
this.isRunning = false;
|
|
3364
|
+
this.currentTask = null;
|
|
3171
3365
|
}
|
|
3172
3366
|
}
|
|
3173
|
-
this.isRunning = false;
|
|
3174
3367
|
}
|
|
3175
3368
|
};
|
|
3176
3369
|
var Command = class {
|