@bensitu/image-editor 1.4.2 → 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 +479 -130
- package/dist/image-editor.cjs +4185 -0
- package/dist/image-editor.cjs.map +7 -0
- package/dist/image-editor.esm.js +868 -377
- 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 +868 -377
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +868 -377
- 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 +64 -22
- package/package.json +4 -3
- package/src/image-editor.js +818 -267
|
@@ -5,7 +5,7 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.
|
|
8
|
+
* @version 1.5.1
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -77,6 +77,7 @@ var ImageEditor = class {
|
|
|
77
77
|
downsampleMimeType: null,
|
|
78
78
|
imageLoadTimeoutMs: 3e4,
|
|
79
79
|
exportMultiplier: 1,
|
|
80
|
+
maxExportPixels: 5e7,
|
|
80
81
|
exportImageAreaByDefault: true,
|
|
81
82
|
defaultMaskWidth: 50,
|
|
82
83
|
defaultMaskHeight: 80,
|
|
@@ -146,7 +147,9 @@ var ImageEditor = class {
|
|
|
146
147
|
this._activeAnimationRejectors = /* @__PURE__ */ new Set();
|
|
147
148
|
this._disposed = false;
|
|
148
149
|
this._initialized = false;
|
|
149
|
-
this.
|
|
150
|
+
this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
151
|
+
this._cropRotationWarningEmitted = false;
|
|
152
|
+
this.onImageLoaded = typeof this.options.onImageLoaded === "function" ? this.options.onImageLoaded : null;
|
|
150
153
|
this.animationQueue = new AnimationQueue();
|
|
151
154
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
152
155
|
}
|
|
@@ -192,10 +195,12 @@ var ImageEditor = class {
|
|
|
192
195
|
* Use this method to set up the editor UI before interacting with it.
|
|
193
196
|
*
|
|
194
197
|
* @param {Object} [idMap={}] - Optional mapping from logical element names to actual DOM element IDs.
|
|
195
|
-
* Supported keys include: canvas, canvasContainer,
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
198
|
+
* Supported keys include: canvas, canvasContainer, imagePlaceholder, scalePercentageInput,
|
|
199
|
+
* rotateLeftDegreesInput, rotateRightDegreesInput, rotateLeftButton, rotateRightButton,
|
|
200
|
+
* createMaskButton, removeSelectedMaskButton, removeAllMasksButton, mergeMasksButton,
|
|
201
|
+
* downloadImageButton, maskList, zoomInButton, zoomOutButton, resetImageTransformButton,
|
|
202
|
+
* undoButton, redoButton, imageInput, uploadArea, enterCropModeButton, applyCropButton,
|
|
203
|
+
* and cancelCropButton. Deprecated 1.x names remain supported as aliases.
|
|
199
204
|
*
|
|
200
205
|
* @returns {void}
|
|
201
206
|
*
|
|
@@ -204,11 +209,17 @@ var ImageEditor = class {
|
|
|
204
209
|
* @example
|
|
205
210
|
* editor.init({
|
|
206
211
|
* canvas: 'myFabricCanvasId',
|
|
207
|
-
*
|
|
212
|
+
* downloadImageButton: 'myDownloadButtonId'
|
|
208
213
|
* });
|
|
209
214
|
*/
|
|
210
215
|
init(idMap = {}) {
|
|
211
|
-
if (!this._fabricLoaded)
|
|
216
|
+
if (!this._fabricLoaded) {
|
|
217
|
+
this._fabricLoaded = !!ensureFabric();
|
|
218
|
+
if (!this._fabricLoaded) {
|
|
219
|
+
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
212
223
|
if (this._initialized || this.canvas) this.dispose();
|
|
213
224
|
this._disposed = false;
|
|
214
225
|
this._initialized = true;
|
|
@@ -227,29 +238,49 @@ var ImageEditor = class {
|
|
|
227
238
|
canvas: "fabricCanvas",
|
|
228
239
|
canvasContainer: null,
|
|
229
240
|
// Pass an ID here if you have a scrollable viewport container
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
imagePlaceholder: "imagePlaceholder",
|
|
242
|
+
imgPlaceholder: null,
|
|
243
|
+
scalePercentageInput: "scalePercentageInput",
|
|
244
|
+
scaleRate: null,
|
|
245
|
+
rotateLeftDegreesInput: "rotateLeftDegreesInput",
|
|
246
|
+
rotationLeftInput: null,
|
|
247
|
+
rotateRightDegreesInput: "rotateRightDegreesInput",
|
|
248
|
+
rotationRightInput: null,
|
|
249
|
+
rotateLeftButton: "rotateLeftButton",
|
|
250
|
+
rotateLeftBtn: null,
|
|
251
|
+
rotateRightButton: "rotateRightButton",
|
|
252
|
+
rotateRightBtn: null,
|
|
253
|
+
createMaskButton: "createMaskButton",
|
|
254
|
+
addMaskBtn: null,
|
|
255
|
+
removeSelectedMaskButton: "removeSelectedMaskButton",
|
|
256
|
+
removeMaskBtn: null,
|
|
257
|
+
removeAllMasksButton: "removeAllMasksButton",
|
|
258
|
+
removeAllMasksBtn: null,
|
|
259
|
+
mergeMasksButton: "mergeMasksButton",
|
|
260
|
+
mergeBtn: null,
|
|
261
|
+
downloadImageButton: "downloadImageButton",
|
|
262
|
+
downloadBtn: null,
|
|
241
263
|
maskList: "maskList",
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
264
|
+
zoomInButton: "zoomInButton",
|
|
265
|
+
zoomInBtn: null,
|
|
266
|
+
zoomOutButton: "zoomOutButton",
|
|
267
|
+
zoomOutBtn: null,
|
|
268
|
+
resetImageTransformButton: "resetImageTransformButton",
|
|
269
|
+
resetBtn: null,
|
|
270
|
+
undoButton: "undoButton",
|
|
271
|
+
undoBtn: null,
|
|
272
|
+
redoButton: "redoButton",
|
|
273
|
+
redoBtn: null,
|
|
247
274
|
imageInput: "imageInput",
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
275
|
+
uploadArea: null,
|
|
276
|
+
enterCropModeButton: "enterCropModeButton",
|
|
277
|
+
cropBtn: null,
|
|
278
|
+
applyCropButton: "applyCropButton",
|
|
279
|
+
applyCropBtn: null,
|
|
280
|
+
cancelCropButton: "cancelCropButton",
|
|
281
|
+
cancelCropBtn: null
|
|
251
282
|
};
|
|
252
|
-
this.elements = {
|
|
283
|
+
this.elements = this._resolveElementIdMap(idMap || {}, defaults);
|
|
253
284
|
this._elementCache = {};
|
|
254
285
|
this._initCanvas();
|
|
255
286
|
this._bindEvents();
|
|
@@ -257,11 +288,68 @@ var ImageEditor = class {
|
|
|
257
288
|
this._updateMaskList();
|
|
258
289
|
this._updateUI();
|
|
259
290
|
if (this.options.initialImageBase64) {
|
|
260
|
-
this.loadImage(this.options.initialImageBase64);
|
|
291
|
+
this.loadImage(this.options.initialImageBase64).catch((error) => this._reportError("initialImageBase64 could not be loaded", error));
|
|
261
292
|
} else {
|
|
262
293
|
this._updatePlaceholderStatus();
|
|
263
294
|
}
|
|
264
295
|
}
|
|
296
|
+
_resolveElementIdMap(idMap, defaults) {
|
|
297
|
+
const resolved = { ...defaults, ...idMap };
|
|
298
|
+
this._resolveElementAliases(resolved, idMap, defaults, "imagePlaceholder", ["imgPlaceholder"]);
|
|
299
|
+
this._resolveElementAliases(resolved, idMap, defaults, "scalePercentageInput", ["scaleRate"]);
|
|
300
|
+
this._resolveElementAliases(resolved, idMap, defaults, "rotateLeftDegreesInput", ["rotationLeftInput"]);
|
|
301
|
+
this._resolveElementAliases(resolved, idMap, defaults, "rotateRightDegreesInput", ["rotationRightInput"]);
|
|
302
|
+
this._resolveElementAlias(resolved, idMap, defaults, "rotateLeftButton", "rotateLeftBtn");
|
|
303
|
+
this._resolveElementAlias(resolved, idMap, defaults, "rotateRightButton", "rotateRightBtn");
|
|
304
|
+
this._resolveElementAlias(resolved, idMap, defaults, "createMaskButton", "addMaskBtn");
|
|
305
|
+
this._resolveElementAliases(resolved, idMap, defaults, "removeSelectedMaskButton", ["removeMaskBtn"]);
|
|
306
|
+
this._resolveElementAlias(resolved, idMap, defaults, "removeAllMasksButton", "removeAllMasksBtn");
|
|
307
|
+
this._resolveElementAlias(resolved, idMap, defaults, "mergeMasksButton", "mergeBtn");
|
|
308
|
+
this._resolveElementAliases(resolved, idMap, defaults, "downloadImageButton", ["downloadBtn"]);
|
|
309
|
+
this._resolveElementAlias(resolved, idMap, defaults, "zoomInButton", "zoomInBtn");
|
|
310
|
+
this._resolveElementAlias(resolved, idMap, defaults, "zoomOutButton", "zoomOutBtn");
|
|
311
|
+
this._resolveElementAlias(resolved, idMap, defaults, "resetImageTransformButton", "resetBtn");
|
|
312
|
+
this._resolveElementAlias(resolved, idMap, defaults, "undoButton", "undoBtn");
|
|
313
|
+
this._resolveElementAlias(resolved, idMap, defaults, "redoButton", "redoBtn");
|
|
314
|
+
this._resolveElementAliases(resolved, idMap, defaults, "enterCropModeButton", ["cropBtn"]);
|
|
315
|
+
this._resolveElementAlias(resolved, idMap, defaults, "applyCropButton", "applyCropBtn");
|
|
316
|
+
this._resolveElementAlias(resolved, idMap, defaults, "cancelCropButton", "cancelCropBtn");
|
|
317
|
+
return resolved;
|
|
318
|
+
}
|
|
319
|
+
_resolveElementAlias(resolved, idMap, defaults, canonicalKey, deprecatedKey) {
|
|
320
|
+
this._resolveElementAliases(resolved, idMap, defaults, canonicalKey, [deprecatedKey]);
|
|
321
|
+
}
|
|
322
|
+
_resolveElementAliases(resolved, idMap, defaults, canonicalKey, deprecatedKeys) {
|
|
323
|
+
const hasCanonicalKey = Object.prototype.hasOwnProperty.call(idMap, canonicalKey);
|
|
324
|
+
if (hasCanonicalKey) {
|
|
325
|
+
resolved[canonicalKey] = idMap[canonicalKey];
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
let deprecatedValue;
|
|
329
|
+
let hasDeprecatedValue = false;
|
|
330
|
+
for (const deprecatedKey of deprecatedKeys) {
|
|
331
|
+
if (Object.prototype.hasOwnProperty.call(idMap, deprecatedKey)) {
|
|
332
|
+
if (!hasDeprecatedValue) {
|
|
333
|
+
deprecatedValue = idMap[deprecatedKey];
|
|
334
|
+
hasDeprecatedValue = true;
|
|
335
|
+
}
|
|
336
|
+
this._warnDeprecatedElementIdKey(deprecatedKey, canonicalKey);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (hasDeprecatedValue) {
|
|
340
|
+
resolved[canonicalKey] = deprecatedValue;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
resolved[canonicalKey] = defaults[canonicalKey];
|
|
344
|
+
}
|
|
345
|
+
_warnDeprecatedElementIdKey(deprecatedKey, canonicalKey) {
|
|
346
|
+
if (!this._deprecatedElementKeyWarnings) this._deprecatedElementKeyWarnings = /* @__PURE__ */ new Set();
|
|
347
|
+
if (this._deprecatedElementKeyWarnings.has(deprecatedKey)) return;
|
|
348
|
+
this._deprecatedElementKeyWarnings.add(deprecatedKey);
|
|
349
|
+
this._reportWarning(
|
|
350
|
+
`ElementIdMap.${deprecatedKey} is deprecated. Use ${canonicalKey} instead. This alias will be removed in v2.0.0.`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
265
353
|
_reportError(message, error = null) {
|
|
266
354
|
const handler = this.options && this.options.onError;
|
|
267
355
|
if (typeof handler !== "function") return;
|
|
@@ -278,6 +366,11 @@ var ImageEditor = class {
|
|
|
278
366
|
} catch {
|
|
279
367
|
}
|
|
280
368
|
}
|
|
369
|
+
_notifyImageLoaded() {
|
|
370
|
+
const optionsCallback = this.options && this.options.onImageLoaded;
|
|
371
|
+
const callback = typeof optionsCallback === "function" ? optionsCallback : this.onImageLoaded;
|
|
372
|
+
if (typeof callback === "function") callback();
|
|
373
|
+
}
|
|
281
374
|
/**
|
|
282
375
|
* Initializes the Fabric canvas, viewport elements, and selection event handlers.
|
|
283
376
|
*
|
|
@@ -300,7 +393,7 @@ var ImageEditor = class {
|
|
|
300
393
|
} else {
|
|
301
394
|
this.containerElement = canvasElement.parentElement;
|
|
302
395
|
}
|
|
303
|
-
this.placeholderElement = this._getElement("
|
|
396
|
+
this.placeholderElement = this._getElement("imagePlaceholder") || null;
|
|
304
397
|
let initialWidth = this.options.canvasWidth;
|
|
305
398
|
let initialHeight = this.options.canvasHeight;
|
|
306
399
|
if (this.containerElement) {
|
|
@@ -396,13 +489,14 @@ var ImageEditor = class {
|
|
|
396
489
|
if (!this.containerElement || !this.containerElement.style) return;
|
|
397
490
|
this._captureContainerOverflowState();
|
|
398
491
|
const shouldPreserveScroll = options.preserveScroll === true;
|
|
399
|
-
|
|
492
|
+
const layoutMode = this._getImageLayoutMode();
|
|
493
|
+
if (layoutMode === "cover") {
|
|
400
494
|
this.containerElement.style.overflow = "scroll";
|
|
401
495
|
if (!shouldPreserveScroll) {
|
|
402
496
|
this.containerElement.scrollLeft = 0;
|
|
403
497
|
this.containerElement.scrollTop = 0;
|
|
404
498
|
}
|
|
405
|
-
} else if (
|
|
499
|
+
} else if (layoutMode === "fit") {
|
|
406
500
|
this.containerElement.style.overflow = "auto";
|
|
407
501
|
if (!shouldPreserveScroll) {
|
|
408
502
|
this.containerElement.scrollLeft = 0;
|
|
@@ -450,20 +544,20 @@ var ImageEditor = class {
|
|
|
450
544
|
});
|
|
451
545
|
}
|
|
452
546
|
});
|
|
453
|
-
this._bindIfExists("
|
|
454
|
-
this._bindIfExists("
|
|
455
|
-
this._bindIfExists("
|
|
547
|
+
this._bindIfExists("zoomInButton", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
548
|
+
this._bindIfExists("zoomOutButton", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep).catch((error) => this._reportError("scaleImage failed", error)));
|
|
549
|
+
this._bindIfExists("resetImageTransformButton", "click", () => {
|
|
456
550
|
this.resetImageTransform().catch((error) => this._reportError("resetImageTransform failed", error));
|
|
457
551
|
});
|
|
458
|
-
this._bindIfExists("
|
|
459
|
-
this._bindIfExists("
|
|
460
|
-
this._bindIfExists("
|
|
461
|
-
this._bindIfExists("
|
|
462
|
-
this._bindIfExists("
|
|
463
|
-
this._bindIfExists("
|
|
464
|
-
this._bindIfExists("
|
|
465
|
-
this._bindIfExists("
|
|
466
|
-
const rotationInputElement = this._getElement("
|
|
552
|
+
this._bindIfExists("createMaskButton", "click", () => this.createMask());
|
|
553
|
+
this._bindIfExists("removeSelectedMaskButton", "click", () => this.removeSelectedMask());
|
|
554
|
+
this._bindIfExists("removeAllMasksButton", "click", () => this.removeAllMasks());
|
|
555
|
+
this._bindIfExists("mergeMasksButton", "click", () => this.mergeMasks().catch((error) => this._reportError("merge error", error)));
|
|
556
|
+
this._bindIfExists("downloadImageButton", "click", () => this.downloadImage());
|
|
557
|
+
this._bindIfExists("undoButton", "click", () => this.undo().catch((error) => this._reportError("undo failed", error)));
|
|
558
|
+
this._bindIfExists("redoButton", "click", () => this.redo().catch((error) => this._reportError("redo failed", error)));
|
|
559
|
+
this._bindIfExists("rotateLeftButton", "click", () => {
|
|
560
|
+
const rotationInputElement = this._getElement("rotateLeftDegreesInput");
|
|
467
561
|
let step = this.options.rotationStep;
|
|
468
562
|
if (rotationInputElement) {
|
|
469
563
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -471,8 +565,8 @@ var ImageEditor = class {
|
|
|
471
565
|
}
|
|
472
566
|
this.rotateImage(this.currentRotation - step).catch((error) => this._reportError("rotateImage failed", error));
|
|
473
567
|
});
|
|
474
|
-
this._bindIfExists("
|
|
475
|
-
const rotationInputElement = this._getElement("
|
|
568
|
+
this._bindIfExists("rotateRightButton", "click", () => {
|
|
569
|
+
const rotationInputElement = this._getElement("rotateRightDegreesInput");
|
|
476
570
|
let step = this.options.rotationStep;
|
|
477
571
|
if (rotationInputElement) {
|
|
478
572
|
const parsedStep = parseFloat(rotationInputElement.value);
|
|
@@ -480,11 +574,11 @@ var ImageEditor = class {
|
|
|
480
574
|
}
|
|
481
575
|
this.rotateImage(this.currentRotation + step).catch((error) => this._reportError("rotateImage failed", error));
|
|
482
576
|
});
|
|
483
|
-
this._bindIfExists("
|
|
484
|
-
this._bindIfExists("
|
|
577
|
+
this._bindIfExists("enterCropModeButton", "click", () => this.enterCropMode());
|
|
578
|
+
this._bindIfExists("applyCropButton", "click", () => {
|
|
485
579
|
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
486
580
|
});
|
|
487
|
-
this._bindIfExists("
|
|
581
|
+
this._bindIfExists("cancelCropButton", "click", () => this.cancelCrop());
|
|
488
582
|
this._bindIfExists("maskList", "click", (event) => this._handleMaskListClick(event));
|
|
489
583
|
}
|
|
490
584
|
/**
|
|
@@ -553,6 +647,12 @@ var ImageEditor = class {
|
|
|
553
647
|
`Only one image layout mode should be enabled. Active modes: ${activeModes.join(", ")}.`
|
|
554
648
|
);
|
|
555
649
|
}
|
|
650
|
+
_getImageLayoutMode() {
|
|
651
|
+
if (this.options.fitImageToCanvas) return "fit";
|
|
652
|
+
if (this.options.coverImageToCanvas) return "cover";
|
|
653
|
+
if (this.options.expandCanvasToImage) return "expand";
|
|
654
|
+
return "contain";
|
|
655
|
+
}
|
|
556
656
|
/**
|
|
557
657
|
* Loads a base64 data URL into the Fabric canvas as the base image.
|
|
558
658
|
*
|
|
@@ -566,12 +666,16 @@ var ImageEditor = class {
|
|
|
566
666
|
if (!this._fabricLoaded) return;
|
|
567
667
|
if (!this.canvas || this._disposed) return;
|
|
568
668
|
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/")) return;
|
|
669
|
+
options = options || {};
|
|
569
670
|
this._assertIdleForOperation("loadImage", options);
|
|
570
|
-
|
|
571
|
-
this.
|
|
572
|
-
|
|
573
|
-
const transaction = this._captureLoadImageTransaction();
|
|
671
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
672
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("loadImage");
|
|
673
|
+
let transaction = null;
|
|
574
674
|
try {
|
|
675
|
+
this._isLoading = true;
|
|
676
|
+
this._updateUI();
|
|
677
|
+
this._warnOnImageLayoutOptionConflict();
|
|
678
|
+
transaction = this._captureLoadImageTransaction();
|
|
575
679
|
const imageElement = await this._createImageElement(imageBase64);
|
|
576
680
|
if (this._disposed || !this.canvas) throw new Error("Editor was disposed while loading image");
|
|
577
681
|
let loadSource = imageBase64;
|
|
@@ -611,7 +715,8 @@ var ImageEditor = class {
|
|
|
611
715
|
const viewport = this._getContainerViewportSize();
|
|
612
716
|
const minWidth = viewport.width;
|
|
613
717
|
const minHeight = viewport.height;
|
|
614
|
-
|
|
718
|
+
const layoutMode = this._getImageLayoutMode();
|
|
719
|
+
if (layoutMode === "fit") {
|
|
615
720
|
const canvasWidth = Math.max(1, minWidth - 1);
|
|
616
721
|
const canvasHeight = Math.max(1, minHeight - 1);
|
|
617
722
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -619,13 +724,13 @@ var ImageEditor = class {
|
|
|
619
724
|
fabricImage.set({ left: 0, top: 0 });
|
|
620
725
|
fabricImage.scale(fitScale);
|
|
621
726
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
622
|
-
} else if (
|
|
727
|
+
} else if (layoutMode === "cover") {
|
|
623
728
|
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
624
729
|
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
625
730
|
fabricImage.set({ left: 0, top: 0 });
|
|
626
731
|
fabricImage.scale(layout.scale);
|
|
627
732
|
this.baseImageScale = fabricImage.scaleX || 1;
|
|
628
|
-
} else if (
|
|
733
|
+
} else if (layoutMode === "expand") {
|
|
629
734
|
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
630
735
|
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
631
736
|
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
@@ -654,14 +759,16 @@ var ImageEditor = class {
|
|
|
654
759
|
this._updateUI();
|
|
655
760
|
this.canvas.renderAll();
|
|
656
761
|
this._lastSnapshot = this._captureCanvasStateOrThrow("loadImage");
|
|
657
|
-
|
|
658
|
-
this.onImageLoaded();
|
|
659
|
-
}
|
|
762
|
+
this._notifyImageLoaded();
|
|
660
763
|
} catch (error) {
|
|
661
|
-
await this._rollbackLoadImageTransaction(
|
|
764
|
+
await this._rollbackLoadImageTransaction(
|
|
765
|
+
transaction,
|
|
766
|
+
this._withInternalOperationOptions(operationToken)
|
|
767
|
+
);
|
|
662
768
|
throw error;
|
|
663
769
|
} finally {
|
|
664
770
|
this._isLoading = false;
|
|
771
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
665
772
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
666
773
|
}
|
|
667
774
|
}
|
|
@@ -709,7 +816,7 @@ var ImageEditor = class {
|
|
|
709
816
|
try {
|
|
710
817
|
imageElement.src = "";
|
|
711
818
|
} catch (error) {
|
|
712
|
-
|
|
819
|
+
this._reportWarning("Image timeout cleanup failed", error);
|
|
713
820
|
}
|
|
714
821
|
}, safeTimeoutMs);
|
|
715
822
|
imageElement.onload = () => settle(() => resolve(imageElement));
|
|
@@ -774,32 +881,38 @@ var ImageEditor = class {
|
|
|
774
881
|
canvasVisibility: this._captureElementVisibility(this._getCanvasVisibilityElement())
|
|
775
882
|
};
|
|
776
883
|
}
|
|
777
|
-
async _rollbackLoadImageTransaction(transaction) {
|
|
884
|
+
async _rollbackLoadImageTransaction(transaction, options = {}) {
|
|
778
885
|
if (!transaction || !this.canvas || this._disposed) return;
|
|
779
886
|
let didRestoreCanvasState = false;
|
|
887
|
+
let didFailCanvasRestore = false;
|
|
780
888
|
try {
|
|
781
889
|
if (transaction.canvasState) {
|
|
782
|
-
await this.loadFromState(transaction.canvasState);
|
|
890
|
+
await this.loadFromState(transaction.canvasState, options);
|
|
783
891
|
didRestoreCanvasState = true;
|
|
784
892
|
}
|
|
785
893
|
} catch (error) {
|
|
786
894
|
this._lastMask = null;
|
|
895
|
+
didFailCanvasRestore = true;
|
|
787
896
|
this._reportError("loadImage rollback failed", error);
|
|
788
897
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
this.currentRotation = transaction.currentRotation;
|
|
792
|
-
this.maskCounter = transaction.maskCounter;
|
|
793
|
-
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
794
|
-
this._lastSnapshot = transaction.lastSnapshot;
|
|
795
|
-
if (didRestoreCanvasState) {
|
|
796
|
-
this._restoreLastMaskReference(transaction.lastMask);
|
|
898
|
+
if (didFailCanvasRestore) {
|
|
899
|
+
this._reconcileEditorStateFromCanvas();
|
|
797
900
|
} else {
|
|
798
|
-
this.
|
|
901
|
+
this.baseImageScale = transaction.baseImageScale;
|
|
902
|
+
this.currentScale = transaction.currentScale;
|
|
903
|
+
this.currentRotation = transaction.currentRotation;
|
|
904
|
+
this.maskCounter = transaction.maskCounter;
|
|
905
|
+
this.isImageLoadedToCanvas = transaction.isImageLoadedToCanvas;
|
|
906
|
+
this._lastSnapshot = transaction.lastSnapshot;
|
|
907
|
+
if (didRestoreCanvasState) {
|
|
908
|
+
this._restoreLastMaskReference(transaction.lastMask);
|
|
909
|
+
} else {
|
|
910
|
+
this._lastMask = null;
|
|
911
|
+
}
|
|
912
|
+
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
913
|
+
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
914
|
+
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
799
915
|
}
|
|
800
|
-
this._lastMaskInitialLeft = transaction.lastMaskInitialLeft;
|
|
801
|
-
this._lastMaskInitialTop = transaction.lastMaskInitialTop;
|
|
802
|
-
this._lastMaskInitialWidth = transaction.lastMaskInitialWidth;
|
|
803
916
|
this._restoreElementVisibility(this.placeholderElement, transaction.placeholderVisibility);
|
|
804
917
|
this._restoreElementVisibility(this._getCanvasVisibilityElement(), transaction.canvasVisibility);
|
|
805
918
|
if (this.containerElement) {
|
|
@@ -812,6 +925,46 @@ var ImageEditor = class {
|
|
|
812
925
|
this._updateUI();
|
|
813
926
|
if (this.canvas) this.canvas.renderAll();
|
|
814
927
|
}
|
|
928
|
+
_reconcileEditorStateFromCanvas() {
|
|
929
|
+
if (!this.canvas) {
|
|
930
|
+
this.originalImage = null;
|
|
931
|
+
this.baseImageScale = 1;
|
|
932
|
+
this.currentScale = 1;
|
|
933
|
+
this.currentRotation = 0;
|
|
934
|
+
this.maskCounter = 0;
|
|
935
|
+
this.isImageLoadedToCanvas = false;
|
|
936
|
+
this._lastSnapshot = null;
|
|
937
|
+
this._clearMaskPlacementMemory();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const canvasObjects = this.canvas.getObjects();
|
|
941
|
+
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
942
|
+
if (this.originalImage) {
|
|
943
|
+
const imageScale = Number(this.originalImage.scaleX) || 1;
|
|
944
|
+
this.baseImageScale = imageScale;
|
|
945
|
+
this.currentScale = 1;
|
|
946
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
947
|
+
} else {
|
|
948
|
+
this.baseImageScale = 1;
|
|
949
|
+
this.currentScale = 1;
|
|
950
|
+
this.currentRotation = 0;
|
|
951
|
+
}
|
|
952
|
+
const masks = canvasObjects.filter((object) => object.maskId);
|
|
953
|
+
this.maskCounter = masks.reduce((max, mask) => Math.max(max, Number(mask.maskId) || 0), 0);
|
|
954
|
+
this._lastMask = masks[masks.length - 1] || null;
|
|
955
|
+
if (!this._lastMask) {
|
|
956
|
+
this._lastMaskInitialLeft = null;
|
|
957
|
+
this._lastMaskInitialTop = null;
|
|
958
|
+
this._lastMaskInitialWidth = null;
|
|
959
|
+
}
|
|
960
|
+
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
961
|
+
try {
|
|
962
|
+
this._lastSnapshot = this._serializeCanvasState();
|
|
963
|
+
} catch (error) {
|
|
964
|
+
this._lastSnapshot = null;
|
|
965
|
+
this._reportWarning("loadImage rollback: failed to reconcile canvas snapshot", error);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
815
968
|
_restoreLastMaskReference(previousLastMask) {
|
|
816
969
|
if (!this.canvas) {
|
|
817
970
|
this._lastMask = null;
|
|
@@ -882,6 +1035,7 @@ var ImageEditor = class {
|
|
|
882
1035
|
* @private
|
|
883
1036
|
*/
|
|
884
1037
|
_setCanvasSizeInt(width, height) {
|
|
1038
|
+
if (!this.canvas) return;
|
|
885
1039
|
const integerWidth = Math.max(1, Math.round(Number(width) || 1));
|
|
886
1040
|
const integerHeight = Math.max(1, Math.round(Number(height) || 1));
|
|
887
1041
|
this.canvas.setWidth(integerWidth);
|
|
@@ -983,9 +1137,9 @@ var ImageEditor = class {
|
|
|
983
1137
|
}
|
|
984
1138
|
_getScrollableCanvasSize(contentWidth, contentHeight, viewport = this._getContainerViewportSize()) {
|
|
985
1139
|
if (this._hasFixedContainerScrollbars()) {
|
|
986
|
-
const
|
|
987
|
-
const safeWidth = Math.max(1, viewport.width -
|
|
988
|
-
const safeHeight = Math.max(1, viewport.height -
|
|
1140
|
+
const safetyMargin2 = this._getScrollSafetyMargin();
|
|
1141
|
+
const safeWidth = Math.max(1, viewport.width - safetyMargin2);
|
|
1142
|
+
const safeHeight = Math.max(1, viewport.height - safetyMargin2);
|
|
989
1143
|
return {
|
|
990
1144
|
width: contentWidth > viewport.width + 0.5 ? this._ceilCanvasDimension(contentWidth) : safeWidth,
|
|
991
1145
|
height: contentHeight > viewport.height + 0.5 ? this._ceilCanvasDimension(contentHeight) : safeHeight,
|
|
@@ -1011,9 +1165,17 @@ var ImageEditor = class {
|
|
|
1011
1165
|
}
|
|
1012
1166
|
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
1013
1167
|
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
1168
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
1169
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1170
|
+
const shouldReserveNoScrollbarMargin = layoutMode === "fit" || layoutMode === "cover";
|
|
1171
|
+
const getNonOverflowAxisSize = (contentSize, effectiveSize, hasOppositeScrollbar) => {
|
|
1172
|
+
const margin = hasOppositeScrollbar ? safetyMargin : shouldReserveNoScrollbarMargin ? 1 : 0;
|
|
1173
|
+
const safeEffectiveSize = Math.max(1, effectiveSize - margin);
|
|
1174
|
+
return contentSize <= safeEffectiveSize + 0.5 ? safeEffectiveSize : effectiveSize;
|
|
1175
|
+
};
|
|
1014
1176
|
return {
|
|
1015
|
-
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
1016
|
-
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
1177
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : getNonOverflowAxisSize(contentWidth, effectiveWidth, hasVertical),
|
|
1178
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : getNonOverflowAxisSize(contentHeight, effectiveHeight, hasHorizontal),
|
|
1017
1179
|
viewportWidth: effectiveWidth,
|
|
1018
1180
|
viewportHeight: effectiveHeight,
|
|
1019
1181
|
hasHorizontal,
|
|
@@ -1135,6 +1297,45 @@ var ImageEditor = class {
|
|
|
1135
1297
|
});
|
|
1136
1298
|
}
|
|
1137
1299
|
}
|
|
1300
|
+
_getSerializableStateObjects() {
|
|
1301
|
+
if (!this.canvas) return [];
|
|
1302
|
+
return this.canvas.getObjects().filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1303
|
+
}
|
|
1304
|
+
_restoreHighPrecisionSerializedGeometry(serializedObjects) {
|
|
1305
|
+
if (!Array.isArray(serializedObjects)) return;
|
|
1306
|
+
const fabricObjects = this._getSerializableStateObjects();
|
|
1307
|
+
const numericProperties = [
|
|
1308
|
+
"left",
|
|
1309
|
+
"top",
|
|
1310
|
+
"width",
|
|
1311
|
+
"height",
|
|
1312
|
+
"scaleX",
|
|
1313
|
+
"scaleY",
|
|
1314
|
+
"angle",
|
|
1315
|
+
"skewX",
|
|
1316
|
+
"skewY",
|
|
1317
|
+
"cropX",
|
|
1318
|
+
"cropY",
|
|
1319
|
+
"radius",
|
|
1320
|
+
"rx",
|
|
1321
|
+
"ry",
|
|
1322
|
+
"strokeWidth"
|
|
1323
|
+
];
|
|
1324
|
+
serializedObjects.forEach((serializedObject, index) => {
|
|
1325
|
+
const fabricObject = fabricObjects[index];
|
|
1326
|
+
if (!serializedObject || !fabricObject) return;
|
|
1327
|
+
numericProperties.forEach((property) => {
|
|
1328
|
+
const numericValue = Number(fabricObject[property]);
|
|
1329
|
+
if (Number.isFinite(numericValue)) serializedObject[property] = numericValue;
|
|
1330
|
+
});
|
|
1331
|
+
if (Array.isArray(serializedObject.points) && Array.isArray(fabricObject.points)) {
|
|
1332
|
+
serializedObject.points = fabricObject.points.map((point) => ({
|
|
1333
|
+
x: Number.isFinite(Number(point && point.x)) ? Number(point.x) : 0,
|
|
1334
|
+
y: Number.isFinite(Number(point && point.y)) ? Number(point.y) : 0
|
|
1335
|
+
}));
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1138
1339
|
_restoreMaskControls(mask) {
|
|
1139
1340
|
if (!mask) return;
|
|
1140
1341
|
const cornerSize = Number(mask.cornerSize);
|
|
@@ -1154,7 +1355,7 @@ var ImageEditor = class {
|
|
|
1154
1355
|
/**
|
|
1155
1356
|
* Captures editor-owned runtime state that Fabric does not include in canvas JSON.
|
|
1156
1357
|
*
|
|
1157
|
-
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number}} Serializable editor metadata.
|
|
1358
|
+
* @returns {{version:number, baseImageScale:number, currentScale:number, currentRotation:number, maskCounter:number, canvasWidth:number, canvasHeight:number}} Serializable editor metadata.
|
|
1158
1359
|
* @private
|
|
1159
1360
|
*/
|
|
1160
1361
|
_serializeEditorMetadata() {
|
|
@@ -1162,12 +1363,16 @@ var ImageEditor = class {
|
|
|
1162
1363
|
const currentScale = Number(this.currentScale);
|
|
1163
1364
|
const currentRotation = Number(this.currentRotation);
|
|
1164
1365
|
const maskCounter = Number(this.maskCounter);
|
|
1366
|
+
const canvasWidth = this.canvas ? Number(this.canvas.getWidth()) : NaN;
|
|
1367
|
+
const canvasHeight = this.canvas ? Number(this.canvas.getHeight()) : NaN;
|
|
1165
1368
|
return {
|
|
1166
1369
|
version: 1,
|
|
1167
1370
|
baseImageScale: Number.isFinite(baseImageScale) && baseImageScale > 0 ? baseImageScale : 1,
|
|
1168
1371
|
currentScale: Number.isFinite(currentScale) && currentScale > 0 ? currentScale : 1,
|
|
1169
1372
|
currentRotation: Number.isFinite(currentRotation) ? currentRotation : 0,
|
|
1170
|
-
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0
|
|
1373
|
+
maskCounter: Number.isFinite(maskCounter) && maskCounter > 0 ? Math.floor(maskCounter) : 0,
|
|
1374
|
+
canvasWidth: Number.isFinite(canvasWidth) && canvasWidth > 0 ? Math.round(canvasWidth) : 1,
|
|
1375
|
+
canvasHeight: Number.isFinite(canvasHeight) && canvasHeight > 0 ? Math.round(canvasHeight) : 1
|
|
1171
1376
|
};
|
|
1172
1377
|
}
|
|
1173
1378
|
_serializeCanvasState() {
|
|
@@ -1176,6 +1381,7 @@ var ImageEditor = class {
|
|
|
1176
1381
|
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
1177
1382
|
if (Array.isArray(jsonObject.objects)) {
|
|
1178
1383
|
jsonObject.objects = jsonObject.objects.filter((object) => !object.isCropRect && !object.maskLabel);
|
|
1384
|
+
this._restoreHighPrecisionSerializedGeometry(jsonObject.objects);
|
|
1179
1385
|
}
|
|
1180
1386
|
jsonObject.imageEditorMetadata = this._serializeEditorMetadata();
|
|
1181
1387
|
return JSON.stringify(jsonObject);
|
|
@@ -1250,6 +1456,12 @@ var ImageEditor = class {
|
|
|
1250
1456
|
if (!Number.isFinite(numericValue)) return false;
|
|
1251
1457
|
return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
|
|
1252
1458
|
}
|
|
1459
|
+
_hasScaledImageEdge(axis) {
|
|
1460
|
+
if (!this.originalImage) return false;
|
|
1461
|
+
const scale = Number(axis === "y" ? this.originalImage.scaleY : this.originalImage.scaleX);
|
|
1462
|
+
if (!Number.isFinite(scale)) return false;
|
|
1463
|
+
return Math.abs(scale - 1) > 0.01;
|
|
1464
|
+
}
|
|
1253
1465
|
_getPartialExportEdges(bounds) {
|
|
1254
1466
|
if (!bounds) return null;
|
|
1255
1467
|
const angle = Math.abs((Number(this.originalImage && this.originalImage.angle) || 0) % 90);
|
|
@@ -1258,8 +1470,8 @@ var ImageEditor = class {
|
|
|
1258
1470
|
return {
|
|
1259
1471
|
left: this._hasFractionalCanvasEdge(bounds.left),
|
|
1260
1472
|
top: this._hasFractionalCanvasEdge(bounds.top),
|
|
1261
|
-
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)),
|
|
1262
|
-
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0))
|
|
1473
|
+
right: this._hasFractionalCanvasEdge((Number(bounds.left) || 0) + (Number(bounds.width) || 0)) || this._hasScaledImageEdge("x"),
|
|
1474
|
+
bottom: this._hasFractionalCanvasEdge((Number(bounds.top) || 0) + (Number(bounds.height) || 0)) || this._hasScaledImageEdge("y")
|
|
1263
1475
|
};
|
|
1264
1476
|
}
|
|
1265
1477
|
async _sealPartialTransparentEdges(dataUrl, edges) {
|
|
@@ -1319,7 +1531,8 @@ var ImageEditor = class {
|
|
|
1319
1531
|
* @private
|
|
1320
1532
|
*/
|
|
1321
1533
|
async _exportCanvasRegionToDataURL({ sourceX, sourceY, sourceWidth, sourceHeight, multiplier = 1, quality = 0.92, format = "jpeg", sealPartialEdges = null }) {
|
|
1322
|
-
const safeMultiplier =
|
|
1534
|
+
const safeMultiplier = this._getSafeExportMultiplier(multiplier);
|
|
1535
|
+
this._assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier);
|
|
1323
1536
|
const safeFormat = this._normalizeImageFormat(format);
|
|
1324
1537
|
const exportFormat = safeFormat === "jpeg" ? "png" : safeFormat;
|
|
1325
1538
|
let regionDataUrl = this.canvas.toDataURL({
|
|
@@ -1335,6 +1548,25 @@ var ImageEditor = class {
|
|
|
1335
1548
|
if (safeFormat !== "jpeg") return regionDataUrl;
|
|
1336
1549
|
return this._convertDataUrlToOpaqueJpeg(regionDataUrl, quality);
|
|
1337
1550
|
}
|
|
1551
|
+
_getSafeExportMultiplier(multiplier) {
|
|
1552
|
+
const numericMultiplier = Number(multiplier);
|
|
1553
|
+
if (!Number.isFinite(numericMultiplier) || numericMultiplier <= 0) {
|
|
1554
|
+
throw new Error("Export multiplier must be a finite positive number");
|
|
1555
|
+
}
|
|
1556
|
+
return Math.max(1, numericMultiplier);
|
|
1557
|
+
}
|
|
1558
|
+
_assertExportPixelBudget(sourceWidth, sourceHeight, safeMultiplier) {
|
|
1559
|
+
const width = Math.max(1, Math.ceil(Number(sourceWidth) || 1));
|
|
1560
|
+
const height = Math.max(1, Math.ceil(Number(sourceHeight) || 1));
|
|
1561
|
+
const outputWidth = Math.ceil(width * safeMultiplier);
|
|
1562
|
+
const outputHeight = Math.ceil(height * safeMultiplier);
|
|
1563
|
+
const outputPixels = outputWidth * outputHeight;
|
|
1564
|
+
const configuredMaxPixels = Number(this.options.maxExportPixels);
|
|
1565
|
+
const maxPixels = Number.isFinite(configuredMaxPixels) && configuredMaxPixels > 0 ? Math.floor(configuredMaxPixels) : 5e7;
|
|
1566
|
+
if (outputPixels > maxPixels) {
|
|
1567
|
+
throw new Error(`Export would create ${outputPixels} pixels, exceeding the configured maxExportPixels limit of ${maxPixels}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1338
1570
|
async _convertDataUrlToOpaqueJpeg(dataUrl, quality = 0.92) {
|
|
1339
1571
|
const imageElement = await this._createImageElement(dataUrl);
|
|
1340
1572
|
const width = Math.max(1, imageElement.naturalWidth || imageElement.width || 1);
|
|
@@ -1379,6 +1611,7 @@ var ImageEditor = class {
|
|
|
1379
1611
|
}
|
|
1380
1612
|
_decodeBase64Payload(base64Payload) {
|
|
1381
1613
|
const payload = String(base64Payload || "");
|
|
1614
|
+
if (!payload) throw new Error("Data URL base64 payload is empty");
|
|
1382
1615
|
if (typeof atob === "function") {
|
|
1383
1616
|
return Uint8Array.from(atob(payload), (char) => char.charCodeAt(0));
|
|
1384
1617
|
}
|
|
@@ -1387,6 +1620,13 @@ var ImageEditor = class {
|
|
|
1387
1620
|
}
|
|
1388
1621
|
throw new Error("Base64 decoding is unavailable");
|
|
1389
1622
|
}
|
|
1623
|
+
_decodeDataUrlPayload(dataUrl) {
|
|
1624
|
+
const match = String(dataUrl || "").match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/i);
|
|
1625
|
+
if (!match || !match[2]) {
|
|
1626
|
+
throw new Error("Export produced an invalid or empty base64 data URL");
|
|
1627
|
+
}
|
|
1628
|
+
return this._decodeBase64Payload(match[2]);
|
|
1629
|
+
}
|
|
1390
1630
|
/**
|
|
1391
1631
|
* Gets the top-left corner coordinates of the given object.
|
|
1392
1632
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
@@ -1496,24 +1736,49 @@ var ImageEditor = class {
|
|
|
1496
1736
|
const currentHeight = this.canvas.getHeight();
|
|
1497
1737
|
let requiredWidth = currentWidth;
|
|
1498
1738
|
let requiredHeight = currentHeight;
|
|
1499
|
-
|
|
1739
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1740
|
+
const usesScrollableFitBounds = layoutMode === "fit" || layoutMode === "cover";
|
|
1741
|
+
let contentWidth = 0;
|
|
1742
|
+
let contentHeight = 0;
|
|
1743
|
+
const includeObjectBounds = (fabricObject, objectPadding = 0) => {
|
|
1500
1744
|
if (!fabricObject) return;
|
|
1501
1745
|
if (typeof fabricObject.setCoords === "function") fabricObject.setCoords();
|
|
1502
1746
|
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
1503
|
-
|
|
1504
|
-
|
|
1747
|
+
const right = Math.ceil(boundingRect.left + boundingRect.width + objectPadding);
|
|
1748
|
+
const bottom = Math.ceil(boundingRect.top + boundingRect.height + objectPadding);
|
|
1749
|
+
contentWidth = Math.max(contentWidth, right);
|
|
1750
|
+
contentHeight = Math.max(contentHeight, bottom);
|
|
1751
|
+
return { right, bottom };
|
|
1752
|
+
};
|
|
1753
|
+
fabricObjects.forEach((fabricObject) => {
|
|
1754
|
+
const bounds = includeObjectBounds(fabricObject, padding);
|
|
1755
|
+
if (!bounds) return;
|
|
1756
|
+
requiredWidth = Math.max(requiredWidth, bounds.right);
|
|
1757
|
+
requiredHeight = Math.max(requiredHeight, bounds.bottom);
|
|
1505
1758
|
});
|
|
1506
|
-
|
|
1759
|
+
if (usesScrollableFitBounds) {
|
|
1760
|
+
if (this.originalImage) includeObjectBounds(this.originalImage, 0);
|
|
1761
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1762
|
+
if (object && object.maskId) includeObjectBounds(object, padding);
|
|
1763
|
+
});
|
|
1764
|
+
const contentSize = this._getScrollableCanvasSize(
|
|
1765
|
+
Math.max(1, contentWidth),
|
|
1766
|
+
Math.max(1, contentHeight)
|
|
1767
|
+
);
|
|
1768
|
+
const newWidth2 = contentSize.hasHorizontal ? Math.max(currentWidth, contentSize.width) : contentSize.width;
|
|
1769
|
+
const newHeight2 = contentSize.hasVertical ? Math.max(currentHeight, contentSize.height) : contentSize.height;
|
|
1770
|
+
if (newWidth2 !== currentWidth || newHeight2 !== currentHeight) {
|
|
1771
|
+
this._setCanvasSizeInt(newWidth2, newHeight2);
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1507
1775
|
let minWidth = 0;
|
|
1508
1776
|
let minHeight = 0;
|
|
1509
|
-
if (
|
|
1777
|
+
if (this.containerElement) {
|
|
1510
1778
|
const viewport = this._getContainerViewportSize();
|
|
1511
1779
|
const safetyMargin = this._getScrollSafetyMargin();
|
|
1512
1780
|
minWidth = Math.max(1, viewport.width - safetyMargin);
|
|
1513
1781
|
minHeight = Math.max(1, viewport.height - safetyMargin);
|
|
1514
|
-
} else if (this.containerElement) {
|
|
1515
|
-
minWidth = Math.floor(this.containerElement.clientWidth || 0);
|
|
1516
|
-
minHeight = Math.floor(this.containerElement.clientHeight || 0);
|
|
1517
1782
|
}
|
|
1518
1783
|
const newWidth = Math.max(currentWidth, minWidth, requiredWidth);
|
|
1519
1784
|
const newHeight = Math.max(currentHeight, minHeight, requiredHeight);
|
|
@@ -1524,16 +1789,60 @@ var ImageEditor = class {
|
|
|
1524
1789
|
this._reportWarning("expandCanvasToFitObjects: failed to expand canvas", error);
|
|
1525
1790
|
}
|
|
1526
1791
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1792
|
+
_captureImageDisplayBounds() {
|
|
1793
|
+
if (!this.originalImage || !this.canvas) return null;
|
|
1794
|
+
this.originalImage.setCoords();
|
|
1795
|
+
const bounds = this.originalImage.getBoundingRect(true, true);
|
|
1796
|
+
const width = Number(bounds && bounds.width);
|
|
1797
|
+
const height = Number(bounds && bounds.height);
|
|
1798
|
+
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) return null;
|
|
1799
|
+
return {
|
|
1800
|
+
left: Number.isFinite(Number(bounds.left)) ? Number(bounds.left) : 0,
|
|
1801
|
+
top: Number.isFinite(Number(bounds.top)) ? Number(bounds.top) : 0,
|
|
1802
|
+
width,
|
|
1803
|
+
height
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
_restoreImageDisplayBounds(displayBounds) {
|
|
1807
|
+
if (!displayBounds || !this.originalImage || !this.canvas) return;
|
|
1808
|
+
const imageWidth = Number(this.originalImage.width);
|
|
1809
|
+
const imageHeight = Number(this.originalImage.height);
|
|
1810
|
+
if (!Number.isFinite(imageWidth) || imageWidth <= 0 || !Number.isFinite(imageHeight) || imageHeight <= 0) return;
|
|
1811
|
+
const scaleX = Number(displayBounds.width) / imageWidth;
|
|
1812
|
+
const scaleY = Number(displayBounds.height) / imageHeight;
|
|
1813
|
+
if (!Number.isFinite(scaleX) || scaleX <= 0 || !Number.isFinite(scaleY) || scaleY <= 0) return;
|
|
1814
|
+
const left = Number(displayBounds.left) || 0;
|
|
1815
|
+
const top = Number(displayBounds.top) || 0;
|
|
1816
|
+
const requiredCanvasWidth = Math.max(1, Math.ceil(left + Number(displayBounds.width)));
|
|
1817
|
+
const requiredCanvasHeight = Math.max(1, Math.ceil(top + Number(displayBounds.height)));
|
|
1818
|
+
const currentCanvasWidth = Math.max(1, Math.round(Number(this.canvas.getWidth()) || 1));
|
|
1819
|
+
const currentCanvasHeight = Math.max(1, Math.round(Number(this.canvas.getHeight()) || 1));
|
|
1820
|
+
const layoutMode = this._getImageLayoutMode();
|
|
1821
|
+
if (layoutMode === "fit" || layoutMode === "cover") {
|
|
1822
|
+
const contentSize = this._getScrollableCanvasSize(requiredCanvasWidth, requiredCanvasHeight);
|
|
1823
|
+
if (contentSize.width !== currentCanvasWidth || contentSize.height !== currentCanvasHeight) {
|
|
1824
|
+
this._setCanvasSizeInt(contentSize.width, contentSize.height);
|
|
1825
|
+
}
|
|
1826
|
+
} else if (requiredCanvasWidth > currentCanvasWidth || requiredCanvasHeight > currentCanvasHeight) {
|
|
1827
|
+
this._setCanvasSizeInt(
|
|
1828
|
+
Math.max(currentCanvasWidth, requiredCanvasWidth),
|
|
1829
|
+
Math.max(currentCanvasHeight, requiredCanvasHeight)
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
this.originalImage.set({
|
|
1833
|
+
originX: "left",
|
|
1834
|
+
originY: "top",
|
|
1835
|
+
left,
|
|
1836
|
+
top,
|
|
1837
|
+
scaleX,
|
|
1838
|
+
scaleY
|
|
1839
|
+
});
|
|
1840
|
+
this.originalImage.setCoords();
|
|
1841
|
+
this.baseImageScale = scaleX;
|
|
1842
|
+
this.currentScale = 1;
|
|
1843
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1844
|
+
this._updateInputs();
|
|
1845
|
+
this.canvas.renderAll();
|
|
1537
1846
|
}
|
|
1538
1847
|
/**
|
|
1539
1848
|
* Scales the original image by a given factor, with animation.
|
|
@@ -1548,7 +1857,14 @@ var ImageEditor = class {
|
|
|
1548
1857
|
} catch (error) {
|
|
1549
1858
|
return Promise.reject(error);
|
|
1550
1859
|
}
|
|
1551
|
-
return this.animationQueue.add(
|
|
1860
|
+
return this.animationQueue.add(async () => {
|
|
1861
|
+
const operationToken = this._beginBusyOperation("scaleImage");
|
|
1862
|
+
try {
|
|
1863
|
+
await this._scaleImageImpl(factor, this._withInternalOperationOptions(operationToken, options));
|
|
1864
|
+
} finally {
|
|
1865
|
+
this._endBusyOperation(operationToken);
|
|
1866
|
+
}
|
|
1867
|
+
}).finally(() => {
|
|
1552
1868
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1553
1869
|
});
|
|
1554
1870
|
}
|
|
@@ -1582,10 +1898,16 @@ var ImageEditor = class {
|
|
|
1582
1898
|
_assertEditorAvailable(operationName) {
|
|
1583
1899
|
if (this._disposed || !this.canvas) throw new Error(`${operationName} cannot run after the editor has been disposed`);
|
|
1584
1900
|
}
|
|
1901
|
+
_isCropModeAllowedOperation(operationName) {
|
|
1902
|
+
return operationName === "applyCrop" || operationName === "cancelCrop";
|
|
1903
|
+
}
|
|
1585
1904
|
_assertIdleForOperation(operationName, options = {}) {
|
|
1586
1905
|
this._assertEditorAvailable(operationName);
|
|
1587
1906
|
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1588
|
-
if (this.
|
|
1907
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1908
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1909
|
+
}
|
|
1910
|
+
if ((this.isAnimating || this.animationQueue && this.animationQueue.isBusy()) && !isOwnInternalOperation) {
|
|
1589
1911
|
throw new Error(`${operationName} cannot run while an animation is running`);
|
|
1590
1912
|
}
|
|
1591
1913
|
if (this._isLoading && !isOwnInternalOperation) {
|
|
@@ -1597,10 +1919,14 @@ var ImageEditor = class {
|
|
|
1597
1919
|
}
|
|
1598
1920
|
_assertCanQueueAnimation(operationName, options = {}) {
|
|
1599
1921
|
this._assertEditorAvailable(operationName);
|
|
1600
|
-
|
|
1922
|
+
const isOwnInternalOperation = this._isOwnInternalOperation(options);
|
|
1923
|
+
if (this._cropMode && !this._isCropModeAllowedOperation(operationName) && !isOwnInternalOperation) {
|
|
1924
|
+
throw new Error(`${operationName} cannot run while crop mode is active`);
|
|
1925
|
+
}
|
|
1926
|
+
if (this._isLoading && !isOwnInternalOperation) {
|
|
1601
1927
|
throw new Error(`${operationName} cannot run while an image is loading`);
|
|
1602
1928
|
}
|
|
1603
|
-
if (this._activeOperationToken && !
|
|
1929
|
+
if (this._activeOperationToken && !isOwnInternalOperation) {
|
|
1604
1930
|
throw new Error(`${operationName} cannot run while ${this._activeOperationName || "another operation"} is running`);
|
|
1605
1931
|
}
|
|
1606
1932
|
}
|
|
@@ -1694,7 +2020,7 @@ var ImageEditor = class {
|
|
|
1694
2020
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1695
2021
|
});
|
|
1696
2022
|
this._updateInputs();
|
|
1697
|
-
if (saveHistory) this.saveState();
|
|
2023
|
+
if (saveHistory) this.saveState(options);
|
|
1698
2024
|
} finally {
|
|
1699
2025
|
if (didStartAnimation) {
|
|
1700
2026
|
this.isAnimating = false;
|
|
@@ -1716,7 +2042,14 @@ var ImageEditor = class {
|
|
|
1716
2042
|
} catch (error) {
|
|
1717
2043
|
return Promise.reject(error);
|
|
1718
2044
|
}
|
|
1719
|
-
return this.animationQueue.add(
|
|
2045
|
+
return this.animationQueue.add(async () => {
|
|
2046
|
+
const operationToken = this._beginBusyOperation("rotateImage");
|
|
2047
|
+
try {
|
|
2048
|
+
await this._rotateImageImpl(degrees, this._withInternalOperationOptions(operationToken, options));
|
|
2049
|
+
} finally {
|
|
2050
|
+
this._endBusyOperation(operationToken);
|
|
2051
|
+
}
|
|
2052
|
+
}).finally(() => {
|
|
1720
2053
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1721
2054
|
});
|
|
1722
2055
|
}
|
|
@@ -1759,7 +2092,7 @@ var ImageEditor = class {
|
|
|
1759
2092
|
if (object.maskId) this._syncMaskLabel(object);
|
|
1760
2093
|
});
|
|
1761
2094
|
this._updateInputs();
|
|
1762
|
-
if (saveHistory) this.saveState();
|
|
2095
|
+
if (saveHistory) this.saveState(options);
|
|
1763
2096
|
didCompleteRotation = true;
|
|
1764
2097
|
} finally {
|
|
1765
2098
|
if (!didCompleteRotation && !this._disposed && image) {
|
|
@@ -1786,11 +2119,23 @@ var ImageEditor = class {
|
|
|
1786
2119
|
return Promise.reject(error);
|
|
1787
2120
|
}
|
|
1788
2121
|
return this.animationQueue.add(async () => {
|
|
2122
|
+
const operationToken = this._beginBusyOperation("resetImageTransform");
|
|
1789
2123
|
const before = this._lastSnapshot || this._captureCanvasStateOrThrow("resetImageTransform");
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2124
|
+
try {
|
|
2125
|
+
await this._scaleImageImpl(1, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2126
|
+
await this._rotateImageImpl(0, this._withInternalOperationOptions(operationToken, { saveHistory: false }));
|
|
2127
|
+
const after = this._captureCanvasStateOrThrow("resetImageTransform");
|
|
2128
|
+
this._pushStateTransition(before, after);
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
try {
|
|
2131
|
+
await this.loadFromState(before, this._withInternalOperationOptions(operationToken));
|
|
2132
|
+
} catch (restoreError) {
|
|
2133
|
+
this._reportError("resetImageTransform rollback failed", restoreError);
|
|
2134
|
+
}
|
|
2135
|
+
throw error;
|
|
2136
|
+
} finally {
|
|
2137
|
+
this._endBusyOperation(operationToken);
|
|
2138
|
+
}
|
|
1794
2139
|
}).finally(() => {
|
|
1795
2140
|
if (!this._disposed && this.canvas) this._updateUI();
|
|
1796
2141
|
}).catch((error) => {
|
|
@@ -1814,8 +2159,13 @@ var ImageEditor = class {
|
|
|
1814
2159
|
* @returns {Promise<void>} Resolves after Fabric has loaded the state and UI state has been refreshed.
|
|
1815
2160
|
* @public
|
|
1816
2161
|
*/
|
|
1817
|
-
loadFromState(serializedState) {
|
|
2162
|
+
loadFromState(serializedState, options = {}) {
|
|
1818
2163
|
if (!serializedState || !this.canvas || this._disposed) return Promise.resolve();
|
|
2164
|
+
try {
|
|
2165
|
+
this._assertIdleForOperation("loadFromState", options);
|
|
2166
|
+
} catch (error) {
|
|
2167
|
+
return Promise.reject(error);
|
|
2168
|
+
}
|
|
1819
2169
|
if (this._cropMode || this._cropRect) {
|
|
1820
2170
|
this._removeCropRect();
|
|
1821
2171
|
this._restoreCropObjectState();
|
|
@@ -1829,10 +2179,13 @@ var ImageEditor = class {
|
|
|
1829
2179
|
try {
|
|
1830
2180
|
const state = typeof serializedState === "string" ? JSON.parse(serializedState) : serializedState;
|
|
1831
2181
|
const editorMetadata = state && state.imageEditorMetadata ? state.imageEditorMetadata : null;
|
|
2182
|
+
const restoredCanvasWidth = Number(editorMetadata && editorMetadata.canvasWidth);
|
|
2183
|
+
const restoredCanvasHeight = Number(editorMetadata && editorMetadata.canvasHeight);
|
|
2184
|
+
const hasRestoredCanvasSize = Number.isFinite(restoredCanvasWidth) && restoredCanvasWidth > 0 && Number.isFinite(restoredCanvasHeight) && restoredCanvasHeight > 0;
|
|
1832
2185
|
if (editorMetadata && Object.prototype.hasOwnProperty.call(editorMetadata, "version") && Number(editorMetadata.version) !== 1) {
|
|
1833
2186
|
this._reportWarning(`loadFromState: unsupported editor metadata version ${editorMetadata.version}`);
|
|
1834
2187
|
}
|
|
1835
|
-
|
|
2188
|
+
const finishLoad = async () => {
|
|
1836
2189
|
try {
|
|
1837
2190
|
if (this._disposed || !this.canvas) {
|
|
1838
2191
|
reject(new Error("Editor was disposed while loading state"));
|
|
@@ -1868,6 +2221,11 @@ var ImageEditor = class {
|
|
|
1868
2221
|
this.currentScale = 1;
|
|
1869
2222
|
this.currentRotation = 0;
|
|
1870
2223
|
}
|
|
2224
|
+
if (hasRestoredCanvasSize) {
|
|
2225
|
+
this._setCanvasSizeInt(restoredCanvasWidth, restoredCanvasHeight);
|
|
2226
|
+
} else if (this.originalImage && this._shouldResizeCanvasToContentBounds()) {
|
|
2227
|
+
this._updateCanvasSizeToImageBounds();
|
|
2228
|
+
}
|
|
1871
2229
|
const masks = canvasObjects.filter((object) => object.maskId);
|
|
1872
2230
|
masks.forEach((mask) => {
|
|
1873
2231
|
this._restoreMaskControls(mask);
|
|
@@ -1895,6 +2253,9 @@ var ImageEditor = class {
|
|
|
1895
2253
|
this._reportError("loadFromState() failed", callbackError);
|
|
1896
2254
|
reject(callbackError);
|
|
1897
2255
|
}
|
|
2256
|
+
};
|
|
2257
|
+
this.canvas.loadFromJSON(state, () => {
|
|
2258
|
+
void finishLoad();
|
|
1898
2259
|
});
|
|
1899
2260
|
} catch (error) {
|
|
1900
2261
|
this._reportError("loadFromState() failed", error);
|
|
@@ -1961,22 +2322,29 @@ var ImageEditor = class {
|
|
|
1961
2322
|
* @returns {void}
|
|
1962
2323
|
* @public
|
|
1963
2324
|
*/
|
|
1964
|
-
saveState() {
|
|
2325
|
+
saveState(options = {}) {
|
|
1965
2326
|
if (!this.canvas) return;
|
|
2327
|
+
try {
|
|
2328
|
+
this._assertIdleForOperation("saveState", options);
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
this._reportError("saveState blocked", error);
|
|
2331
|
+
this._updateUI();
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
1966
2334
|
try {
|
|
1967
2335
|
const after = this._captureCanvasStateOrThrow("saveState");
|
|
1968
2336
|
const before = this._lastSnapshot || after;
|
|
1969
2337
|
if (after === before) return;
|
|
1970
2338
|
let executedOnce = false;
|
|
1971
2339
|
const command = new Command(
|
|
1972
|
-
() => {
|
|
2340
|
+
(commandOptions = {}) => {
|
|
1973
2341
|
if (executedOnce) {
|
|
1974
|
-
return this.loadFromState(after);
|
|
2342
|
+
return this.loadFromState(after, commandOptions);
|
|
1975
2343
|
}
|
|
1976
2344
|
executedOnce = true;
|
|
1977
2345
|
return void 0;
|
|
1978
2346
|
},
|
|
1979
|
-
() => this.loadFromState(before)
|
|
2347
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
1980
2348
|
);
|
|
1981
2349
|
this.historyManager.execute(command);
|
|
1982
2350
|
this._lastSnapshot = after;
|
|
@@ -2005,8 +2373,8 @@ var ImageEditor = class {
|
|
|
2005
2373
|
if (before === after) return;
|
|
2006
2374
|
if (!this.historyManager) this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
2007
2375
|
const command = new Command(
|
|
2008
|
-
() => this.loadFromState(after),
|
|
2009
|
-
() => this.loadFromState(before)
|
|
2376
|
+
(commandOptions = {}) => this.loadFromState(after, commandOptions),
|
|
2377
|
+
(commandOptions = {}) => this.loadFromState(before, commandOptions)
|
|
2010
2378
|
);
|
|
2011
2379
|
this.historyManager.push(command);
|
|
2012
2380
|
this._lastSnapshot = after;
|
|
@@ -2019,8 +2387,16 @@ var ImageEditor = class {
|
|
|
2019
2387
|
* @public
|
|
2020
2388
|
*/
|
|
2021
2389
|
undo() {
|
|
2022
|
-
|
|
2390
|
+
try {
|
|
2391
|
+
this._assertIdleForOperation("undo");
|
|
2392
|
+
} catch (error) {
|
|
2393
|
+
return Promise.reject(error);
|
|
2394
|
+
}
|
|
2395
|
+
const operationToken = this._beginBusyOperation("undo");
|
|
2396
|
+
return this.historyManager.undo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2023
2397
|
this._updateUI();
|
|
2398
|
+
}).finally(() => {
|
|
2399
|
+
this._endBusyOperation(operationToken);
|
|
2024
2400
|
}).catch((error) => {
|
|
2025
2401
|
this._reportError("undo failed", error);
|
|
2026
2402
|
throw error;
|
|
@@ -2033,8 +2409,16 @@ var ImageEditor = class {
|
|
|
2033
2409
|
* @public
|
|
2034
2410
|
*/
|
|
2035
2411
|
redo() {
|
|
2036
|
-
|
|
2412
|
+
try {
|
|
2413
|
+
this._assertIdleForOperation("redo");
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
return Promise.reject(error);
|
|
2416
|
+
}
|
|
2417
|
+
const operationToken = this._beginBusyOperation("redo");
|
|
2418
|
+
return this.historyManager.redo(this._withInternalOperationOptions(operationToken)).then(() => {
|
|
2037
2419
|
this._updateUI();
|
|
2420
|
+
}).finally(() => {
|
|
2421
|
+
this._endBusyOperation(operationToken);
|
|
2038
2422
|
}).catch((error) => {
|
|
2039
2423
|
this._reportError("redo failed", error);
|
|
2040
2424
|
throw error;
|
|
@@ -2042,14 +2426,7 @@ var ImageEditor = class {
|
|
|
2042
2426
|
}
|
|
2043
2427
|
_rebindMaskEvents(mask) {
|
|
2044
2428
|
if (!mask) return;
|
|
2045
|
-
|
|
2046
|
-
try {
|
|
2047
|
-
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
2048
|
-
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
2049
|
-
} catch (error) {
|
|
2050
|
-
void error;
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2429
|
+
this._cleanupMaskEvents(mask);
|
|
2053
2430
|
const metadata = {};
|
|
2054
2431
|
if (!Number.isFinite(Number(mask.originalAlpha))) {
|
|
2055
2432
|
metadata.originalAlpha = Number.isFinite(Number(mask.opacity)) ? Number(mask.opacity) : 0.5;
|
|
@@ -2076,6 +2453,22 @@ var ImageEditor = class {
|
|
|
2076
2453
|
mask.on("mouseout", mouseout);
|
|
2077
2454
|
mask.__imageEditorMaskHandlers = { mouseover, mouseout };
|
|
2078
2455
|
}
|
|
2456
|
+
_cleanupMaskEvents(mask) {
|
|
2457
|
+
if (!mask || !mask.__imageEditorMaskHandlers) return;
|
|
2458
|
+
try {
|
|
2459
|
+
if (typeof mask.off === "function") {
|
|
2460
|
+
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
2461
|
+
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
2462
|
+
}
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
this._reportWarning("Mask event cleanup failed", error);
|
|
2465
|
+
}
|
|
2466
|
+
try {
|
|
2467
|
+
delete mask.__imageEditorMaskHandlers;
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
this._reportWarning("Mask event metadata cleanup failed", error);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2079
2472
|
/**
|
|
2080
2473
|
* Creates a mask and adds it to the canvas.
|
|
2081
2474
|
*
|
|
@@ -2141,18 +2534,43 @@ var ImageEditor = class {
|
|
|
2141
2534
|
}
|
|
2142
2535
|
return value != null ? value : fallback;
|
|
2143
2536
|
};
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2537
|
+
const rejectInvalidMask = (message, error = null) => {
|
|
2538
|
+
this._reportWarning(`createMask: ${message}`, error);
|
|
2539
|
+
return null;
|
|
2540
|
+
};
|
|
2541
|
+
const resolveNumber = (value, fallback, axis, fieldName, constraints = {}) => {
|
|
2542
|
+
const resolvedValue = resolveValue(value, fallback, axis);
|
|
2543
|
+
const numericValue = Number(resolvedValue);
|
|
2544
|
+
if (!Number.isFinite(numericValue)) {
|
|
2545
|
+
throw new Error(`${fieldName} must be a finite number`);
|
|
2546
|
+
}
|
|
2547
|
+
if (constraints.positive && numericValue <= 0) {
|
|
2548
|
+
throw new Error(`${fieldName} must be greater than 0`);
|
|
2549
|
+
}
|
|
2550
|
+
if (constraints.nonNegative && numericValue < 0) {
|
|
2551
|
+
throw new Error(`${fieldName} must be 0 or greater`);
|
|
2552
|
+
}
|
|
2553
|
+
return numericValue;
|
|
2554
|
+
};
|
|
2555
|
+
try {
|
|
2556
|
+
maskConfig.gap = resolveNumber(maskConfig.gap, 5, "width", "gap", { nonNegative: true });
|
|
2557
|
+
maskConfig.width = resolveNumber(maskConfig.width, this.options.defaultMaskWidth, "width", "width", { positive: true });
|
|
2558
|
+
maskConfig.height = resolveNumber(maskConfig.height, this.options.defaultMaskHeight, "height", "height", { positive: true });
|
|
2559
|
+
maskConfig.angle = resolveNumber(maskConfig.angle, 0, "width", "angle");
|
|
2560
|
+
maskConfig.alpha = Math.max(0, Math.min(1, resolveNumber(maskConfig.alpha, 0.5, "width", "alpha")));
|
|
2561
|
+
if (maskConfig.left === void 0 && this._lastMask) {
|
|
2562
|
+
const previousMask = this._lastMask;
|
|
2563
|
+
if (typeof previousMask.setCoords === "function") previousMask.setCoords();
|
|
2564
|
+
const previousBounds = typeof previousMask.getBoundingRect === "function" ? previousMask.getBoundingRect(true, true) : { left: previousMask.left || firstOffset, top: previousMask.top || firstOffset, width: previousMask.width || 0 };
|
|
2565
|
+
left = Math.round(previousBounds.left + previousBounds.width + maskConfig.gap);
|
|
2566
|
+
top = Math.round(previousBounds.top ?? firstOffset);
|
|
2567
|
+
} else {
|
|
2568
|
+
left = resolveNumber(maskConfig.left, firstOffset, "width", "left");
|
|
2569
|
+
top = resolveNumber(maskConfig.top, firstOffset, "height", "top");
|
|
2570
|
+
}
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
return rejectInvalidMask("invalid numeric configuration", error);
|
|
2153
2573
|
}
|
|
2154
|
-
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth, "width");
|
|
2155
|
-
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight, "height");
|
|
2156
2574
|
maskConfig.left = left;
|
|
2157
2575
|
maskConfig.top = top;
|
|
2158
2576
|
let mask;
|
|
@@ -2161,10 +2579,15 @@ var ImageEditor = class {
|
|
|
2161
2579
|
} else {
|
|
2162
2580
|
switch (shapeType) {
|
|
2163
2581
|
case "circle":
|
|
2582
|
+
try {
|
|
2583
|
+
maskConfig.radius = resolveNumber(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2, "min", "radius", { positive: true });
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
return rejectInvalidMask("invalid circle radius", error);
|
|
2586
|
+
}
|
|
2164
2587
|
mask = new fabric.Circle({
|
|
2165
2588
|
left,
|
|
2166
2589
|
top,
|
|
2167
|
-
radius:
|
|
2590
|
+
radius: maskConfig.radius,
|
|
2168
2591
|
fill: maskConfig.color,
|
|
2169
2592
|
opacity: maskConfig.alpha,
|
|
2170
2593
|
angle: maskConfig.angle,
|
|
@@ -2172,11 +2595,17 @@ var ImageEditor = class {
|
|
|
2172
2595
|
});
|
|
2173
2596
|
break;
|
|
2174
2597
|
case "ellipse":
|
|
2598
|
+
try {
|
|
2599
|
+
maskConfig.rx = resolveNumber(maskConfig.rx, maskConfig.width / 2, "width", "rx", { positive: true });
|
|
2600
|
+
maskConfig.ry = resolveNumber(maskConfig.ry, maskConfig.height / 2, "height", "ry", { positive: true });
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
return rejectInvalidMask("invalid ellipse radius", error);
|
|
2603
|
+
}
|
|
2175
2604
|
mask = new fabric.Ellipse({
|
|
2176
2605
|
left,
|
|
2177
2606
|
top,
|
|
2178
|
-
rx:
|
|
2179
|
-
ry:
|
|
2607
|
+
rx: maskConfig.rx,
|
|
2608
|
+
ry: maskConfig.ry,
|
|
2180
2609
|
fill: maskConfig.color,
|
|
2181
2610
|
opacity: maskConfig.alpha,
|
|
2182
2611
|
angle: maskConfig.angle,
|
|
@@ -2185,8 +2614,20 @@ var ImageEditor = class {
|
|
|
2185
2614
|
break;
|
|
2186
2615
|
case "polygon": {
|
|
2187
2616
|
let polygonPoints = maskConfig.points || [];
|
|
2188
|
-
if (Array.isArray(polygonPoints)
|
|
2189
|
-
|
|
2617
|
+
if (!Array.isArray(polygonPoints) || polygonPoints.length < 3) {
|
|
2618
|
+
return rejectInvalidMask("polygon masks require at least three points");
|
|
2619
|
+
}
|
|
2620
|
+
try {
|
|
2621
|
+
polygonPoints = polygonPoints.map((point) => {
|
|
2622
|
+
const x = Number(Array.isArray(point) ? point[0] : point.x);
|
|
2623
|
+
const y = Number(Array.isArray(point) ? point[1] : point.y);
|
|
2624
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
2625
|
+
throw new Error("polygon point coordinates must be finite numbers");
|
|
2626
|
+
}
|
|
2627
|
+
return { x, y };
|
|
2628
|
+
});
|
|
2629
|
+
} catch (error) {
|
|
2630
|
+
return rejectInvalidMask("invalid polygon points", error);
|
|
2190
2631
|
}
|
|
2191
2632
|
mask = new fabric.Polygon(polygonPoints, {
|
|
2192
2633
|
left,
|
|
@@ -2200,11 +2641,17 @@ var ImageEditor = class {
|
|
|
2200
2641
|
}
|
|
2201
2642
|
case "rect":
|
|
2202
2643
|
default:
|
|
2644
|
+
try {
|
|
2645
|
+
if (maskConfig.rx != null) maskConfig.rx = resolveNumber(maskConfig.rx, 0, "width", "rx", { nonNegative: true });
|
|
2646
|
+
if (maskConfig.ry != null) maskConfig.ry = resolveNumber(maskConfig.ry, 0, "height", "ry", { nonNegative: true });
|
|
2647
|
+
} catch (error) {
|
|
2648
|
+
return rejectInvalidMask("invalid rectangle corner radius", error);
|
|
2649
|
+
}
|
|
2203
2650
|
mask = new fabric.Rect({
|
|
2204
2651
|
left,
|
|
2205
2652
|
top,
|
|
2206
|
-
width:
|
|
2207
|
-
height:
|
|
2653
|
+
width: maskConfig.width,
|
|
2654
|
+
height: maskConfig.height,
|
|
2208
2655
|
fill: maskConfig.color,
|
|
2209
2656
|
opacity: maskConfig.alpha,
|
|
2210
2657
|
angle: maskConfig.angle,
|
|
@@ -2242,10 +2689,10 @@ var ImageEditor = class {
|
|
|
2242
2689
|
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
2243
2690
|
});
|
|
2244
2691
|
this._rebindMaskEvents(mask);
|
|
2245
|
-
this.
|
|
2692
|
+
this._expandCanvasToFitObjects([mask]);
|
|
2246
2693
|
this._lastMaskInitialLeft = left;
|
|
2247
2694
|
this._lastMaskInitialTop = top;
|
|
2248
|
-
this._lastMaskInitialWidth =
|
|
2695
|
+
this._lastMaskInitialWidth = maskConfig.width;
|
|
2249
2696
|
const maskId = ++this.maskCounter;
|
|
2250
2697
|
mask.set({
|
|
2251
2698
|
maskId,
|
|
@@ -2286,6 +2733,7 @@ var ImageEditor = class {
|
|
|
2286
2733
|
this.canvas.discardActiveObject();
|
|
2287
2734
|
selectedMasks.forEach((mask) => {
|
|
2288
2735
|
this._removeLabelForMask(mask);
|
|
2736
|
+
this._cleanupMaskEvents(mask);
|
|
2289
2737
|
this.canvas.remove(mask);
|
|
2290
2738
|
});
|
|
2291
2739
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
@@ -2310,7 +2758,10 @@ var ImageEditor = class {
|
|
|
2310
2758
|
const saveHistory = options.saveHistory !== false;
|
|
2311
2759
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2312
2760
|
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2313
|
-
masks.forEach((mask) =>
|
|
2761
|
+
masks.forEach((mask) => {
|
|
2762
|
+
this._cleanupMaskEvents(mask);
|
|
2763
|
+
this.canvas.remove(mask);
|
|
2764
|
+
});
|
|
2314
2765
|
this.canvas.discardActiveObject();
|
|
2315
2766
|
this._lastMask = null;
|
|
2316
2767
|
this._lastMaskInitialLeft = null;
|
|
@@ -2379,7 +2830,7 @@ var ImageEditor = class {
|
|
|
2379
2830
|
if (backup.labelInCanvas) this.canvas.bringToFront(backup.label);
|
|
2380
2831
|
this._syncMaskLabel(backup.mask);
|
|
2381
2832
|
} catch (error) {
|
|
2382
|
-
|
|
2833
|
+
this._reportWarning("restoreMaskLabelBackups: failed to restore mask label", error);
|
|
2383
2834
|
}
|
|
2384
2835
|
});
|
|
2385
2836
|
}
|
|
@@ -2510,7 +2961,6 @@ var ImageEditor = class {
|
|
|
2510
2961
|
try {
|
|
2511
2962
|
if (canvasObjectSet.has(label)) {
|
|
2512
2963
|
this.canvas.remove(label);
|
|
2513
|
-
canvasObjectSet.delete(label);
|
|
2514
2964
|
}
|
|
2515
2965
|
} catch (error) {
|
|
2516
2966
|
void error;
|
|
@@ -2670,6 +3120,7 @@ var ImageEditor = class {
|
|
|
2670
3120
|
this._assertIdleForOperation("mergeMasks");
|
|
2671
3121
|
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2672
3122
|
if (!masks.length) return;
|
|
3123
|
+
const beforeImageDisplayBounds = this._captureImageDisplayBounds();
|
|
2673
3124
|
const beforeJson = this._serializeCanvasState();
|
|
2674
3125
|
const operationToken = this._beginBusyOperation("mergeMasks");
|
|
2675
3126
|
this.canvas.discardActiveObject();
|
|
@@ -2688,12 +3139,13 @@ var ImageEditor = class {
|
|
|
2688
3139
|
preserveScroll: true,
|
|
2689
3140
|
resetMaskCounter: false
|
|
2690
3141
|
}));
|
|
3142
|
+
this._restoreImageDisplayBounds(beforeImageDisplayBounds);
|
|
2691
3143
|
const afterJson = this._serializeCanvasState();
|
|
2692
3144
|
this._pushStateTransition(beforeJson, afterJson);
|
|
2693
3145
|
} catch (error) {
|
|
2694
3146
|
this._reportError("merge error", error);
|
|
2695
3147
|
try {
|
|
2696
|
-
await this.loadFromState(beforeJson);
|
|
3148
|
+
await this.loadFromState(beforeJson, this._withInternalOperationOptions(operationToken));
|
|
2697
3149
|
} catch (restoreError) {
|
|
2698
3150
|
this._reportError("mergeMasks rollback failed", restoreError);
|
|
2699
3151
|
}
|
|
@@ -2750,24 +3202,65 @@ var ImageEditor = class {
|
|
|
2750
3202
|
*/
|
|
2751
3203
|
async exportImageBase64(options = {}) {
|
|
2752
3204
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
3205
|
+
options = options || {};
|
|
2753
3206
|
this._assertIdleForOperation("exportImageBase64", options);
|
|
3207
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3208
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageBase64");
|
|
2754
3209
|
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
2755
3210
|
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
2756
3211
|
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
2757
3212
|
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
3213
|
+
try {
|
|
3214
|
+
if (!exportImageArea) {
|
|
3215
|
+
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
3216
|
+
const editableMasks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3217
|
+
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
3218
|
+
const maskStyleBackups2 = this._captureMaskExportBackups(editableMasks);
|
|
3219
|
+
const labelBackups2 = this._captureMaskLabelBackups(editableMasks);
|
|
3220
|
+
const activeObjectBackup2 = this._captureActiveObjectBackup();
|
|
3221
|
+
try {
|
|
3222
|
+
masks2.forEach((mask) => {
|
|
3223
|
+
mask.set({ visible: false });
|
|
3224
|
+
});
|
|
3225
|
+
this.canvas.discardActiveObject();
|
|
3226
|
+
this.canvas.renderAll();
|
|
3227
|
+
this.originalImage.setCoords();
|
|
3228
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
3229
|
+
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
3230
|
+
return await this._exportCanvasRegionToDataURL({
|
|
3231
|
+
...exportRegion,
|
|
3232
|
+
multiplier,
|
|
3233
|
+
quality,
|
|
3234
|
+
format,
|
|
3235
|
+
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
3236
|
+
});
|
|
3237
|
+
} finally {
|
|
3238
|
+
maskVisibilityBackups.forEach((backup) => {
|
|
3239
|
+
try {
|
|
3240
|
+
backup.object.set({ visible: backup.visible });
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
void error;
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
3246
|
+
this._restoreMaskLabelBackups(labelBackups2);
|
|
3247
|
+
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3248
|
+
this.canvas.renderAll();
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3252
|
+
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
3253
|
+
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
3254
|
+
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2765
3255
|
try {
|
|
2766
|
-
|
|
2767
|
-
mask.set({ visible: false });
|
|
2768
|
-
});
|
|
3256
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2769
3257
|
this.canvas.discardActiveObject();
|
|
2770
3258
|
this.canvas.renderAll();
|
|
3259
|
+
masks.forEach((mask) => {
|
|
3260
|
+
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
3261
|
+
mask.setCoords();
|
|
3262
|
+
});
|
|
3263
|
+
this.canvas.renderAll();
|
|
2771
3264
|
this.originalImage.setCoords();
|
|
2772
3265
|
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2773
3266
|
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
@@ -2779,50 +3272,14 @@ var ImageEditor = class {
|
|
|
2779
3272
|
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2780
3273
|
});
|
|
2781
3274
|
} finally {
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
} catch (error) {
|
|
2786
|
-
void error;
|
|
2787
|
-
}
|
|
2788
|
-
});
|
|
2789
|
-
this._restoreMaskExportBackups(maskStyleBackups2);
|
|
2790
|
-
this._restoreMaskLabelBackups(labelBackups2);
|
|
2791
|
-
this._restoreActiveObjectBackup(activeObjectBackup2);
|
|
3275
|
+
this._restoreMaskExportBackups(maskStyleBackups);
|
|
3276
|
+
this._restoreMaskLabelBackups(labelBackups);
|
|
3277
|
+
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2792
3278
|
this.canvas.renderAll();
|
|
2793
3279
|
}
|
|
2794
|
-
}
|
|
2795
|
-
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
2796
|
-
const maskStyleBackups = this._captureMaskExportBackups(masks);
|
|
2797
|
-
const labelBackups = this._captureMaskLabelBackups(masks);
|
|
2798
|
-
const activeObjectBackup = this._captureActiveObjectBackup();
|
|
2799
|
-
let finalBase64;
|
|
2800
|
-
try {
|
|
2801
|
-
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
2802
|
-
this.canvas.discardActiveObject();
|
|
2803
|
-
this.canvas.renderAll();
|
|
2804
|
-
masks.forEach((mask) => {
|
|
2805
|
-
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
2806
|
-
mask.setCoords();
|
|
2807
|
-
});
|
|
2808
|
-
this.canvas.renderAll();
|
|
2809
|
-
this.originalImage.setCoords();
|
|
2810
|
-
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
2811
|
-
const exportRegion = this._getClampedCanvasRegion(imageBounds);
|
|
2812
|
-
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
2813
|
-
...exportRegion,
|
|
2814
|
-
multiplier,
|
|
2815
|
-
quality,
|
|
2816
|
-
format,
|
|
2817
|
-
sealPartialEdges: this._getPartialExportEdges(imageBounds)
|
|
2818
|
-
});
|
|
2819
3280
|
} finally {
|
|
2820
|
-
this.
|
|
2821
|
-
this._restoreMaskLabelBackups(labelBackups);
|
|
2822
|
-
this._restoreActiveObjectBackup(activeObjectBackup);
|
|
2823
|
-
this.canvas.renderAll();
|
|
3281
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2824
3282
|
}
|
|
2825
|
-
return finalBase64;
|
|
2826
3283
|
}
|
|
2827
3284
|
/**
|
|
2828
3285
|
* Backward-compatible alias for {@link ImageEditor#exportImageBase64}.
|
|
@@ -2854,7 +3311,10 @@ var ImageEditor = class {
|
|
|
2854
3311
|
*/
|
|
2855
3312
|
async exportImageFile(options = {}) {
|
|
2856
3313
|
if (!this.originalImage) throw new Error("No image loaded");
|
|
2857
|
-
|
|
3314
|
+
options = options || {};
|
|
3315
|
+
this._assertIdleForOperation("exportImageFile", options);
|
|
3316
|
+
const isNestedOperation = this._isOwnInternalOperation(options);
|
|
3317
|
+
const operationToken = isNestedOperation ? this._getInternalOperationToken(options) : this._beginBusyOperation("exportImageFile");
|
|
2858
3318
|
const {
|
|
2859
3319
|
mergeMask = true,
|
|
2860
3320
|
fileType = "jpeg",
|
|
@@ -2864,48 +3324,52 @@ var ImageEditor = class {
|
|
|
2864
3324
|
} = options;
|
|
2865
3325
|
const safeFileType = this._normalizeImageFormat(fileType);
|
|
2866
3326
|
const normalizedQuality = this._normalizeQuality(quality);
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3327
|
+
try {
|
|
3328
|
+
let imageBase64;
|
|
3329
|
+
if (mergeMask) {
|
|
3330
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3331
|
+
exportImageArea: true,
|
|
3332
|
+
multiplier,
|
|
3333
|
+
quality: normalizedQuality,
|
|
3334
|
+
fileType: safeFileType
|
|
3335
|
+
}));
|
|
3336
|
+
} else {
|
|
3337
|
+
imageBase64 = await this.exportImageBase64(this._withInternalOperationOptions(operationToken, {
|
|
3338
|
+
exportImageArea: false,
|
|
3339
|
+
multiplier,
|
|
3340
|
+
quality: normalizedQuality,
|
|
3341
|
+
fileType: safeFileType
|
|
3342
|
+
}));
|
|
3343
|
+
}
|
|
3344
|
+
let imageDataUrl = imageBase64;
|
|
3345
|
+
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
3346
|
+
imageDataUrl = await new Promise((resolve, reject) => {
|
|
3347
|
+
const imageElement = new window.Image();
|
|
3348
|
+
imageElement.crossOrigin = "Anonymous";
|
|
3349
|
+
imageElement.onload = () => {
|
|
3350
|
+
try {
|
|
3351
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
3352
|
+
offscreenCanvas.width = imageElement.width;
|
|
3353
|
+
offscreenCanvas.height = imageElement.height;
|
|
3354
|
+
const context = offscreenCanvas.getContext("2d");
|
|
3355
|
+
if (!context) throw new Error("Unable to create 2D canvas context for export conversion");
|
|
3356
|
+
context.drawImage(imageElement, 0, 0);
|
|
3357
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, normalizedQuality);
|
|
3358
|
+
resolve(convertedDataUrl);
|
|
3359
|
+
} catch (error) {
|
|
3360
|
+
reject(error);
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
imageElement.onerror = reject;
|
|
3364
|
+
imageElement.src = imageBase64;
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
const bytes = this._decodeDataUrlPayload(imageDataUrl);
|
|
3368
|
+
const mime = `image/${safeFileType}`;
|
|
3369
|
+
return new File([bytes], fileName, { type: mime });
|
|
3370
|
+
} finally {
|
|
3371
|
+
if (!isNestedOperation) this._endBusyOperation(operationToken);
|
|
2905
3372
|
}
|
|
2906
|
-
const bytes = this._decodeBase64Payload(imageDataUrl.split(",")[1]);
|
|
2907
|
-
const mime = `image/${safeFileType}`;
|
|
2908
|
-
return new File([bytes], fileName, { type: mime });
|
|
2909
3373
|
}
|
|
2910
3374
|
_clearMaskPlacementMemory() {
|
|
2911
3375
|
this._lastMask = null;
|
|
@@ -2913,7 +3377,7 @@ var ImageEditor = class {
|
|
|
2913
3377
|
this._lastMaskInitialTop = null;
|
|
2914
3378
|
this._lastMaskInitialWidth = null;
|
|
2915
3379
|
}
|
|
2916
|
-
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
3380
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error, options = {}) {
|
|
2917
3381
|
this._reportError(message, error);
|
|
2918
3382
|
if (this._cropRect && this.canvas) this._removeCropRect();
|
|
2919
3383
|
this._cropRect = null;
|
|
@@ -2924,7 +3388,7 @@ var ImageEditor = class {
|
|
|
2924
3388
|
this._prevSelectionSetting = void 0;
|
|
2925
3389
|
if (beforeJson) {
|
|
2926
3390
|
try {
|
|
2927
|
-
await this.loadFromState(beforeJson);
|
|
3391
|
+
await this.loadFromState(beforeJson, options);
|
|
2928
3392
|
} catch (restoreError) {
|
|
2929
3393
|
this._reportError("applyCrop: rollback failed", restoreError);
|
|
2930
3394
|
}
|
|
@@ -2949,28 +3413,38 @@ var ImageEditor = class {
|
|
|
2949
3413
|
this._cropPrevEvented = null;
|
|
2950
3414
|
}
|
|
2951
3415
|
_removeCropRect() {
|
|
2952
|
-
if (
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
3416
|
+
if (this._cropHandlers && this._cropHandlers.length) {
|
|
3417
|
+
this._cropHandlers.forEach((targetHandlers) => {
|
|
3418
|
+
(targetHandlers.handlers || []).forEach((handlerRecord) => {
|
|
3419
|
+
try {
|
|
2957
3420
|
if (targetHandlers.target && typeof targetHandlers.target.off === "function") {
|
|
2958
3421
|
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2959
3422
|
}
|
|
2960
|
-
})
|
|
3423
|
+
} catch (error) {
|
|
3424
|
+
this._reportWarning("Crop handler cleanup failed", error);
|
|
3425
|
+
}
|
|
2961
3426
|
});
|
|
2962
|
-
}
|
|
2963
|
-
} catch (error) {
|
|
2964
|
-
void error;
|
|
3427
|
+
});
|
|
2965
3428
|
}
|
|
2966
3429
|
try {
|
|
2967
|
-
if (this.canvas) this.canvas.remove(this._cropRect);
|
|
3430
|
+
if (this.canvas && this._cropRect) this.canvas.remove(this._cropRect);
|
|
2968
3431
|
} catch (error) {
|
|
2969
3432
|
void error;
|
|
2970
3433
|
}
|
|
2971
3434
|
this._cropRect = null;
|
|
2972
3435
|
this._cropHandlers = [];
|
|
2973
3436
|
}
|
|
3437
|
+
_getCropRectContentBounds(cropRect) {
|
|
3438
|
+
if (!cropRect) return { left: 0, top: 0, width: 1, height: 1 };
|
|
3439
|
+
const width = Math.max(1, (Number(cropRect.width) || 1) * Math.abs(Number(cropRect.scaleX) || 1));
|
|
3440
|
+
const height = Math.max(1, (Number(cropRect.height) || 1) * Math.abs(Number(cropRect.scaleY) || 1));
|
|
3441
|
+
return {
|
|
3442
|
+
left: Number(cropRect.left) || 0,
|
|
3443
|
+
top: Number(cropRect.top) || 0,
|
|
3444
|
+
width,
|
|
3445
|
+
height
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
2974
3448
|
/**
|
|
2975
3449
|
* Enters crop mode by creating a resizable crop rectangle above the base image.
|
|
2976
3450
|
*
|
|
@@ -2994,14 +3468,19 @@ var ImageEditor = class {
|
|
|
2994
3468
|
const padding = this.options.crop && this.options.crop.padding ? this.options.crop.padding : 10;
|
|
2995
3469
|
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
2996
3470
|
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
2997
|
-
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width
|
|
2998
|
-
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height
|
|
3471
|
+
const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
|
|
3472
|
+
const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
|
|
2999
3473
|
const configuredMinWidth = Math.max(1, Number(this.options.crop.minWidth) || 50);
|
|
3000
3474
|
const configuredMinHeight = Math.max(1, Number(this.options.crop.minHeight) || 50);
|
|
3001
3475
|
const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
|
|
3002
3476
|
const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
|
|
3003
3477
|
const width = minCropWidth;
|
|
3004
3478
|
const height = minCropHeight;
|
|
3479
|
+
const requestedCropRotation = !!(this.options.crop && this.options.crop.allowRotationOfCropRect);
|
|
3480
|
+
if (requestedCropRotation && !this._cropRotationWarningEmitted) {
|
|
3481
|
+
this._cropRotationWarningEmitted = true;
|
|
3482
|
+
this._reportWarning("crop.allowRotationOfCropRect is disabled in v1.x because rotated crop export is not supported");
|
|
3483
|
+
}
|
|
3005
3484
|
const cropRect = new fabric.Rect({
|
|
3006
3485
|
left,
|
|
3007
3486
|
top,
|
|
@@ -3013,8 +3492,8 @@ var ImageEditor = class {
|
|
|
3013
3492
|
strokeWidth: 1,
|
|
3014
3493
|
strokeUniform: true,
|
|
3015
3494
|
selectable: true,
|
|
3016
|
-
hasRotatingPoint:
|
|
3017
|
-
lockRotation:
|
|
3495
|
+
hasRotatingPoint: false,
|
|
3496
|
+
lockRotation: true,
|
|
3018
3497
|
cornerSize: 8,
|
|
3019
3498
|
objectCaching: false,
|
|
3020
3499
|
originX: "left",
|
|
@@ -3051,7 +3530,7 @@ var ImageEditor = class {
|
|
|
3051
3530
|
const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
|
|
3052
3531
|
cropRect.set({ scaleX: nextScaleX, scaleY: nextScaleY });
|
|
3053
3532
|
cropRect.setCoords();
|
|
3054
|
-
const cropBounds =
|
|
3533
|
+
const cropBounds = this._getCropRectContentBounds(cropRect);
|
|
3055
3534
|
const imageLeft = Number(imageBounds.left) || 0;
|
|
3056
3535
|
const imageTop = Number(imageBounds.top) || 0;
|
|
3057
3536
|
const imageRight = imageLeft + (Number(imageBounds.width) || 0);
|
|
@@ -3125,89 +3604,100 @@ var ImageEditor = class {
|
|
|
3125
3604
|
async applyCrop() {
|
|
3126
3605
|
if (!this.canvas || !this._cropMode || !this._cropRect) return;
|
|
3127
3606
|
this._assertIdleForOperation("applyCrop");
|
|
3128
|
-
this.
|
|
3129
|
-
const
|
|
3130
|
-
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3131
|
-
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3132
|
-
this._restoreCropObjectState();
|
|
3133
|
-
let beforeJson;
|
|
3134
|
-
try {
|
|
3135
|
-
beforeJson = this._serializeCanvasState();
|
|
3136
|
-
} catch (error) {
|
|
3137
|
-
this._reportWarning("applyCrop: could not serialize before state", error);
|
|
3138
|
-
beforeJson = null;
|
|
3139
|
-
}
|
|
3140
|
-
const preservedMasks = [];
|
|
3607
|
+
const operationToken = this._beginBusyOperation("applyCrop");
|
|
3608
|
+
const internalOptions = this._withInternalOperationOptions(operationToken);
|
|
3141
3609
|
try {
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
preservedMasks.push(mask);
|
|
3154
|
-
}
|
|
3155
|
-
});
|
|
3156
|
-
this._clearMaskPlacementMemory();
|
|
3157
|
-
this.canvas.discardActiveObject();
|
|
3158
|
-
this.canvas.renderAll();
|
|
3610
|
+
this._cropRect.setCoords();
|
|
3611
|
+
const rectBounds = this._getCropRectContentBounds(this._cropRect);
|
|
3612
|
+
const cropRegion = this._getClampedCanvasRegion(rectBounds, { includePartialPixels: false });
|
|
3613
|
+
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
3614
|
+
this._restoreCropObjectState();
|
|
3615
|
+
let beforeJson;
|
|
3616
|
+
try {
|
|
3617
|
+
beforeJson = this._serializeCanvasState();
|
|
3618
|
+
} catch (error) {
|
|
3619
|
+
this._reportError("applyCrop: failed to capture rollback state", error);
|
|
3620
|
+
beforeJson = null;
|
|
3159
3621
|
}
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3622
|
+
if (!beforeJson) {
|
|
3623
|
+
this.cancelCrop();
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
const preservedMasks = [];
|
|
3627
|
+
try {
|
|
3628
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
3629
|
+
if (masks && masks.length) {
|
|
3630
|
+
masks.forEach((mask) => {
|
|
3631
|
+
mask.setCoords();
|
|
3632
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
3633
|
+
const intersectsCrop = maskBounds.left < cropRegion.sourceX + cropRegion.sourceWidth && maskBounds.left + maskBounds.width > cropRegion.sourceX && maskBounds.top < cropRegion.sourceY + cropRegion.sourceHeight && maskBounds.top + maskBounds.height > cropRegion.sourceY;
|
|
3634
|
+
this._removeLabelForMask(mask);
|
|
3635
|
+
this._cleanupMaskEvents(mask);
|
|
3636
|
+
this.canvas.remove(mask);
|
|
3637
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
3638
|
+
this._translateObjectByCanvasOffset(mask, -cropRegion.sourceX, -cropRegion.sourceY);
|
|
3639
|
+
mask.set({ visible: true });
|
|
3640
|
+
preservedMasks.push(mask);
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
this._clearMaskPlacementMemory();
|
|
3644
|
+
this.canvas.discardActiveObject();
|
|
3645
|
+
this.canvas.renderAll();
|
|
3646
|
+
}
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to prepare masks", error, internalOptions);
|
|
3649
|
+
return;
|
|
3650
|
+
}
|
|
3651
|
+
this._removeCropRect();
|
|
3652
|
+
this._cropMode = false;
|
|
3653
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
3654
|
+
this._prevSelectionSetting = void 0;
|
|
3655
|
+
let croppedBase64;
|
|
3656
|
+
try {
|
|
3657
|
+
croppedBase64 = await this._exportCanvasRegionToDataURL({
|
|
3658
|
+
...cropRegion,
|
|
3659
|
+
multiplier: 1,
|
|
3660
|
+
quality: this._normalizeQuality(this.options.downsampleQuality),
|
|
3661
|
+
format: "jpeg"
|
|
3187
3662
|
});
|
|
3188
|
-
|
|
3189
|
-
this.
|
|
3190
|
-
|
|
3191
|
-
this.canvas.renderAll();
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to create cropped image", error, internalOptions);
|
|
3665
|
+
return;
|
|
3192
3666
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3667
|
+
try {
|
|
3668
|
+
await this.loadImage(croppedBase64, this._withInternalOperationOptions(operationToken, { resetMaskCounter: false }));
|
|
3669
|
+
if (preservedMasks.length) {
|
|
3670
|
+
preservedMasks.forEach((mask) => {
|
|
3671
|
+
this._rebindMaskEvents(mask);
|
|
3672
|
+
this.canvas.add(mask);
|
|
3673
|
+
this.canvas.bringToFront(mask);
|
|
3674
|
+
});
|
|
3675
|
+
this._lastMask = preservedMasks[preservedMasks.length - 1];
|
|
3676
|
+
this.maskCounter = preservedMasks.reduce((max, mask) => Math.max(max, mask.maskId || 0), this.maskCounter);
|
|
3677
|
+
this._updateMaskList();
|
|
3678
|
+
this.canvas.renderAll();
|
|
3679
|
+
}
|
|
3680
|
+
} catch (error) {
|
|
3681
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", error, internalOptions);
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
let afterJson;
|
|
3685
|
+
try {
|
|
3686
|
+
afterJson = preservedMasks.length ? this._serializeCanvasState() : this._lastSnapshot;
|
|
3687
|
+
} catch (error) {
|
|
3688
|
+
this._reportWarning("applyCrop: failed to serialize after state", error);
|
|
3689
|
+
afterJson = null;
|
|
3690
|
+
}
|
|
3691
|
+
try {
|
|
3692
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
3693
|
+
} catch (error) {
|
|
3694
|
+
this._reportWarning("applyCrop: failed to push history command", error);
|
|
3695
|
+
}
|
|
3696
|
+
this._updateUI();
|
|
3697
|
+
this.canvas.renderAll();
|
|
3698
|
+
} finally {
|
|
3699
|
+
this._endBusyOperation(operationToken);
|
|
3208
3700
|
}
|
|
3209
|
-
this._updateUI();
|
|
3210
|
-
this.canvas.renderAll();
|
|
3211
3701
|
}
|
|
3212
3702
|
/* ---------- Misc / UI ---------- */
|
|
3213
3703
|
/**
|
|
@@ -3216,7 +3706,7 @@ var ImageEditor = class {
|
|
|
3216
3706
|
* @private
|
|
3217
3707
|
*/
|
|
3218
3708
|
_updateInputs() {
|
|
3219
|
-
const scaleInputElement = this._getElement("
|
|
3709
|
+
const scaleInputElement = this._getElement("scalePercentageInput");
|
|
3220
3710
|
if (scaleInputElement) scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
3221
3711
|
}
|
|
3222
3712
|
/**
|
|
@@ -3240,7 +3730,7 @@ var ImageEditor = class {
|
|
|
3240
3730
|
for (const key of Object.keys(this.elements || {})) {
|
|
3241
3731
|
const element = this._getElement(key);
|
|
3242
3732
|
if (!element) continue;
|
|
3243
|
-
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3733
|
+
if (key === "applyCropButton" || key === "cancelCropButton" || key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
3244
3734
|
this._setDisabled(key, false);
|
|
3245
3735
|
} else {
|
|
3246
3736
|
this._setDisabled(key, true);
|
|
@@ -3248,24 +3738,24 @@ var ImageEditor = class {
|
|
|
3248
3738
|
}
|
|
3249
3739
|
return;
|
|
3250
3740
|
}
|
|
3251
|
-
this._setDisabled("
|
|
3252
|
-
this._setDisabled("
|
|
3253
|
-
this._setDisabled("
|
|
3254
|
-
this._setDisabled("
|
|
3255
|
-
this._setDisabled("
|
|
3256
|
-
this._setDisabled("
|
|
3257
|
-
this._setDisabled("
|
|
3258
|
-
this._setDisabled("
|
|
3259
|
-
this._setDisabled("
|
|
3260
|
-
this._setDisabled("
|
|
3261
|
-
this._setDisabled("
|
|
3262
|
-
this._setDisabled("
|
|
3263
|
-
this._setDisabled("
|
|
3264
|
-
this._setDisabled("
|
|
3265
|
-
this._setDisabled("
|
|
3266
|
-
this._setDisabled("
|
|
3267
|
-
this._setDisabled("
|
|
3268
|
-
this._setDisabled("
|
|
3741
|
+
this._setDisabled("zoomInButton", !hasImage || isBusy || this.currentScale >= this.options.maxScale);
|
|
3742
|
+
this._setDisabled("zoomOutButton", !hasImage || isBusy || this.currentScale <= this.options.minScale);
|
|
3743
|
+
this._setDisabled("rotateLeftButton", !hasImage || isBusy);
|
|
3744
|
+
this._setDisabled("rotateRightButton", !hasImage || isBusy);
|
|
3745
|
+
this._setDisabled("createMaskButton", !hasImage || isBusy);
|
|
3746
|
+
this._setDisabled("removeSelectedMaskButton", !hasSelectedMask || isBusy);
|
|
3747
|
+
this._setDisabled("removeAllMasksButton", !hasMasks || isBusy);
|
|
3748
|
+
this._setDisabled("mergeMasksButton", !hasImage || !hasMasks || isBusy);
|
|
3749
|
+
this._setDisabled("downloadImageButton", !hasImage || isBusy);
|
|
3750
|
+
this._setDisabled("resetImageTransformButton", !hasImage || isDefaultTransform || isBusy);
|
|
3751
|
+
this._setDisabled("undoButton", !hasImage || isBusy || !canUndo);
|
|
3752
|
+
this._setDisabled("redoButton", !hasImage || isBusy || !canRedo);
|
|
3753
|
+
this._setDisabled("enterCropModeButton", !hasImage || isBusy);
|
|
3754
|
+
this._setDisabled("applyCropButton", true);
|
|
3755
|
+
this._setDisabled("cancelCropButton", true);
|
|
3756
|
+
this._setDisabled("scalePercentageInput", !hasImage || isBusy);
|
|
3757
|
+
this._setDisabled("rotateLeftDegreesInput", !hasImage || isBusy);
|
|
3758
|
+
this._setDisabled("rotateRightDegreesInput", !hasImage || isBusy);
|
|
3269
3759
|
this._setDisabled("maskList", !hasImage || isBusy);
|
|
3270
3760
|
this._setDisabled("imageInput", isBusy);
|
|
3271
3761
|
this._setDisabled("uploadArea", isBusy);
|
|
@@ -3273,7 +3763,7 @@ var ImageEditor = class {
|
|
|
3273
3763
|
/**
|
|
3274
3764
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
3275
3765
|
*
|
|
3276
|
-
* @param {string} key - Key of the element in this.elements (e.g. '
|
|
3766
|
+
* @param {string} key - Key of the element in this.elements (e.g. 'zoomInButton').
|
|
3277
3767
|
* @param {boolean} disabled - If true, disables the element; otherwise enables.
|
|
3278
3768
|
* @private
|
|
3279
3769
|
*/
|
|
@@ -3397,14 +3887,7 @@ var ImageEditor = class {
|
|
|
3397
3887
|
} catch (error) {
|
|
3398
3888
|
void error;
|
|
3399
3889
|
}
|
|
3400
|
-
if (this._cropRect)
|
|
3401
|
-
try {
|
|
3402
|
-
this.canvas.remove(this._cropRect);
|
|
3403
|
-
} catch (error) {
|
|
3404
|
-
void error;
|
|
3405
|
-
}
|
|
3406
|
-
this._cropRect = null;
|
|
3407
|
-
}
|
|
3890
|
+
if (this._cropRect) this._removeCropRect();
|
|
3408
3891
|
if (this.containerElement && this._containerOriginalOverflow) {
|
|
3409
3892
|
try {
|
|
3410
3893
|
this._restoreContainerOverflowState();
|
|
@@ -3427,11 +3910,19 @@ var ImageEditor = class {
|
|
|
3427
3910
|
this.canvasElement.style.display = this._canvasElementOriginalStyle.display;
|
|
3428
3911
|
this.canvasElement.style.width = this._canvasElementOriginalStyle.width;
|
|
3429
3912
|
this.canvasElement.style.height = this._canvasElementOriginalStyle.height;
|
|
3913
|
+
this.canvasElement.style.maxWidth = this._canvasElementOriginalStyle.maxWidth;
|
|
3430
3914
|
} catch (error) {
|
|
3431
3915
|
void error;
|
|
3432
3916
|
}
|
|
3433
3917
|
}
|
|
3434
3918
|
if (this.canvas) {
|
|
3919
|
+
try {
|
|
3920
|
+
this.canvas.getObjects().forEach((object) => {
|
|
3921
|
+
if (object && object.maskId) this._cleanupMaskEvents(object);
|
|
3922
|
+
});
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
void error;
|
|
3925
|
+
}
|
|
3435
3926
|
try {
|
|
3436
3927
|
this.canvas.dispose();
|
|
3437
3928
|
} catch (error) {
|
|
@@ -3528,7 +4019,7 @@ var AnimationQueue = class {
|
|
|
3528
4019
|
task.reject(error);
|
|
3529
4020
|
}
|
|
3530
4021
|
} finally {
|
|
3531
|
-
if (
|
|
4022
|
+
if (this.currentTask === task) this.currentTask = null;
|
|
3532
4023
|
}
|
|
3533
4024
|
}
|
|
3534
4025
|
} finally {
|
|
@@ -3626,11 +4117,11 @@ var HistoryManager = class {
|
|
|
3626
4117
|
*
|
|
3627
4118
|
* @returns {Promise<void>} Resolves after the undo task completes.
|
|
3628
4119
|
*/
|
|
3629
|
-
undo() {
|
|
4120
|
+
undo(options = {}) {
|
|
3630
4121
|
return this.enqueue(async () => {
|
|
3631
4122
|
if (this.currentIndex >= 0) {
|
|
3632
4123
|
const index = this.currentIndex;
|
|
3633
|
-
await this.history[index].undo();
|
|
4124
|
+
await this.history[index].undo(options);
|
|
3634
4125
|
this.currentIndex = index - 1;
|
|
3635
4126
|
}
|
|
3636
4127
|
});
|
|
@@ -3640,11 +4131,11 @@ var HistoryManager = class {
|
|
|
3640
4131
|
*
|
|
3641
4132
|
* @returns {Promise<void>} Resolves after the redo task completes.
|
|
3642
4133
|
*/
|
|
3643
|
-
redo() {
|
|
4134
|
+
redo(options = {}) {
|
|
3644
4135
|
return this.enqueue(async () => {
|
|
3645
4136
|
if (this.currentIndex < this.history.length - 1) {
|
|
3646
4137
|
const index = this.currentIndex + 1;
|
|
3647
|
-
await this.history[index].execute();
|
|
4138
|
+
await this.history[index].execute(options);
|
|
3648
4139
|
this.currentIndex = index;
|
|
3649
4140
|
}
|
|
3650
4141
|
});
|