@bensitu/image-editor 1.2.1 → 1.2.2
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 +12 -26
- package/dist/image-editor.esm.js +1325 -763
- package/dist/image-editor.esm.js.map +3 -3
- package/dist/image-editor.esm.min.js +3 -8
- package/dist/image-editor.esm.min.js.map +3 -3
- package/dist/image-editor.esm.min.mjs +3 -8
- package/dist/image-editor.esm.min.mjs.map +3 -3
- package/dist/image-editor.esm.mjs +1325 -763
- package/dist/image-editor.esm.mjs.map +3 -3
- package/dist/image-editor.js +1325 -763
- package/dist/image-editor.js.map +3 -3
- package/dist/image-editor.min.js +2 -7
- package/dist/image-editor.min.js.map +3 -3
- package/image-editor.d.ts +29 -7
- package/package.json +1 -1
- package/src/image-editor.js +1344 -729
|
@@ -5,15 +5,10 @@ import fabricModule from "fabric";
|
|
|
5
5
|
/**
|
|
6
6
|
* @file image-editor.js
|
|
7
7
|
* @module image-editor
|
|
8
|
-
* @version 1.2.
|
|
8
|
+
* @version 1.2.2
|
|
9
9
|
* @author Ben Situ
|
|
10
10
|
* @license MIT
|
|
11
11
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
12
|
-
*
|
|
13
|
-
* This source file is free software, available under the MIT license.
|
|
14
|
-
* It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
15
|
-
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
16
|
-
* See the license files for details.
|
|
17
12
|
*/
|
|
18
13
|
var fabric = null;
|
|
19
14
|
function getGlobalScope() {
|
|
@@ -115,9 +110,9 @@ var ImageEditor = class {
|
|
|
115
110
|
this._reportError("fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.");
|
|
116
111
|
}
|
|
117
112
|
this.canvas = null;
|
|
118
|
-
this.
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
113
|
+
this.canvasElement = null;
|
|
114
|
+
this.containerElement = null;
|
|
115
|
+
this.placeholderElement = null;
|
|
121
116
|
this.originalImage = null;
|
|
122
117
|
this.baseImageScale = 1;
|
|
123
118
|
this.currentScale = 1;
|
|
@@ -127,18 +122,49 @@ var ImageEditor = class {
|
|
|
127
122
|
this.elements = {};
|
|
128
123
|
this.isImageLoadedToCanvas = false;
|
|
129
124
|
this.maxHistorySize = 50;
|
|
130
|
-
this.
|
|
125
|
+
this._handlersByElementKey = {};
|
|
131
126
|
this._lastMask = null;
|
|
132
127
|
this._lastMaskInitialLeft = null;
|
|
133
128
|
this._lastMaskInitialTop = null;
|
|
134
129
|
this._lastMaskInitialWidth = null;
|
|
130
|
+
this._lastSnapshot = null;
|
|
135
131
|
this._cropMode = false;
|
|
136
132
|
this._cropRect = null;
|
|
137
133
|
this._cropHandlers = [];
|
|
134
|
+
this._cropPrevEvented = null;
|
|
135
|
+
this._prevSelectionSetting = void 0;
|
|
136
|
+
this._containerOriginalOverflow = void 0;
|
|
138
137
|
this.onImageLoaded = typeof options.onImageLoaded === "function" ? options.onImageLoaded : null;
|
|
139
138
|
this.animQueue = new AnimationQueue();
|
|
140
139
|
this.historyManager = new HistoryManager(this.maxHistorySize);
|
|
141
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* @deprecated Use canvasElement instead.
|
|
143
|
+
*/
|
|
144
|
+
get canvasEl() {
|
|
145
|
+
return this.canvasElement;
|
|
146
|
+
}
|
|
147
|
+
set canvasEl(value) {
|
|
148
|
+
this.canvasElement = value;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* @deprecated Use containerElement instead.
|
|
152
|
+
*/
|
|
153
|
+
get containerEl() {
|
|
154
|
+
return this.containerElement;
|
|
155
|
+
}
|
|
156
|
+
set containerEl(value) {
|
|
157
|
+
this.containerElement = value;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* @deprecated Use placeholderElement instead.
|
|
161
|
+
*/
|
|
162
|
+
get placeholderEl() {
|
|
163
|
+
return this.placeholderElement;
|
|
164
|
+
}
|
|
165
|
+
set placeholderEl(value) {
|
|
166
|
+
this.placeholderElement = value;
|
|
167
|
+
}
|
|
142
168
|
/**
|
|
143
169
|
* Initializes the editor, binds to DOM elements, sets up event handlers,
|
|
144
170
|
* and (optionally) loads an initial image.
|
|
@@ -223,127 +249,163 @@ var ImageEditor = class {
|
|
|
223
249
|
* @private
|
|
224
250
|
*/
|
|
225
251
|
_initCanvas() {
|
|
226
|
-
const
|
|
227
|
-
if (!
|
|
252
|
+
const canvasElement = document.getElementById(this.elements.canvas);
|
|
253
|
+
if (!canvasElement)
|
|
228
254
|
throw new Error("Canvas is not found: " + this.elements.canvas);
|
|
229
|
-
this.
|
|
255
|
+
this.canvasElement = canvasElement;
|
|
230
256
|
if (this.elements.canvasContainer) {
|
|
231
|
-
const
|
|
232
|
-
this.
|
|
257
|
+
const containerElement = document.getElementById(this.elements.canvasContainer);
|
|
258
|
+
this.containerElement = containerElement || canvasElement.parentElement;
|
|
233
259
|
} else {
|
|
234
|
-
this.
|
|
260
|
+
this.containerElement = canvasElement.parentElement;
|
|
235
261
|
}
|
|
236
|
-
this.
|
|
237
|
-
let
|
|
238
|
-
let
|
|
239
|
-
if (this.
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
262
|
+
this.placeholderElement = document.getElementById(this.elements.imgPlaceholder) || null;
|
|
263
|
+
let initialWidth = this.options.canvasWidth;
|
|
264
|
+
let initialHeight = this.options.canvasHeight;
|
|
265
|
+
if (this.containerElement) {
|
|
266
|
+
const containerWidth = Math.floor(this.containerElement.clientWidth);
|
|
267
|
+
const containerHeight = Math.floor(this.containerElement.clientHeight);
|
|
268
|
+
if (containerWidth > 0 && containerHeight > 0) {
|
|
269
|
+
initialWidth = containerWidth;
|
|
270
|
+
initialHeight = containerHeight;
|
|
245
271
|
}
|
|
246
272
|
}
|
|
247
|
-
this.canvas = new fabric.Canvas(
|
|
248
|
-
width:
|
|
249
|
-
height:
|
|
273
|
+
this.canvas = new fabric.Canvas(canvasElement, {
|
|
274
|
+
width: initialWidth,
|
|
275
|
+
height: initialHeight,
|
|
250
276
|
backgroundColor: this.options.backgroundColor,
|
|
251
277
|
selection: this.options.groupSelection,
|
|
252
278
|
preserveObjectStacking: true
|
|
253
279
|
});
|
|
254
|
-
this.canvas.on("selection:created", (
|
|
255
|
-
this.canvas.on("selection:updated", (
|
|
256
|
-
this.canvas.on("selection:cleared", () => this.
|
|
257
|
-
this.canvas.on("object:moving", (
|
|
258
|
-
if (
|
|
259
|
-
this._syncMaskLabel(
|
|
280
|
+
this.canvas.on("selection:created", (event) => this._handleSelectionChanged(event.selected));
|
|
281
|
+
this.canvas.on("selection:updated", (event) => this._handleSelectionChanged(event.selected));
|
|
282
|
+
this.canvas.on("selection:cleared", () => this._handleSelectionChanged([]));
|
|
283
|
+
this.canvas.on("object:moving", (event) => {
|
|
284
|
+
if (event.target && event.target.maskId)
|
|
285
|
+
this._syncMaskLabel(event.target);
|
|
260
286
|
});
|
|
261
|
-
this.canvas.on("object:scaling", (
|
|
262
|
-
if (
|
|
263
|
-
this._syncMaskLabel(
|
|
287
|
+
this.canvas.on("object:scaling", (event) => {
|
|
288
|
+
if (event.target && event.target.maskId)
|
|
289
|
+
this._syncMaskLabel(event.target);
|
|
264
290
|
});
|
|
265
|
-
this.canvas.on("object:rotating", (
|
|
266
|
-
if (
|
|
267
|
-
this._syncMaskLabel(
|
|
291
|
+
this.canvas.on("object:rotating", (event) => {
|
|
292
|
+
if (event.target && event.target.maskId)
|
|
293
|
+
this._syncMaskLabel(event.target);
|
|
268
294
|
});
|
|
269
|
-
this.canvas.on("object:modified", (
|
|
270
|
-
|
|
271
|
-
|
|
295
|
+
this.canvas.on("object:modified", (event) => this._handleObjectModified(event.target));
|
|
296
|
+
this.canvasElement.style.display = "block";
|
|
297
|
+
}
|
|
298
|
+
_handleObjectModified(target) {
|
|
299
|
+
const masks = this._getModifiedMasks(target);
|
|
300
|
+
if (!masks.length)
|
|
301
|
+
return;
|
|
302
|
+
masks.forEach((mask) => {
|
|
303
|
+
if (typeof mask.setCoords === "function")
|
|
304
|
+
mask.setCoords();
|
|
305
|
+
this._syncMaskLabel(mask);
|
|
306
|
+
this._expandCanvasToFitObject(mask);
|
|
272
307
|
});
|
|
273
|
-
this.
|
|
308
|
+
this.saveState();
|
|
309
|
+
}
|
|
310
|
+
_getModifiedMasks(target) {
|
|
311
|
+
if (!target)
|
|
312
|
+
return [];
|
|
313
|
+
if (target.maskId)
|
|
314
|
+
return [target];
|
|
315
|
+
const objects = typeof target.getObjects === "function" ? target.getObjects() : [];
|
|
316
|
+
return Array.isArray(objects) ? objects.filter((object) => object && object.maskId) : [];
|
|
317
|
+
}
|
|
318
|
+
_syncContainerOverflow() {
|
|
319
|
+
if (!this.containerElement || !this.containerElement.style)
|
|
320
|
+
return;
|
|
321
|
+
if (this._containerOriginalOverflow === void 0) {
|
|
322
|
+
this._containerOriginalOverflow = this.containerElement.style.overflow || "";
|
|
323
|
+
}
|
|
324
|
+
if (this.options.coverImageToCanvas) {
|
|
325
|
+
const shouldResetScroll = !this.isImageLoadedToCanvas;
|
|
326
|
+
this.containerElement.style.overflow = "scroll";
|
|
327
|
+
if (shouldResetScroll) {
|
|
328
|
+
this.containerElement.scrollLeft = 0;
|
|
329
|
+
this.containerElement.scrollTop = 0;
|
|
330
|
+
}
|
|
331
|
+
} else if (this.options.fitImageToCanvas) {
|
|
332
|
+
this.containerElement.style.overflow = "auto";
|
|
333
|
+
this.containerElement.scrollLeft = 0;
|
|
334
|
+
this.containerElement.scrollTop = 0;
|
|
335
|
+
} else {
|
|
336
|
+
this.containerElement.style.overflow = this._containerOriginalOverflow;
|
|
337
|
+
}
|
|
274
338
|
}
|
|
275
339
|
/**
|
|
276
340
|
* DOM / UI bindings
|
|
277
341
|
* @private
|
|
278
342
|
*/
|
|
279
343
|
_bindEvents() {
|
|
280
|
-
this._bindIfExists("uploadArea", "click", () =>
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
344
|
+
this._bindIfExists("uploadArea", "click", () => {
|
|
345
|
+
const uploadAreaElement = document.getElementById(this.elements.uploadArea);
|
|
346
|
+
if (this._isElementDisabled(uploadAreaElement))
|
|
347
|
+
return;
|
|
348
|
+
document.getElementById(this.elements.imageInput)?.click();
|
|
349
|
+
});
|
|
350
|
+
this._bindIfExists("imageInput", "change", (event) => {
|
|
351
|
+
const file = event.target.files && event.target.files[0];
|
|
352
|
+
if (file)
|
|
353
|
+
this._loadImageFile(file);
|
|
354
|
+
});
|
|
289
355
|
this._bindIfExists("zoomInBtn", "click", () => this.scaleImage(this.currentScale + this.options.scaleStep));
|
|
290
356
|
this._bindIfExists("zoomOutBtn", "click", () => this.scaleImage(this.currentScale - this.options.scaleStep));
|
|
291
357
|
this._bindIfExists("resetBtn", "click", () => {
|
|
292
|
-
this.
|
|
358
|
+
this.resetImageTransform();
|
|
293
359
|
});
|
|
294
|
-
this._bindIfExists("addMaskBtn", "click", () => this.
|
|
360
|
+
this._bindIfExists("addMaskBtn", "click", () => this.createMask());
|
|
295
361
|
this._bindIfExists("removeMaskBtn", "click", () => this.removeSelectedMask());
|
|
296
362
|
this._bindIfExists("removeAllMasksBtn", "click", () => this.removeAllMasks());
|
|
297
|
-
this._bindIfExists("mergeBtn", "click", () => this.
|
|
363
|
+
this._bindIfExists("mergeBtn", "click", () => this.mergeMasks());
|
|
298
364
|
this._bindIfExists("downloadBtn", "click", () => this.downloadImage());
|
|
299
365
|
this._bindIfExists("undoBtn", "click", () => this.undo());
|
|
300
366
|
this._bindIfExists("redoBtn", "click", () => this.redo());
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
step = p;
|
|
322
|
-
}
|
|
323
|
-
this.rotateImage(this.currentRotation + step);
|
|
324
|
-
});
|
|
367
|
+
this._bindIfExists("rotateLeftBtn", "click", () => {
|
|
368
|
+
const rotationInputElement = document.getElementById(this.elements.rotationLeftInput);
|
|
369
|
+
let step = this.options.rotationStep;
|
|
370
|
+
if (rotationInputElement) {
|
|
371
|
+
const parsedStep = parseFloat(rotationInputElement.value);
|
|
372
|
+
if (!isNaN(parsedStep))
|
|
373
|
+
step = parsedStep;
|
|
374
|
+
}
|
|
375
|
+
this.rotateImage(this.currentRotation - step);
|
|
376
|
+
});
|
|
377
|
+
this._bindIfExists("rotateRightBtn", "click", () => {
|
|
378
|
+
const rotationInputElement = document.getElementById(this.elements.rotationRightInput);
|
|
379
|
+
let step = this.options.rotationStep;
|
|
380
|
+
if (rotationInputElement) {
|
|
381
|
+
const parsedStep = parseFloat(rotationInputElement.value);
|
|
382
|
+
if (!isNaN(parsedStep))
|
|
383
|
+
step = parsedStep;
|
|
384
|
+
}
|
|
385
|
+
this.rotateImage(this.currentRotation + step);
|
|
386
|
+
});
|
|
325
387
|
this._bindIfExists("cropBtn", "click", () => this.enterCropMode());
|
|
326
388
|
this._bindIfExists("applyCropBtn", "click", () => {
|
|
327
|
-
this.applyCrop().catch((
|
|
389
|
+
this.applyCrop().catch((error) => this._reportError("applyCrop failed", error));
|
|
328
390
|
});
|
|
329
391
|
this._bindIfExists("cancelCropBtn", "click", () => this.cancelCrop());
|
|
330
392
|
}
|
|
331
393
|
/**
|
|
332
394
|
* Event binding element check
|
|
333
395
|
*
|
|
334
|
-
* @param {*}
|
|
396
|
+
* @param {*} eventName
|
|
335
397
|
* @param {*} handler
|
|
336
398
|
* @param {*} key
|
|
337
399
|
* @private
|
|
338
400
|
*/
|
|
339
|
-
_bindIfExists(key,
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
if (!this.
|
|
345
|
-
this.
|
|
346
|
-
this.
|
|
401
|
+
_bindIfExists(key, eventName, handler) {
|
|
402
|
+
const element = document.getElementById(this.elements[key]);
|
|
403
|
+
if (element) {
|
|
404
|
+
element.addEventListener(eventName, handler);
|
|
405
|
+
this._handlersByElementKey = this._handlersByElementKey || {};
|
|
406
|
+
if (!this._handlersByElementKey[key])
|
|
407
|
+
this._handlersByElementKey[key] = [];
|
|
408
|
+
this._handlersByElementKey[key].push({ eventName, handler });
|
|
347
409
|
}
|
|
348
410
|
}
|
|
349
411
|
/**
|
|
@@ -356,88 +418,88 @@ var ImageEditor = class {
|
|
|
356
418
|
if (!file || !file.type.startsWith("image/"))
|
|
357
419
|
return;
|
|
358
420
|
const reader = new FileReader();
|
|
359
|
-
reader.onload = (
|
|
360
|
-
reader.onerror = (
|
|
361
|
-
this._reportError("Image file could not be read",
|
|
421
|
+
reader.onload = (event) => this.loadImage(event.target.result);
|
|
422
|
+
reader.onerror = (event) => {
|
|
423
|
+
this._reportError("Image file could not be read", event);
|
|
362
424
|
};
|
|
363
425
|
reader.readAsDataURL(file);
|
|
364
426
|
}
|
|
365
427
|
/**
|
|
366
428
|
* Load a base64 encoded image string into fabric.
|
|
367
429
|
* @async
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
async loadImage(
|
|
430
|
+
* @param {String} imageBase64
|
|
431
|
+
*/
|
|
432
|
+
async loadImage(imageBase64) {
|
|
371
433
|
if (!this._fabricLoaded)
|
|
372
434
|
return;
|
|
373
435
|
if (!this.canvas)
|
|
374
436
|
return;
|
|
375
|
-
if (!
|
|
437
|
+
if (!imageBase64 || typeof imageBase64 !== "string" || !imageBase64.startsWith("data:image/"))
|
|
376
438
|
return;
|
|
377
439
|
this._setPlaceholderVisible(false);
|
|
378
|
-
|
|
379
|
-
|
|
440
|
+
this._syncContainerOverflow();
|
|
441
|
+
const imageElement = await this._createImageElement(imageBase64);
|
|
442
|
+
let loadSource = imageBase64;
|
|
380
443
|
if (this.options.downsampleOnLoad) {
|
|
381
|
-
const
|
|
382
|
-
if (
|
|
444
|
+
const shouldResize = imageElement.naturalWidth > this.options.downsampleMaxWidth || imageElement.naturalHeight > this.options.downsampleMaxHeight;
|
|
445
|
+
if (shouldResize) {
|
|
383
446
|
const ratio = Math.min(
|
|
384
|
-
this.options.downsampleMaxWidth /
|
|
385
|
-
this.options.downsampleMaxHeight /
|
|
447
|
+
this.options.downsampleMaxWidth / imageElement.naturalWidth,
|
|
448
|
+
this.options.downsampleMaxHeight / imageElement.naturalHeight
|
|
386
449
|
);
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
|
|
450
|
+
const targetWidth = Math.round(imageElement.naturalWidth * ratio);
|
|
451
|
+
const targetHeight = Math.round(imageElement.naturalHeight * ratio);
|
|
452
|
+
loadSource = this._resampleImageToDataURL(imageElement, targetWidth, targetHeight, this.options.downsampleQuality);
|
|
390
453
|
}
|
|
391
454
|
}
|
|
392
455
|
return new Promise((resolve, reject) => {
|
|
393
|
-
fabric.Image.fromURL(
|
|
456
|
+
fabric.Image.fromURL(loadSource, (fabricImage) => {
|
|
394
457
|
try {
|
|
395
|
-
if (!
|
|
458
|
+
if (!fabricImage)
|
|
396
459
|
throw new Error("Image could not be loaded");
|
|
397
460
|
this.canvas.discardActiveObject();
|
|
398
461
|
this._hideAllMaskLabels();
|
|
399
462
|
this.canvas.clear();
|
|
400
463
|
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
const
|
|
464
|
+
fabricImage.set({ originX: "left", originY: "top", selectable: false, evented: false });
|
|
465
|
+
const imageWidth = fabricImage.width;
|
|
466
|
+
const imageHeight = fabricImage.height;
|
|
467
|
+
const viewport = this._getContainerViewportSize();
|
|
468
|
+
const minWidth = viewport.width;
|
|
469
|
+
const minHeight = viewport.height;
|
|
406
470
|
if (this.options.fitImageToCanvas) {
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
this._setCanvasSizeInt(
|
|
410
|
-
const fitScale = Math.min(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
this.baseImageScale =
|
|
471
|
+
const canvasWidth = Math.max(1, Math.min(this.options.canvasWidth, minWidth) - 1);
|
|
472
|
+
const canvasHeight = Math.max(1, Math.min(this.options.canvasHeight, minHeight) - 1);
|
|
473
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
474
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
475
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
476
|
+
fabricImage.scale(fitScale);
|
|
477
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
414
478
|
} else if (this.options.coverImageToCanvas) {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
fimg.scale(coverScale);
|
|
421
|
-
this.baseImageScale = fimg.scaleX || 1;
|
|
479
|
+
const layout = this._calculateCoverCanvasLayout(imageWidth, imageHeight);
|
|
480
|
+
this._setCanvasSizeInt(layout.canvasWidth, layout.canvasHeight);
|
|
481
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
482
|
+
fabricImage.scale(layout.scale);
|
|
483
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
422
484
|
} else if (this.options.expandCanvasToImage) {
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
this._setCanvasSizeInt(
|
|
426
|
-
|
|
427
|
-
|
|
485
|
+
const canvasWidth = Math.max(minWidth, Math.floor(imageWidth));
|
|
486
|
+
const canvasHeight = Math.max(minHeight, Math.floor(imageHeight));
|
|
487
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
488
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
489
|
+
fabricImage.scale(1);
|
|
428
490
|
this.baseImageScale = 1;
|
|
429
491
|
} else {
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
this._setCanvasSizeInt(
|
|
433
|
-
const fitScale = Math.min(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.baseImageScale =
|
|
492
|
+
const canvasWidth = Math.max(this.options.canvasWidth, minWidth);
|
|
493
|
+
const canvasHeight = Math.max(this.options.canvasHeight, minHeight);
|
|
494
|
+
this._setCanvasSizeInt(canvasWidth, canvasHeight);
|
|
495
|
+
const fitScale = Math.min(canvasWidth / imageWidth, canvasHeight / imageHeight, 1);
|
|
496
|
+
fabricImage.set({ left: 0, top: 0 });
|
|
497
|
+
fabricImage.scale(fitScale);
|
|
498
|
+
this.baseImageScale = fabricImage.scaleX || 1;
|
|
437
499
|
}
|
|
438
|
-
this.originalImage =
|
|
439
|
-
this.canvas.add(
|
|
440
|
-
this.canvas.sendToBack(
|
|
500
|
+
this.originalImage = fabricImage;
|
|
501
|
+
this.canvas.add(fabricImage);
|
|
502
|
+
this.canvas.sendToBack(fabricImage);
|
|
441
503
|
this._lastMask = null;
|
|
442
504
|
this._lastMaskInitialLeft = null;
|
|
443
505
|
this._lastMaskInitialTop = null;
|
|
@@ -450,12 +512,17 @@ var ImageEditor = class {
|
|
|
450
512
|
this.isImageLoadedToCanvas = true;
|
|
451
513
|
this._updateUI();
|
|
452
514
|
this.canvas.renderAll();
|
|
515
|
+
try {
|
|
516
|
+
this._lastSnapshot = this._serializeCanvasState();
|
|
517
|
+
} catch (error) {
|
|
518
|
+
this._reportWarning("loadImage: failed to capture initial canvas snapshot", error);
|
|
519
|
+
}
|
|
453
520
|
if (typeof this.onImageLoaded === "function") {
|
|
454
521
|
this.onImageLoaded();
|
|
455
522
|
}
|
|
456
523
|
resolve();
|
|
457
|
-
} catch (
|
|
458
|
-
reject(
|
|
524
|
+
} catch (error) {
|
|
525
|
+
reject(error);
|
|
459
526
|
}
|
|
460
527
|
}, { crossOrigin: "anonymous" });
|
|
461
528
|
});
|
|
@@ -471,43 +538,43 @@ var ImageEditor = class {
|
|
|
471
538
|
/**
|
|
472
539
|
* Creates an HTMLImageElement from a given data URL.
|
|
473
540
|
*
|
|
474
|
-
* @param {string}
|
|
541
|
+
* @param {string} dataUrl - A data URL representing the image (e.g., "data:image/png;base64,...").
|
|
475
542
|
* @returns {Promise<HTMLImageElement>} A promise that resolves to the created image element when loaded, or rejects on error.
|
|
476
543
|
* @private
|
|
477
544
|
*/
|
|
478
|
-
_createImageElement(
|
|
479
|
-
return new Promise((
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
545
|
+
_createImageElement(dataUrl) {
|
|
546
|
+
return new Promise((resolve, reject) => {
|
|
547
|
+
const imageElement = new Image();
|
|
548
|
+
imageElement.onload = () => {
|
|
549
|
+
imageElement.onload = null;
|
|
550
|
+
imageElement.onerror = null;
|
|
551
|
+
resolve(imageElement);
|
|
485
552
|
};
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
553
|
+
imageElement.onerror = (error) => {
|
|
554
|
+
imageElement.onload = null;
|
|
555
|
+
imageElement.onerror = null;
|
|
556
|
+
reject(error);
|
|
490
557
|
};
|
|
491
|
-
|
|
558
|
+
imageElement.src = dataUrl;
|
|
492
559
|
});
|
|
493
560
|
}
|
|
494
561
|
/**
|
|
495
562
|
* Resamples the given image element to a new width and height and returns the result as a JPEG data URL.
|
|
496
563
|
*
|
|
497
|
-
* @param {HTMLImageElement}
|
|
498
|
-
* @param {number}
|
|
499
|
-
* @param {number}
|
|
564
|
+
* @param {HTMLImageElement} imageElement - The image element to resample.
|
|
565
|
+
* @param {number} targetWidth - Target width (in pixels) for the resampled image.
|
|
566
|
+
* @param {number} targetHeight - Target height (in pixels) for the resampled image.
|
|
500
567
|
* @param {number} [quality=0.92] - JPEG image quality between 0 and 1 (optional, default 0.92).
|
|
501
568
|
* @returns {string} A data URL representing the resampled image as JPEG.
|
|
502
569
|
* @private
|
|
503
570
|
*/
|
|
504
|
-
_resampleImageToDataURL(
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
return
|
|
571
|
+
_resampleImageToDataURL(imageElement, targetWidth, targetHeight, quality = 0.92) {
|
|
572
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
573
|
+
offscreenCanvas.width = targetWidth;
|
|
574
|
+
offscreenCanvas.height = targetHeight;
|
|
575
|
+
const context = offscreenCanvas.getContext("2d");
|
|
576
|
+
context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, targetWidth, targetHeight);
|
|
577
|
+
return offscreenCanvas.toDataURL("image/jpeg", quality);
|
|
511
578
|
}
|
|
512
579
|
/**
|
|
513
580
|
* Sets canvas size to integer width and height values to prevent scrollbars due to sub-pixel rendering.
|
|
@@ -524,61 +591,369 @@ var ImageEditor = class {
|
|
|
524
591
|
this.canvas.setHeight(ih);
|
|
525
592
|
if (typeof this.canvas.calcOffset === "function")
|
|
526
593
|
this.canvas.calcOffset();
|
|
527
|
-
if (this.
|
|
528
|
-
this.
|
|
529
|
-
this.
|
|
530
|
-
this.
|
|
594
|
+
if (this.canvasElement) {
|
|
595
|
+
this.canvasElement.style.width = iw + "px";
|
|
596
|
+
this.canvasElement.style.height = ih + "px";
|
|
597
|
+
this.canvasElement.style.maxWidth = "none";
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
_ceilCanvasDimension(value) {
|
|
601
|
+
const numericValue = Number(value) || 0;
|
|
602
|
+
const roundedValue = Math.round(numericValue);
|
|
603
|
+
if (Math.abs(numericValue - roundedValue) < 0.01)
|
|
604
|
+
return roundedValue;
|
|
605
|
+
return Math.ceil(numericValue);
|
|
606
|
+
}
|
|
607
|
+
_getContainerViewportSize() {
|
|
608
|
+
if (!this.containerElement) {
|
|
609
|
+
return {
|
|
610
|
+
width: Math.max(1, Math.floor(this.options.canvasWidth || 1)),
|
|
611
|
+
height: Math.max(1, Math.floor(this.options.canvasHeight || 1))
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (this._hasFixedContainerScrollbars()) {
|
|
615
|
+
return {
|
|
616
|
+
width: Math.max(1, Math.floor(this.containerElement.clientWidth || this.options.canvasWidth || 1)),
|
|
617
|
+
height: Math.max(1, Math.floor(this.containerElement.clientHeight || this.options.canvasHeight || 1))
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const previousOverflow = this.containerElement.style.overflow;
|
|
621
|
+
this.containerElement.style.overflow = "hidden";
|
|
622
|
+
const width = Math.max(1, Math.floor(this.containerElement.clientWidth || this.options.canvasWidth || 1));
|
|
623
|
+
const height = Math.max(1, Math.floor(this.containerElement.clientHeight || this.options.canvasHeight || 1));
|
|
624
|
+
this.containerElement.style.overflow = previousOverflow;
|
|
625
|
+
return { width, height };
|
|
626
|
+
}
|
|
627
|
+
_hasFixedContainerScrollbars() {
|
|
628
|
+
if (!this.containerElement)
|
|
629
|
+
return false;
|
|
630
|
+
const inlineOverflow = this.containerElement.style.overflow;
|
|
631
|
+
const inlineOverflowX = this.containerElement.style.overflowX;
|
|
632
|
+
const inlineOverflowY = this.containerElement.style.overflowY;
|
|
633
|
+
let computedOverflow = "";
|
|
634
|
+
let computedOverflowX = "";
|
|
635
|
+
let computedOverflowY = "";
|
|
636
|
+
if (typeof window !== "undefined" && typeof window.getComputedStyle === "function") {
|
|
637
|
+
const style = window.getComputedStyle(this.containerElement);
|
|
638
|
+
computedOverflow = style.overflow;
|
|
639
|
+
computedOverflowX = style.overflowX;
|
|
640
|
+
computedOverflowY = style.overflowY;
|
|
641
|
+
}
|
|
642
|
+
return [inlineOverflow, inlineOverflowX, inlineOverflowY, computedOverflow, computedOverflowX, computedOverflowY].some((value) => value === "scroll");
|
|
643
|
+
}
|
|
644
|
+
_getScrollbarSize() {
|
|
645
|
+
if (typeof document === "undefined" || !document.createElement || !document.body) {
|
|
646
|
+
return { width: 0, height: 0 };
|
|
647
|
+
}
|
|
648
|
+
const probe = document.createElement("div");
|
|
649
|
+
probe.style.position = "absolute";
|
|
650
|
+
probe.style.visibility = "hidden";
|
|
651
|
+
probe.style.overflow = "scroll";
|
|
652
|
+
probe.style.width = "100px";
|
|
653
|
+
probe.style.height = "100px";
|
|
654
|
+
probe.style.top = "-9999px";
|
|
655
|
+
document.body.appendChild(probe);
|
|
656
|
+
const width = Math.max(0, probe.offsetWidth - probe.clientWidth);
|
|
657
|
+
const height = Math.max(0, probe.offsetHeight - probe.clientHeight);
|
|
658
|
+
document.body.removeChild(probe);
|
|
659
|
+
return { width, height };
|
|
660
|
+
}
|
|
661
|
+
_getScrollSafetyMargin() {
|
|
662
|
+
return 2;
|
|
663
|
+
}
|
|
664
|
+
_getScrollableCanvasSize(contentWidth, contentHeight, viewport = this._getContainerViewportSize()) {
|
|
665
|
+
if (this._hasFixedContainerScrollbars()) {
|
|
666
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
667
|
+
const safeWidth = Math.max(1, viewport.width - safetyMargin);
|
|
668
|
+
const safeHeight = Math.max(1, viewport.height - safetyMargin);
|
|
669
|
+
return {
|
|
670
|
+
width: contentWidth > viewport.width + 0.5 ? this._ceilCanvasDimension(contentWidth) : safeWidth,
|
|
671
|
+
height: contentHeight > viewport.height + 0.5 ? this._ceilCanvasDimension(contentHeight) : safeHeight,
|
|
672
|
+
viewportWidth: viewport.width,
|
|
673
|
+
viewportHeight: viewport.height,
|
|
674
|
+
hasHorizontal: true,
|
|
675
|
+
hasVertical: true
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const scrollbar = this._getScrollbarSize();
|
|
679
|
+
let hasVertical = false;
|
|
680
|
+
let hasHorizontal = false;
|
|
681
|
+
let effectiveWidth = viewport.width;
|
|
682
|
+
let effectiveHeight = viewport.height;
|
|
683
|
+
for (let i = 0; i < 4; i += 1) {
|
|
684
|
+
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
685
|
+
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
686
|
+
const nextHasVertical = contentHeight > effectiveHeight + 0.5;
|
|
687
|
+
const nextHasHorizontal = contentWidth > effectiveWidth + 0.5;
|
|
688
|
+
if (nextHasVertical === hasVertical && nextHasHorizontal === hasHorizontal)
|
|
689
|
+
break;
|
|
690
|
+
hasVertical = nextHasVertical;
|
|
691
|
+
hasHorizontal = nextHasHorizontal;
|
|
692
|
+
}
|
|
693
|
+
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
694
|
+
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
695
|
+
return {
|
|
696
|
+
width: hasHorizontal ? this._ceilCanvasDimension(contentWidth) : effectiveWidth,
|
|
697
|
+
height: hasVertical ? this._ceilCanvasDimension(contentHeight) : effectiveHeight,
|
|
698
|
+
viewportWidth: effectiveWidth,
|
|
699
|
+
viewportHeight: effectiveHeight,
|
|
700
|
+
hasHorizontal,
|
|
701
|
+
hasVertical
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
_calculateCoverCanvasLayout(imageWidth, imageHeight) {
|
|
705
|
+
const viewport = this._getContainerViewportSize();
|
|
706
|
+
if (this._hasFixedContainerScrollbars()) {
|
|
707
|
+
const safetyMargin = this._getScrollSafetyMargin();
|
|
708
|
+
const targetWidth = Math.max(1, viewport.width - safetyMargin);
|
|
709
|
+
const targetHeight = Math.max(1, viewport.height - safetyMargin);
|
|
710
|
+
const scale2 = Math.min(1, Math.max(targetWidth / imageWidth, targetHeight / imageHeight));
|
|
711
|
+
const contentWidth2 = imageWidth * scale2;
|
|
712
|
+
const contentHeight2 = imageHeight * scale2;
|
|
713
|
+
const canvasSize2 = this._getScrollableCanvasSize(contentWidth2, contentHeight2, viewport);
|
|
714
|
+
return {
|
|
715
|
+
scale: scale2,
|
|
716
|
+
canvasWidth: canvasSize2.width,
|
|
717
|
+
canvasHeight: canvasSize2.height
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
const scrollbar = this._getScrollbarSize();
|
|
721
|
+
let hasVertical = false;
|
|
722
|
+
let hasHorizontal = false;
|
|
723
|
+
let scale = 1;
|
|
724
|
+
let contentWidth = imageWidth;
|
|
725
|
+
let contentHeight = imageHeight;
|
|
726
|
+
let effectiveWidth = viewport.width;
|
|
727
|
+
let effectiveHeight = viewport.height;
|
|
728
|
+
for (let i = 0; i < 4; i += 1) {
|
|
729
|
+
effectiveWidth = Math.max(1, viewport.width - (hasVertical ? scrollbar.width : 0));
|
|
730
|
+
effectiveHeight = Math.max(1, viewport.height - (hasHorizontal ? scrollbar.height : 0));
|
|
731
|
+
scale = Math.min(1, Math.max(effectiveWidth / imageWidth, effectiveHeight / imageHeight));
|
|
732
|
+
contentWidth = imageWidth * scale;
|
|
733
|
+
contentHeight = imageHeight * scale;
|
|
734
|
+
const nextHasVertical = contentHeight > effectiveHeight + 0.5;
|
|
735
|
+
const nextHasHorizontal = contentWidth > effectiveWidth + 0.5;
|
|
736
|
+
if (nextHasVertical === hasVertical && nextHasHorizontal === hasHorizontal)
|
|
737
|
+
break;
|
|
738
|
+
hasVertical = nextHasVertical;
|
|
739
|
+
hasHorizontal = nextHasHorizontal;
|
|
740
|
+
}
|
|
741
|
+
const canvasSize = this._getScrollableCanvasSize(contentWidth, contentHeight, viewport);
|
|
742
|
+
return {
|
|
743
|
+
scale,
|
|
744
|
+
canvasWidth: canvasSize.width,
|
|
745
|
+
canvasHeight: canvasSize.height
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
_getStateProperties() {
|
|
749
|
+
return [
|
|
750
|
+
"maskId",
|
|
751
|
+
"maskName",
|
|
752
|
+
"maskLabel",
|
|
753
|
+
"isCropRect",
|
|
754
|
+
"originalAlpha",
|
|
755
|
+
"originalStroke",
|
|
756
|
+
"originalStrokeWidth",
|
|
757
|
+
"selectable",
|
|
758
|
+
"evented",
|
|
759
|
+
"hasControls",
|
|
760
|
+
"lockRotation",
|
|
761
|
+
"borderColor",
|
|
762
|
+
"cornerColor",
|
|
763
|
+
"cornerSize",
|
|
764
|
+
"transparentCorners",
|
|
765
|
+
"strokeUniform",
|
|
766
|
+
"strokeDashArray"
|
|
767
|
+
];
|
|
768
|
+
}
|
|
769
|
+
_getMaskNormalStyle(mask) {
|
|
770
|
+
const strokeWidth = Number(mask && mask.originalStrokeWidth);
|
|
771
|
+
const opacity = Number(mask && mask.originalAlpha);
|
|
772
|
+
const style = {
|
|
773
|
+
stroke: mask && mask.originalStroke || "#ccc",
|
|
774
|
+
strokeWidth: Number.isFinite(strokeWidth) ? strokeWidth : 1
|
|
775
|
+
};
|
|
776
|
+
if (Number.isFinite(opacity))
|
|
777
|
+
style.opacity = opacity;
|
|
778
|
+
return style;
|
|
779
|
+
}
|
|
780
|
+
_withNormalizedMaskStyles(callback) {
|
|
781
|
+
if (!this.canvas)
|
|
782
|
+
return callback();
|
|
783
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
784
|
+
const maskStyleBackups = masks.map((mask) => ({
|
|
785
|
+
object: mask,
|
|
786
|
+
stroke: mask.stroke,
|
|
787
|
+
strokeWidth: mask.strokeWidth,
|
|
788
|
+
opacity: mask.opacity
|
|
789
|
+
}));
|
|
790
|
+
try {
|
|
791
|
+
masks.forEach((mask) => {
|
|
792
|
+
mask.set(this._getMaskNormalStyle(mask));
|
|
793
|
+
});
|
|
794
|
+
return callback();
|
|
795
|
+
} finally {
|
|
796
|
+
maskStyleBackups.forEach((backup) => {
|
|
797
|
+
try {
|
|
798
|
+
backup.object.set({
|
|
799
|
+
stroke: backup.stroke,
|
|
800
|
+
strokeWidth: backup.strokeWidth,
|
|
801
|
+
opacity: backup.opacity
|
|
802
|
+
});
|
|
803
|
+
} catch (error) {
|
|
804
|
+
}
|
|
805
|
+
});
|
|
531
806
|
}
|
|
532
807
|
}
|
|
808
|
+
_restoreMaskControls(mask) {
|
|
809
|
+
if (!mask)
|
|
810
|
+
return;
|
|
811
|
+
const cornerSize = Number(mask.cornerSize);
|
|
812
|
+
mask.set({
|
|
813
|
+
selectable: mask.selectable !== false,
|
|
814
|
+
evented: mask.evented !== false,
|
|
815
|
+
hasControls: mask.hasControls !== false,
|
|
816
|
+
lockRotation: typeof mask.lockRotation === "boolean" ? mask.lockRotation : !this.options.maskRotatable,
|
|
817
|
+
borderColor: mask.borderColor || "red",
|
|
818
|
+
cornerColor: mask.cornerColor || "black",
|
|
819
|
+
cornerSize: Number.isFinite(cornerSize) ? cornerSize : 8,
|
|
820
|
+
transparentCorners: mask.transparentCorners === true,
|
|
821
|
+
strokeUniform: mask.strokeUniform !== false
|
|
822
|
+
});
|
|
823
|
+
if (typeof mask.setCoords === "function")
|
|
824
|
+
mask.setCoords();
|
|
825
|
+
}
|
|
826
|
+
_serializeCanvasState() {
|
|
827
|
+
if (!this.canvas)
|
|
828
|
+
return null;
|
|
829
|
+
return this._withNormalizedMaskStyles(() => {
|
|
830
|
+
const jsonObject = this.canvas.toJSON(this._getStateProperties());
|
|
831
|
+
if (Array.isArray(jsonObject.objects)) {
|
|
832
|
+
jsonObject.objects = jsonObject.objects.filter((object) => !object.isCropRect && !object.maskLabel);
|
|
833
|
+
}
|
|
834
|
+
return JSON.stringify(jsonObject);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
_normalizeQuality(quality) {
|
|
838
|
+
const numericQuality = Number(quality);
|
|
839
|
+
if (!Number.isFinite(numericQuality))
|
|
840
|
+
return this.options.downsampleQuality ?? 0.92;
|
|
841
|
+
return Math.max(0, Math.min(1, numericQuality));
|
|
842
|
+
}
|
|
843
|
+
_normalizeImageFormat(format) {
|
|
844
|
+
const typeMapping = {
|
|
845
|
+
"jpeg": "jpeg",
|
|
846
|
+
"jpg": "jpeg",
|
|
847
|
+
"image/jpeg": "jpeg",
|
|
848
|
+
"png": "png",
|
|
849
|
+
"image/png": "png",
|
|
850
|
+
"webp": "webp",
|
|
851
|
+
"image/webp": "webp"
|
|
852
|
+
};
|
|
853
|
+
return typeMapping[String(format || "jpeg").toLowerCase()] || "jpeg";
|
|
854
|
+
}
|
|
855
|
+
_getClampedCanvasRegion(bounds, options = {}) {
|
|
856
|
+
const canvasWidth = Math.max(1, Math.round(this.canvas.getWidth()));
|
|
857
|
+
const canvasHeight = Math.max(1, Math.round(this.canvas.getHeight()));
|
|
858
|
+
const left = Number(bounds.left) || 0;
|
|
859
|
+
const top = Number(bounds.top) || 0;
|
|
860
|
+
const width = Math.max(0, Number(bounds.width) || 0);
|
|
861
|
+
const height = Math.max(0, Number(bounds.height) || 0);
|
|
862
|
+
const includePartialPixels = options.includePartialPixels !== false;
|
|
863
|
+
const roundEnd = includePartialPixels ? Math.ceil : Math.floor;
|
|
864
|
+
const sourceX = Math.min(canvasWidth - 1, Math.max(0, Math.floor(left)));
|
|
865
|
+
const sourceY = Math.min(canvasHeight - 1, Math.max(0, Math.floor(top)));
|
|
866
|
+
const endX = Math.min(canvasWidth, Math.max(sourceX + 1, roundEnd(left + width)));
|
|
867
|
+
const endY = Math.min(canvasHeight, Math.max(sourceY + 1, roundEnd(top + height)));
|
|
868
|
+
return {
|
|
869
|
+
sx: sourceX,
|
|
870
|
+
sy: sourceY,
|
|
871
|
+
sw: Math.max(1, endX - sourceX),
|
|
872
|
+
sh: Math.max(1, endY - sourceY)
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async _cropDataUrl(dataUrl, sourceX, sourceY, sourceWidth, sourceHeight, multiplier, format = "jpeg", quality = 0.92) {
|
|
876
|
+
return new Promise((resolve, reject) => {
|
|
877
|
+
const imageElement = new Image();
|
|
878
|
+
imageElement.onload = () => {
|
|
879
|
+
try {
|
|
880
|
+
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
881
|
+
const scaledSourceX = Math.round(sourceX * safeMultiplier);
|
|
882
|
+
const scaledSourceY = Math.round(sourceY * safeMultiplier);
|
|
883
|
+
const scaledSourceWidth = Math.max(1, Math.round(sourceWidth * safeMultiplier));
|
|
884
|
+
const scaledSourceHeight = Math.max(1, Math.round(sourceHeight * safeMultiplier));
|
|
885
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
886
|
+
offscreenCanvas.width = scaledSourceWidth;
|
|
887
|
+
offscreenCanvas.height = scaledSourceHeight;
|
|
888
|
+
const context = offscreenCanvas.getContext("2d");
|
|
889
|
+
context.drawImage(imageElement, scaledSourceX, scaledSourceY, scaledSourceWidth, scaledSourceHeight, 0, 0, scaledSourceWidth, scaledSourceHeight);
|
|
890
|
+
resolve(offscreenCanvas.toDataURL(`image/${format}`, quality));
|
|
891
|
+
} catch (error) {
|
|
892
|
+
reject(error);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
imageElement.onerror = reject;
|
|
896
|
+
imageElement.src = dataUrl;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
async _exportCanvasRegionToDataURL({ sx, sy, sw, sh, multiplier = 1, quality = 0.92, format = "jpeg" }) {
|
|
900
|
+
const safeMultiplier = Math.max(1, Number(multiplier) || 1);
|
|
901
|
+
const fullDataUrl = this.canvas.toDataURL({
|
|
902
|
+
format,
|
|
903
|
+
quality,
|
|
904
|
+
multiplier: safeMultiplier
|
|
905
|
+
});
|
|
906
|
+
return this._cropDataUrl(fullDataUrl, sx, sy, sw, sh, safeMultiplier, format, quality);
|
|
907
|
+
}
|
|
533
908
|
/**
|
|
534
909
|
* Gets the top-left corner coordinates of the given object.
|
|
535
910
|
* Used for geometry calculations (e.g., scale, rotate).
|
|
536
911
|
*
|
|
537
|
-
* @param {Object}
|
|
912
|
+
* @param {Object} fabricObject - The object for which to get the top-left coordinates. Should support setCoords and getCoords/getBoundingRect methods.
|
|
538
913
|
* @returns {{x: number, y: number}} The top-left corner point as an object with x and y properties.
|
|
539
914
|
* @private
|
|
540
915
|
*/
|
|
541
|
-
_getObjectTopLeftPoint(
|
|
542
|
-
if (!
|
|
916
|
+
_getObjectTopLeftPoint(fabricObject) {
|
|
917
|
+
if (!fabricObject)
|
|
543
918
|
return { x: 0, y: 0 };
|
|
544
|
-
|
|
545
|
-
const coords = typeof
|
|
919
|
+
fabricObject.setCoords();
|
|
920
|
+
const coords = typeof fabricObject.getCoords === "function" ? fabricObject.getCoords() : null;
|
|
546
921
|
if (coords && coords.length)
|
|
547
922
|
return coords[0];
|
|
548
|
-
const
|
|
549
|
-
return { x:
|
|
923
|
+
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
924
|
+
return { x: boundingRect.left, y: boundingRect.top };
|
|
550
925
|
}
|
|
551
926
|
/**
|
|
552
927
|
* Sets the object's origin at the specified origin point, keeping a reference point fixed in position.
|
|
553
928
|
*
|
|
554
|
-
* @param {Object}
|
|
929
|
+
* @param {Object} fabricObject - The object to modify. Should support set, setPositionByOrigin, and setCoords.
|
|
555
930
|
* @param {string} originX - The new originX ("left", "center", "right", etc.).
|
|
556
931
|
* @param {string} originY - The new originY ("top", "center", "bottom", etc.).
|
|
557
932
|
* @param {{x: number, y: number}} refPoint - The point to keep fixed while setting the new origins.
|
|
558
933
|
* @private
|
|
559
934
|
*/
|
|
560
|
-
_setObjectOriginKeepingPosition(
|
|
561
|
-
if (!
|
|
935
|
+
_setObjectOriginKeepingPosition(fabricObject, originX, originY, refPoint) {
|
|
936
|
+
if (!fabricObject || !refPoint || !fabricObject.setPositionByOrigin)
|
|
562
937
|
return;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
938
|
+
fabricObject.set({ originX, originY });
|
|
939
|
+
fabricObject.setPositionByOrigin(refPoint, originX, originY);
|
|
940
|
+
fabricObject.setCoords();
|
|
566
941
|
}
|
|
567
942
|
/**
|
|
568
943
|
* Moves the object so its bounding box aligns with the canvas's top-left corner (0, 0).
|
|
569
944
|
*
|
|
570
|
-
* @param {Object}
|
|
945
|
+
* @param {Object} fabricObject - The object to align.
|
|
571
946
|
* @private
|
|
572
947
|
*/
|
|
573
|
-
_alignObjectBoundingBoxToCanvasTopLeft(
|
|
574
|
-
if (!
|
|
948
|
+
_alignObjectBoundingBoxToCanvasTopLeft(fabricObject) {
|
|
949
|
+
if (!fabricObject)
|
|
575
950
|
return;
|
|
576
|
-
|
|
577
|
-
const
|
|
578
|
-
const
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
951
|
+
fabricObject.setCoords();
|
|
952
|
+
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
953
|
+
const deltaX = boundingRect.left;
|
|
954
|
+
const deltaY = boundingRect.top;
|
|
955
|
+
fabricObject.set({ left: (fabricObject.left || 0) - deltaX, top: (fabricObject.top || 0) - deltaY });
|
|
956
|
+
fabricObject.setCoords();
|
|
582
957
|
this.canvas.renderAll();
|
|
583
958
|
}
|
|
584
959
|
/**
|
|
@@ -590,16 +965,26 @@ var ImageEditor = class {
|
|
|
590
965
|
if (!this.originalImage)
|
|
591
966
|
return;
|
|
592
967
|
this.originalImage.setCoords();
|
|
593
|
-
const
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
968
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
969
|
+
const size = this._getScrollableCanvasSize(imageBounds.width, imageBounds.height);
|
|
970
|
+
this._setCanvasSizeInt(size.width, size.height);
|
|
971
|
+
}
|
|
972
|
+
_expandCanvasToFitObject(fabricObject, padding = 10) {
|
|
973
|
+
if (!this.canvas || !fabricObject || !this.options.expandCanvasToImage)
|
|
598
974
|
return;
|
|
975
|
+
try {
|
|
976
|
+
fabricObject.setCoords();
|
|
977
|
+
const boundingRect = fabricObject.getBoundingRect(true, true);
|
|
978
|
+
const requiredWidth = Math.ceil(boundingRect.left + boundingRect.width + padding);
|
|
979
|
+
const requiredHeight = Math.ceil(boundingRect.top + boundingRect.height + padding);
|
|
980
|
+
const minWidth = this.containerElement ? Math.floor(this.containerElement.clientWidth || 0) : 0;
|
|
981
|
+
const minHeight = this.containerElement ? Math.floor(this.containerElement.clientHeight || 0) : 0;
|
|
982
|
+
const newWidth = Math.max(this.canvas.getWidth(), minWidth, requiredWidth);
|
|
983
|
+
const newHeight = Math.max(this.canvas.getHeight(), minHeight, requiredHeight);
|
|
984
|
+
this._setCanvasSizeInt(newWidth, newHeight);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
this._reportWarning("expandCanvasToFitObject: failed to expand canvas", error);
|
|
599
987
|
}
|
|
600
|
-
const newW = Math.max(containerW || 0, Math.floor(br.width));
|
|
601
|
-
const newH = Math.max(containerH || 0, Math.floor(br.height));
|
|
602
|
-
this._setCanvasSizeInt(newW, newH);
|
|
603
988
|
}
|
|
604
989
|
/**
|
|
605
990
|
* Scales the original image by a given factor, with animation.
|
|
@@ -608,8 +993,8 @@ var ImageEditor = class {
|
|
|
608
993
|
* @returns {Promise<void>} Promise that resolves once the scaling animation finishes.
|
|
609
994
|
* @public
|
|
610
995
|
*/
|
|
611
|
-
scaleImage(factor) {
|
|
612
|
-
return this.animQueue.add(() => this._scaleImageImpl(factor));
|
|
996
|
+
scaleImage(factor, options = {}) {
|
|
997
|
+
return this.animQueue.add(() => this._scaleImageImpl(factor, options));
|
|
613
998
|
}
|
|
614
999
|
/**
|
|
615
1000
|
* Scales the original image by a given factor, with animation.
|
|
@@ -618,46 +1003,49 @@ var ImageEditor = class {
|
|
|
618
1003
|
* @returns {Promise<void>} Promise that resolves once the scaling animation finishes.
|
|
619
1004
|
* @private
|
|
620
1005
|
*/
|
|
621
|
-
_scaleImageImpl(factor) {
|
|
1006
|
+
_scaleImageImpl(factor, options = {}) {
|
|
622
1007
|
if (!this.originalImage)
|
|
623
1008
|
return Promise.resolve();
|
|
624
1009
|
if (this.isAnimating)
|
|
625
1010
|
return Promise.resolve();
|
|
1011
|
+
const saveHistory = options.saveHistory !== false;
|
|
626
1012
|
factor = Math.max(this.options.minScale, Math.min(this.options.maxScale, factor));
|
|
627
1013
|
this.currentScale = factor;
|
|
628
1014
|
this.isAnimating = true;
|
|
629
1015
|
this._updateUI();
|
|
630
|
-
const
|
|
1016
|
+
const targetScale = this.baseImageScale * factor;
|
|
631
1017
|
const topLeft = this._getObjectTopLeftPoint(this.originalImage);
|
|
632
1018
|
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", topLeft);
|
|
633
|
-
const
|
|
634
|
-
this.originalImage.animate("scaleX",
|
|
1019
|
+
const scaleXAnimation = new Promise((resolve) => {
|
|
1020
|
+
this.originalImage.animate("scaleX", targetScale, {
|
|
635
1021
|
duration: this.options.animationDuration,
|
|
636
1022
|
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
637
|
-
onComplete:
|
|
1023
|
+
onComplete: resolve
|
|
638
1024
|
});
|
|
639
1025
|
});
|
|
640
|
-
const
|
|
641
|
-
this.originalImage.animate("scaleY",
|
|
1026
|
+
const scaleYAnimation = new Promise((resolve) => {
|
|
1027
|
+
this.originalImage.animate("scaleY", targetScale, {
|
|
642
1028
|
duration: this.options.animationDuration,
|
|
643
1029
|
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
644
|
-
onComplete:
|
|
1030
|
+
onComplete: resolve
|
|
645
1031
|
});
|
|
646
1032
|
});
|
|
647
|
-
return Promise.all([
|
|
648
|
-
this.originalImage.set({ scaleX:
|
|
1033
|
+
return Promise.all([scaleXAnimation, scaleYAnimation]).then(() => {
|
|
1034
|
+
this.originalImage.set({ scaleX: targetScale, scaleY: targetScale });
|
|
649
1035
|
this.originalImage.setCoords();
|
|
650
|
-
if (this.options.expandCanvasToImage)
|
|
1036
|
+
if (this.options.expandCanvasToImage || this.options.coverImageToCanvas) {
|
|
651
1037
|
this._updateCanvasSizeToImageBounds();
|
|
1038
|
+
}
|
|
652
1039
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
653
|
-
this.canvas.getObjects().forEach((
|
|
654
|
-
if (
|
|
655
|
-
this._syncMaskLabel(
|
|
1040
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1041
|
+
if (object.maskId)
|
|
1042
|
+
this._syncMaskLabel(object);
|
|
656
1043
|
});
|
|
657
1044
|
this.isAnimating = false;
|
|
658
1045
|
this._updateInputs();
|
|
659
1046
|
this._updateUI();
|
|
660
|
-
|
|
1047
|
+
if (saveHistory)
|
|
1048
|
+
this.saveState();
|
|
661
1049
|
}).catch(() => {
|
|
662
1050
|
this.isAnimating = false;
|
|
663
1051
|
this._updateUI();
|
|
@@ -670,8 +1058,8 @@ var ImageEditor = class {
|
|
|
670
1058
|
* @returns {Promise<void>} Promise that resolves once the rotation animation finishes.
|
|
671
1059
|
* @public
|
|
672
1060
|
*/
|
|
673
|
-
rotateImage(
|
|
674
|
-
return this.animQueue.add(() => this._rotateImageImpl(
|
|
1061
|
+
rotateImage(degrees, options = {}) {
|
|
1062
|
+
return this.animQueue.add(() => this._rotateImageImpl(degrees, options));
|
|
675
1063
|
}
|
|
676
1064
|
/**
|
|
677
1065
|
* Rotates the original image by a given number of degrees, with animation.
|
|
@@ -680,97 +1068,129 @@ var ImageEditor = class {
|
|
|
680
1068
|
* @returns {Promise<void>} Promise that resolves once the rotation animation finishes.
|
|
681
1069
|
* @private
|
|
682
1070
|
*/
|
|
683
|
-
_rotateImageImpl(degrees) {
|
|
1071
|
+
_rotateImageImpl(degrees, options = {}) {
|
|
684
1072
|
if (!this.originalImage)
|
|
685
1073
|
return Promise.resolve();
|
|
686
1074
|
if (this.isAnimating)
|
|
687
1075
|
return Promise.resolve();
|
|
688
1076
|
if (isNaN(degrees))
|
|
689
1077
|
return Promise.resolve();
|
|
1078
|
+
const saveHistory = options.saveHistory !== false;
|
|
690
1079
|
this.currentRotation = degrees;
|
|
691
1080
|
this.isAnimating = true;
|
|
692
1081
|
this._updateUI();
|
|
693
1082
|
const center = this.originalImage.getCenterPoint();
|
|
694
1083
|
this._setObjectOriginKeepingPosition(this.originalImage, "center", "center", center);
|
|
695
|
-
const
|
|
1084
|
+
const rotationAnimation = new Promise((resolve) => {
|
|
696
1085
|
this.originalImage.animate("angle", degrees, {
|
|
697
1086
|
duration: this.options.animationDuration,
|
|
698
1087
|
onChange: this.canvas.renderAll.bind(this.canvas),
|
|
699
|
-
onComplete:
|
|
1088
|
+
onComplete: resolve
|
|
700
1089
|
});
|
|
701
1090
|
});
|
|
702
|
-
return
|
|
1091
|
+
return rotationAnimation.then(() => {
|
|
703
1092
|
this.originalImage.set("angle", degrees);
|
|
704
1093
|
this.originalImage.setCoords();
|
|
705
|
-
if (this.options.expandCanvasToImage)
|
|
1094
|
+
if (this.options.expandCanvasToImage || this.options.coverImageToCanvas) {
|
|
706
1095
|
this._updateCanvasSizeToImageBounds();
|
|
1096
|
+
}
|
|
707
1097
|
this._alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
|
|
708
1098
|
const newTopLeft = this._getObjectTopLeftPoint(this.originalImage);
|
|
709
1099
|
this._setObjectOriginKeepingPosition(this.originalImage, "left", "top", newTopLeft);
|
|
710
|
-
this.canvas.getObjects().forEach((
|
|
711
|
-
if (
|
|
712
|
-
this._syncMaskLabel(
|
|
1100
|
+
this.canvas.getObjects().forEach((object) => {
|
|
1101
|
+
if (object.maskId)
|
|
1102
|
+
this._syncMaskLabel(object);
|
|
713
1103
|
});
|
|
714
1104
|
this.isAnimating = false;
|
|
715
1105
|
this._updateInputs();
|
|
716
1106
|
this._updateUI();
|
|
717
|
-
|
|
1107
|
+
if (saveHistory)
|
|
1108
|
+
this.saveState();
|
|
718
1109
|
}).catch(() => {
|
|
719
1110
|
this.isAnimating = false;
|
|
720
1111
|
this._updateUI();
|
|
721
1112
|
});
|
|
722
1113
|
}
|
|
723
1114
|
/**
|
|
724
|
-
* Resets the image: scales to 1 and rotates to 0 degrees.
|
|
1115
|
+
* Resets the image transform: scales to 1 and rotates to 0 degrees.
|
|
725
1116
|
* @returns {Promise<void>} Promise that resolves when reset is complete.
|
|
726
1117
|
*/
|
|
727
|
-
|
|
1118
|
+
resetImageTransform() {
|
|
728
1119
|
if (!this.originalImage)
|
|
729
1120
|
return Promise.resolve();
|
|
730
|
-
return this.
|
|
731
|
-
this.
|
|
1121
|
+
return this.animQueue.add(async () => {
|
|
1122
|
+
const before = this._serializeCanvasState();
|
|
1123
|
+
await this._scaleImageImpl(1, { saveHistory: false });
|
|
1124
|
+
await this._rotateImageImpl(0, { saveHistory: false });
|
|
1125
|
+
const after = this._serializeCanvasState();
|
|
1126
|
+
this._pushStateTransition(before, after);
|
|
732
1127
|
}).catch((err) => {
|
|
733
|
-
this._reportError("
|
|
1128
|
+
this._reportError("resetImageTransform() failed", err);
|
|
734
1129
|
});
|
|
735
1130
|
}
|
|
1131
|
+
/**
|
|
1132
|
+
* @deprecated Use resetImageTransform() instead.
|
|
1133
|
+
*/
|
|
1134
|
+
reset() {
|
|
1135
|
+
return this.resetImageTransform();
|
|
1136
|
+
}
|
|
736
1137
|
/**
|
|
737
1138
|
* Restores a canvas state that was previously stored by saveState().
|
|
738
1139
|
* @param {string} jsonString - the JSON string returned by fabric.toJSON().
|
|
739
1140
|
*/
|
|
740
1141
|
loadFromState(jsonString) {
|
|
741
1142
|
if (!jsonString || !this.canvas)
|
|
742
|
-
return;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
this.originalImage
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1143
|
+
return Promise.resolve();
|
|
1144
|
+
return new Promise((resolve) => {
|
|
1145
|
+
try {
|
|
1146
|
+
const json = typeof jsonString === "string" ? JSON.parse(jsonString) : jsonString;
|
|
1147
|
+
this.canvas.loadFromJSON(json, () => {
|
|
1148
|
+
try {
|
|
1149
|
+
this._hideAllMaskLabels();
|
|
1150
|
+
const canvasObjects = this.canvas.getObjects();
|
|
1151
|
+
this.originalImage = canvasObjects.find((object) => object.type === "image" && !object.maskId) || null;
|
|
1152
|
+
if (this.originalImage) {
|
|
1153
|
+
this.originalImage.set({ originX: "left", originY: "top", selectable: false, evented: false, hasControls: false, hoverCursor: "default" });
|
|
1154
|
+
this.canvas.sendToBack(this.originalImage);
|
|
1155
|
+
this.currentRotation = Number(this.originalImage.angle) || 0;
|
|
1156
|
+
const baseScale = Number(this.baseImageScale) || 1;
|
|
1157
|
+
const imageScale = Number(this.originalImage.scaleX) || baseScale;
|
|
1158
|
+
this.currentScale = imageScale / baseScale;
|
|
1159
|
+
} else {
|
|
1160
|
+
this.currentScale = 1;
|
|
1161
|
+
this.currentRotation = 0;
|
|
1162
|
+
}
|
|
1163
|
+
const masks = canvasObjects.filter((object) => object.maskId);
|
|
1164
|
+
masks.forEach((mask) => {
|
|
1165
|
+
this._restoreMaskControls(mask);
|
|
1166
|
+
this._rebindMaskEvents(mask);
|
|
1167
|
+
mask.set(this._getMaskNormalStyle(mask));
|
|
1168
|
+
});
|
|
1169
|
+
this.maskCounter = masks.reduce((max, mask) => Math.max(max, mask.maskId), 0);
|
|
1170
|
+
this._lastMask = masks.length ? masks[masks.length - 1] : null;
|
|
1171
|
+
if (!this._lastMask) {
|
|
1172
|
+
this._lastMaskInitialLeft = null;
|
|
1173
|
+
this._lastMaskInitialTop = null;
|
|
1174
|
+
this._lastMaskInitialWidth = null;
|
|
1175
|
+
}
|
|
1176
|
+
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
1177
|
+
this.canvas.renderAll();
|
|
1178
|
+
this._updateInputs();
|
|
1179
|
+
this._updateMaskList();
|
|
1180
|
+
this._updatePlaceholderStatus();
|
|
1181
|
+
this._lastSnapshot = this._serializeCanvasState();
|
|
1182
|
+
this._updateUI();
|
|
1183
|
+
} catch (callbackError) {
|
|
1184
|
+
this._reportError("loadFromState() failed", callbackError);
|
|
1185
|
+
} finally {
|
|
1186
|
+
resolve();
|
|
761
1187
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
this._reportError("loadFromState() failed", callbackError);
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
} catch (e) {
|
|
772
|
-
this._reportError("loadFromState() failed", e);
|
|
773
|
-
}
|
|
1188
|
+
});
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
this._reportError("loadFromState() failed", error);
|
|
1191
|
+
resolve();
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
774
1194
|
}
|
|
775
1195
|
/**
|
|
776
1196
|
* Saves the current state of the canvas to history, storing any mask/raster label information.
|
|
@@ -778,51 +1198,117 @@ var ImageEditor = class {
|
|
|
778
1198
|
saveState() {
|
|
779
1199
|
if (!this.canvas)
|
|
780
1200
|
return;
|
|
781
|
-
const
|
|
1201
|
+
const activeObject = this.canvas.getActiveObject();
|
|
782
1202
|
this._hideAllMaskLabels();
|
|
783
1203
|
try {
|
|
784
|
-
const
|
|
785
|
-
if (Array.isArray(jsonObj.objects)) {
|
|
786
|
-
jsonObj.objects = jsonObj.objects.filter((o) => !o.isCropRect);
|
|
787
|
-
}
|
|
788
|
-
const after = JSON.stringify(jsonObj);
|
|
1204
|
+
const after = this._serializeCanvasState();
|
|
789
1205
|
const before = this._lastSnapshot || after;
|
|
1206
|
+
if (after === before)
|
|
1207
|
+
return;
|
|
790
1208
|
let executedOnce = false;
|
|
791
|
-
const
|
|
1209
|
+
const command = new Command(
|
|
792
1210
|
() => {
|
|
793
1211
|
if (executedOnce) {
|
|
794
|
-
this.loadFromState(after);
|
|
1212
|
+
return this.loadFromState(after);
|
|
795
1213
|
}
|
|
796
1214
|
executedOnce = true;
|
|
1215
|
+
return void 0;
|
|
797
1216
|
},
|
|
798
|
-
() =>
|
|
799
|
-
this.loadFromState(before);
|
|
800
|
-
}
|
|
1217
|
+
() => this.loadFromState(before)
|
|
801
1218
|
);
|
|
802
|
-
this.historyManager.execute(
|
|
1219
|
+
this.historyManager.execute(command);
|
|
803
1220
|
this._lastSnapshot = after;
|
|
804
|
-
|
|
805
|
-
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
this._reportWarning("saveState: failed to save canvas snapshot", error);
|
|
1223
|
+
} finally {
|
|
1224
|
+
if (activeObject && activeObject.maskId && this.canvas.getObjects().includes(activeObject)) {
|
|
1225
|
+
this._handleSelectionChanged([activeObject]);
|
|
806
1226
|
}
|
|
807
1227
|
this._updateUI();
|
|
808
|
-
} catch (err) {
|
|
809
|
-
this._reportWarning("saveState: failed to save canvas snapshot", err);
|
|
810
1228
|
}
|
|
811
1229
|
}
|
|
1230
|
+
_pushStateTransition(before, after) {
|
|
1231
|
+
if (!before || !after)
|
|
1232
|
+
return;
|
|
1233
|
+
if (before === after)
|
|
1234
|
+
return;
|
|
1235
|
+
if (!this.historyManager)
|
|
1236
|
+
this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
1237
|
+
const command = new Command(
|
|
1238
|
+
() => this.loadFromState(after),
|
|
1239
|
+
() => this.loadFromState(before)
|
|
1240
|
+
);
|
|
1241
|
+
this.historyManager.push(command);
|
|
1242
|
+
this._lastSnapshot = after;
|
|
1243
|
+
this._updateUI();
|
|
1244
|
+
}
|
|
812
1245
|
/**
|
|
813
1246
|
* Undo the last state change, if possible.
|
|
814
1247
|
*/
|
|
815
1248
|
undo() {
|
|
816
|
-
this.historyManager.undo()
|
|
1249
|
+
return this.historyManager.undo().then(() => {
|
|
1250
|
+
this._updateUI();
|
|
1251
|
+
}).catch((error) => {
|
|
1252
|
+
this._reportError("undo failed", error);
|
|
1253
|
+
});
|
|
817
1254
|
}
|
|
818
1255
|
/**
|
|
819
1256
|
* Redo the next state change, if possible.
|
|
820
1257
|
*/
|
|
821
1258
|
redo() {
|
|
822
|
-
this.historyManager.redo()
|
|
1259
|
+
return this.historyManager.redo().then(() => {
|
|
1260
|
+
this._updateUI();
|
|
1261
|
+
}).catch((error) => {
|
|
1262
|
+
this._reportError("redo failed", error);
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
_rebindMaskEvents(mask) {
|
|
1266
|
+
if (!mask)
|
|
1267
|
+
return;
|
|
1268
|
+
if (mask.__imageEditorMaskHandlers) {
|
|
1269
|
+
try {
|
|
1270
|
+
mask.off("mouseover", mask.__imageEditorMaskHandlers.mouseover);
|
|
1271
|
+
mask.off("mouseout", mask.__imageEditorMaskHandlers.mouseout);
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const metadata = {};
|
|
1276
|
+
if (!Number.isFinite(Number(mask.originalAlpha))) {
|
|
1277
|
+
metadata.originalAlpha = Number.isFinite(Number(mask.opacity)) ? Number(mask.opacity) : 0.5;
|
|
1278
|
+
}
|
|
1279
|
+
if (!mask.originalStroke)
|
|
1280
|
+
metadata.originalStroke = mask.stroke || "#ccc";
|
|
1281
|
+
if (!Number.isFinite(Number(mask.originalStrokeWidth))) {
|
|
1282
|
+
metadata.originalStrokeWidth = Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1;
|
|
1283
|
+
}
|
|
1284
|
+
if (Object.keys(metadata).length)
|
|
1285
|
+
mask.set(metadata);
|
|
1286
|
+
const normalStyle = {
|
|
1287
|
+
stroke: mask.originalStroke || "#ccc",
|
|
1288
|
+
strokeWidth: mask.originalStrokeWidth,
|
|
1289
|
+
opacity: mask.originalAlpha
|
|
1290
|
+
};
|
|
1291
|
+
const hoverStyle = {
|
|
1292
|
+
stroke: "#ff5500",
|
|
1293
|
+
strokeWidth: 2,
|
|
1294
|
+
opacity: Math.min(mask.originalAlpha + 0.2, 1)
|
|
1295
|
+
};
|
|
1296
|
+
const mouseover = () => {
|
|
1297
|
+
mask.set(hoverStyle);
|
|
1298
|
+
if (mask.canvas)
|
|
1299
|
+
mask.canvas.requestRenderAll();
|
|
1300
|
+
};
|
|
1301
|
+
const mouseout = () => {
|
|
1302
|
+
mask.set(normalStyle);
|
|
1303
|
+
if (mask.canvas)
|
|
1304
|
+
mask.canvas.requestRenderAll();
|
|
1305
|
+
};
|
|
1306
|
+
mask.on("mouseover", mouseover);
|
|
1307
|
+
mask.on("mouseout", mouseout);
|
|
1308
|
+
mask.__imageEditorMaskHandlers = { mouseover, mouseout };
|
|
823
1309
|
}
|
|
824
1310
|
/**
|
|
825
|
-
*
|
|
1311
|
+
* Creates a mask and adds it to the canvas.
|
|
826
1312
|
* Mask placement and properties are determined by the provided config and instance options.
|
|
827
1313
|
* Canvas and list UI are updated accordingly.
|
|
828
1314
|
* @param {Object} [config={}] - Optional mask configuration overrides:
|
|
@@ -836,15 +1322,15 @@ var ImageEditor = class {
|
|
|
836
1322
|
* @param {boolean} [config.selectable=true]
|
|
837
1323
|
* @param {Object} [config.styles] - Custom styles (stroke, dashArray, etc)
|
|
838
1324
|
* @param {function} [config.onCreate] - Callback after mask created (receives Fabric object)
|
|
839
|
-
* @param {function} [config.fabricGenerator] - (
|
|
1325
|
+
* @param {function} [config.fabricGenerator] - (maskConfig) => new FabricObj
|
|
840
1326
|
* @returns {fabric.Rect|null} The created mask object, or null if canvas is not available.
|
|
841
1327
|
* @public
|
|
842
1328
|
*/
|
|
843
|
-
|
|
1329
|
+
createMask(config = {}) {
|
|
844
1330
|
if (!this.canvas)
|
|
845
1331
|
return null;
|
|
846
1332
|
const shapeType = config.shape || "rect";
|
|
847
|
-
const
|
|
1333
|
+
const maskConfig = {
|
|
848
1334
|
shape: shapeType,
|
|
849
1335
|
width: this.options.defaultMaskWidth,
|
|
850
1336
|
height: this.options.defaultMaskHeight,
|
|
@@ -860,80 +1346,71 @@ var ImageEditor = class {
|
|
|
860
1346
|
const firstOffset = 10;
|
|
861
1347
|
let left = firstOffset;
|
|
862
1348
|
let top = firstOffset;
|
|
863
|
-
const resolveValue = (
|
|
864
|
-
if (typeof
|
|
865
|
-
return
|
|
866
|
-
if (typeof
|
|
867
|
-
const percent = parseFloat(
|
|
1349
|
+
const resolveValue = (value, fallback) => {
|
|
1350
|
+
if (typeof value === "function")
|
|
1351
|
+
return value(this.canvas, this.options);
|
|
1352
|
+
if (typeof value === "string" && value.endsWith("%")) {
|
|
1353
|
+
const percent = parseFloat(value) / 100;
|
|
868
1354
|
return Math.floor((this.canvas ? this.canvas.getWidth() : 0) * percent);
|
|
869
1355
|
}
|
|
870
|
-
return
|
|
1356
|
+
return value != null ? value : fallback;
|
|
871
1357
|
};
|
|
872
|
-
if (
|
|
873
|
-
const
|
|
874
|
-
let
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
} else if (
|
|
878
|
-
|
|
1358
|
+
if (maskConfig.left === void 0 && this._lastMask) {
|
|
1359
|
+
const previousMask = this._lastMask;
|
|
1360
|
+
let previousMaskRight = previousMask.left;
|
|
1361
|
+
if (previousMask.getScaledWidth) {
|
|
1362
|
+
previousMaskRight += previousMask.getScaledWidth();
|
|
1363
|
+
} else if (previousMask.width) {
|
|
1364
|
+
previousMaskRight += previousMask.width * (previousMask.scaleX ?? 1);
|
|
879
1365
|
}
|
|
880
|
-
left = Math.round(
|
|
881
|
-
top =
|
|
1366
|
+
left = Math.round(previousMaskRight + maskConfig.gap);
|
|
1367
|
+
top = previousMask.top ?? firstOffset;
|
|
882
1368
|
} else {
|
|
883
|
-
left = resolveValue(
|
|
884
|
-
top = resolveValue(
|
|
885
|
-
}
|
|
886
|
-
cfg.width = resolveValue(cfg.width, this.options.defaultMaskWidth);
|
|
887
|
-
cfg.height = resolveValue(cfg.height, this.options.defaultMaskHeight);
|
|
888
|
-
if (this.options.expandCanvasToImage && shapeType === "rect") {
|
|
889
|
-
const requiredW = Math.ceil(left + cfg.width + 10);
|
|
890
|
-
const requiredH = Math.ceil(top + cfg.height + 10);
|
|
891
|
-
const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || 0) : 0;
|
|
892
|
-
const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || 0) : 0;
|
|
893
|
-
const newW = Math.max(this.canvas.getWidth(), minW, requiredW);
|
|
894
|
-
const newH = Math.max(this.canvas.getHeight(), minH, requiredH);
|
|
895
|
-
this._setCanvasSizeInt(newW, newH);
|
|
1369
|
+
left = resolveValue(maskConfig.left, firstOffset);
|
|
1370
|
+
top = resolveValue(maskConfig.top, firstOffset);
|
|
896
1371
|
}
|
|
1372
|
+
maskConfig.width = resolveValue(maskConfig.width, this.options.defaultMaskWidth);
|
|
1373
|
+
maskConfig.height = resolveValue(maskConfig.height, this.options.defaultMaskHeight);
|
|
897
1374
|
let mask;
|
|
898
|
-
if (typeof
|
|
899
|
-
mask =
|
|
1375
|
+
if (typeof maskConfig.fabricGenerator === "function") {
|
|
1376
|
+
mask = maskConfig.fabricGenerator(maskConfig, this.canvas, this.options);
|
|
900
1377
|
} else {
|
|
901
1378
|
switch (shapeType) {
|
|
902
1379
|
case "circle":
|
|
903
1380
|
mask = new fabric.Circle({
|
|
904
1381
|
left,
|
|
905
1382
|
top,
|
|
906
|
-
radius: resolveValue(
|
|
907
|
-
fill:
|
|
908
|
-
opacity:
|
|
909
|
-
angle:
|
|
910
|
-
...
|
|
1383
|
+
radius: resolveValue(maskConfig.radius, Math.min(maskConfig.width, maskConfig.height) / 2),
|
|
1384
|
+
fill: maskConfig.color,
|
|
1385
|
+
opacity: maskConfig.alpha,
|
|
1386
|
+
angle: maskConfig.angle,
|
|
1387
|
+
...maskConfig.styles
|
|
911
1388
|
});
|
|
912
1389
|
break;
|
|
913
1390
|
case "ellipse":
|
|
914
1391
|
mask = new fabric.Ellipse({
|
|
915
1392
|
left,
|
|
916
1393
|
top,
|
|
917
|
-
rx: resolveValue(
|
|
918
|
-
ry: resolveValue(
|
|
919
|
-
fill:
|
|
920
|
-
opacity:
|
|
921
|
-
angle:
|
|
922
|
-
...
|
|
1394
|
+
rx: resolveValue(maskConfig.rx, maskConfig.width / 2),
|
|
1395
|
+
ry: resolveValue(maskConfig.ry, maskConfig.height / 2),
|
|
1396
|
+
fill: maskConfig.color,
|
|
1397
|
+
opacity: maskConfig.alpha,
|
|
1398
|
+
angle: maskConfig.angle,
|
|
1399
|
+
...maskConfig.styles
|
|
923
1400
|
});
|
|
924
1401
|
break;
|
|
925
1402
|
case "polygon": {
|
|
926
|
-
let
|
|
927
|
-
if (Array.isArray(
|
|
928
|
-
|
|
1403
|
+
let polygonPoints = maskConfig.points || [];
|
|
1404
|
+
if (Array.isArray(polygonPoints) && polygonPoints.length && typeof polygonPoints[0] === "object") {
|
|
1405
|
+
polygonPoints = polygonPoints.map((point) => ({ x: Number(point.x), y: Number(point.y) }));
|
|
929
1406
|
}
|
|
930
|
-
mask = new fabric.Polygon(
|
|
1407
|
+
mask = new fabric.Polygon(polygonPoints, {
|
|
931
1408
|
left,
|
|
932
1409
|
top,
|
|
933
|
-
fill:
|
|
934
|
-
opacity:
|
|
935
|
-
angle:
|
|
936
|
-
...
|
|
1410
|
+
fill: maskConfig.color,
|
|
1411
|
+
opacity: maskConfig.alpha,
|
|
1412
|
+
angle: maskConfig.angle,
|
|
1413
|
+
...maskConfig.styles
|
|
937
1414
|
});
|
|
938
1415
|
break;
|
|
939
1416
|
}
|
|
@@ -942,80 +1419,92 @@ var ImageEditor = class {
|
|
|
942
1419
|
mask = new fabric.Rect({
|
|
943
1420
|
left,
|
|
944
1421
|
top,
|
|
945
|
-
width: resolveValue(
|
|
946
|
-
height: resolveValue(
|
|
947
|
-
fill:
|
|
948
|
-
opacity:
|
|
949
|
-
angle:
|
|
950
|
-
rx:
|
|
1422
|
+
width: resolveValue(maskConfig.width, this.options.defaultMaskWidth),
|
|
1423
|
+
height: resolveValue(maskConfig.height, this.options.defaultMaskHeight),
|
|
1424
|
+
fill: maskConfig.color,
|
|
1425
|
+
opacity: maskConfig.alpha,
|
|
1426
|
+
angle: maskConfig.angle,
|
|
1427
|
+
rx: maskConfig.rx,
|
|
951
1428
|
// Rounded Corners
|
|
952
|
-
ry:
|
|
953
|
-
...
|
|
1429
|
+
ry: maskConfig.ry,
|
|
1430
|
+
...maskConfig.styles
|
|
954
1431
|
});
|
|
955
1432
|
}
|
|
956
1433
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
mask.
|
|
978
|
-
mask.
|
|
1434
|
+
const styles = maskConfig.styles || {};
|
|
1435
|
+
const hasStyle = (property) => Object.prototype.hasOwnProperty.call(styles, property);
|
|
1436
|
+
const maskSettings = {
|
|
1437
|
+
selectable: maskConfig.selectable !== false,
|
|
1438
|
+
hasControls: "hasControls" in maskConfig ? maskConfig.hasControls : true,
|
|
1439
|
+
lockRotation: !this.options.maskRotatable,
|
|
1440
|
+
borderColor: "borderColor" in maskConfig ? maskConfig.borderColor : "red",
|
|
1441
|
+
cornerColor: "cornerColor" in maskConfig ? maskConfig.cornerColor : "black",
|
|
1442
|
+
cornerSize: "cornerSize" in maskConfig ? maskConfig.cornerSize : 8,
|
|
1443
|
+
transparentCorners: "transparentCorners" in maskConfig ? maskConfig.transparentCorners : false,
|
|
1444
|
+
stroke: hasStyle("stroke") ? styles.stroke : "#ccc",
|
|
1445
|
+
strokeWidth: hasStyle("strokeWidth") ? styles.strokeWidth : 1,
|
|
1446
|
+
strokeUniform: "strokeUniform" in maskConfig ? maskConfig.strokeUniform : hasStyle("strokeUniform") ? styles.strokeUniform : true
|
|
1447
|
+
};
|
|
1448
|
+
if (hasStyle("strokeDashArray"))
|
|
1449
|
+
maskSettings.strokeDashArray = styles.strokeDashArray;
|
|
1450
|
+
mask.set(maskSettings);
|
|
1451
|
+
mask.setCoords();
|
|
1452
|
+
mask.set({
|
|
1453
|
+
originalAlpha: maskConfig.alpha,
|
|
1454
|
+
originalStroke: mask.stroke || "#ccc",
|
|
1455
|
+
originalStrokeWidth: Number.isFinite(Number(mask.strokeWidth)) ? Number(mask.strokeWidth) : 1
|
|
979
1456
|
});
|
|
1457
|
+
this._rebindMaskEvents(mask);
|
|
1458
|
+
this._expandCanvasToFitObject(mask);
|
|
980
1459
|
this._lastMaskInitialLeft = left;
|
|
981
1460
|
this._lastMaskInitialTop = top;
|
|
982
|
-
this._lastMaskInitialWidth = resolveValue(
|
|
983
|
-
|
|
984
|
-
mask.
|
|
1461
|
+
this._lastMaskInitialWidth = resolveValue(maskConfig.width, this.options.defaultMaskWidth);
|
|
1462
|
+
const maskId = ++this.maskCounter;
|
|
1463
|
+
mask.set({
|
|
1464
|
+
maskId,
|
|
1465
|
+
maskName: `${this.options.maskName}${maskId}`
|
|
1466
|
+
});
|
|
985
1467
|
this._lastMask = mask;
|
|
986
1468
|
this.canvas.add(mask);
|
|
987
1469
|
this.canvas.bringToFront(mask);
|
|
988
|
-
if (
|
|
1470
|
+
if (maskConfig.selectable)
|
|
989
1471
|
this.canvas.setActiveObject(mask);
|
|
990
|
-
this.
|
|
1472
|
+
this._handleSelectionChanged([mask]);
|
|
991
1473
|
this._updateMaskList();
|
|
992
1474
|
this._updateUI();
|
|
993
1475
|
this.canvas.renderAll();
|
|
994
1476
|
this.saveState();
|
|
995
|
-
if (typeof
|
|
996
|
-
|
|
1477
|
+
if (typeof maskConfig.onCreate === "function")
|
|
1478
|
+
maskConfig.onCreate(mask, this.canvas);
|
|
997
1479
|
return mask;
|
|
998
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* @deprecated Use createMask() instead.
|
|
1483
|
+
*/
|
|
1484
|
+
addMask(config = {}) {
|
|
1485
|
+
return this.createMask(config);
|
|
1486
|
+
}
|
|
999
1487
|
/**
|
|
1000
1488
|
* Removes the currently selected mask from the canvas, if any.
|
|
1001
1489
|
* The associated label is also removed. UI and mask list are updated.
|
|
1002
1490
|
*/
|
|
1003
1491
|
removeSelectedMask() {
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1492
|
+
const activeObject = this.canvas.getActiveObject();
|
|
1493
|
+
const selectedMasks = this._getModifiedMasks(activeObject);
|
|
1494
|
+
if (!selectedMasks.length)
|
|
1006
1495
|
return;
|
|
1007
|
-
this._removeLabelForMask(active);
|
|
1008
|
-
this.canvas.remove(active);
|
|
1009
|
-
if (this._lastMask === active) {
|
|
1010
|
-
const masks = this.canvas.getObjects().filter((o) => o.maskId);
|
|
1011
|
-
this._lastMask = masks.length ? masks[masks.length - 1] : null;
|
|
1012
|
-
if (!this._lastMask) {
|
|
1013
|
-
this._lastMaskInitialLeft = null;
|
|
1014
|
-
this._lastMaskInitialTop = null;
|
|
1015
|
-
this._lastMaskInitialWidth = null;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
1496
|
this.canvas.discardActiveObject();
|
|
1497
|
+
selectedMasks.forEach((mask) => {
|
|
1498
|
+
this._removeLabelForMask(mask);
|
|
1499
|
+
this.canvas.remove(mask);
|
|
1500
|
+
});
|
|
1501
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1502
|
+
this._lastMask = masks.length ? masks[masks.length - 1] : null;
|
|
1503
|
+
if (!this._lastMask) {
|
|
1504
|
+
this._lastMaskInitialLeft = null;
|
|
1505
|
+
this._lastMaskInitialTop = null;
|
|
1506
|
+
this._lastMaskInitialWidth = null;
|
|
1507
|
+
}
|
|
1019
1508
|
this._updateMaskList();
|
|
1020
1509
|
this._updateUI();
|
|
1021
1510
|
this.canvas.renderAll();
|
|
@@ -1025,10 +1514,11 @@ var ImageEditor = class {
|
|
|
1025
1514
|
* Removes all masks from the canvas, including their labels.
|
|
1026
1515
|
* UI and internal mask placement memory are reset.
|
|
1027
1516
|
*/
|
|
1028
|
-
removeAllMasks() {
|
|
1029
|
-
const
|
|
1030
|
-
masks.
|
|
1031
|
-
masks.forEach((
|
|
1517
|
+
removeAllMasks(options = {}) {
|
|
1518
|
+
const saveHistory = options.saveHistory !== false;
|
|
1519
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1520
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
1521
|
+
masks.forEach((mask) => this.canvas.remove(mask));
|
|
1032
1522
|
this.canvas.discardActiveObject();
|
|
1033
1523
|
this._lastMask = null;
|
|
1034
1524
|
this._lastMaskInitialLeft = null;
|
|
@@ -1037,7 +1527,8 @@ var ImageEditor = class {
|
|
|
1037
1527
|
this._updateMaskList();
|
|
1038
1528
|
this._updateUI();
|
|
1039
1529
|
this.canvas.renderAll();
|
|
1040
|
-
|
|
1530
|
+
if (saveHistory)
|
|
1531
|
+
this.saveState();
|
|
1041
1532
|
}
|
|
1042
1533
|
/**
|
|
1043
1534
|
* Removes the label associated with the specified mask object, if it exists.
|
|
@@ -1050,15 +1541,15 @@ var ImageEditor = class {
|
|
|
1050
1541
|
return;
|
|
1051
1542
|
if (mask.__label) {
|
|
1052
1543
|
try {
|
|
1053
|
-
const
|
|
1054
|
-
if (
|
|
1544
|
+
const canvasObjects = this.canvas.getObjects();
|
|
1545
|
+
if (canvasObjects.includes(mask.__label)) {
|
|
1055
1546
|
this.canvas.remove(mask.__label);
|
|
1056
1547
|
}
|
|
1057
|
-
} catch (
|
|
1548
|
+
} catch (error) {
|
|
1058
1549
|
}
|
|
1059
1550
|
try {
|
|
1060
1551
|
delete mask.__label;
|
|
1061
|
-
} catch (
|
|
1552
|
+
} catch (error) {
|
|
1062
1553
|
}
|
|
1063
1554
|
}
|
|
1064
1555
|
}
|
|
@@ -1073,12 +1564,12 @@ var ImageEditor = class {
|
|
|
1073
1564
|
if (!mask || !this.options.maskLabelOnSelect)
|
|
1074
1565
|
return;
|
|
1075
1566
|
this._removeLabelForMask(mask);
|
|
1076
|
-
let
|
|
1567
|
+
let textObject = null;
|
|
1077
1568
|
if (this.options.label && typeof this.options.label.create === "function") {
|
|
1078
|
-
|
|
1569
|
+
textObject = this.options.label.create(mask, fabric);
|
|
1079
1570
|
}
|
|
1080
|
-
if (!
|
|
1081
|
-
let
|
|
1571
|
+
if (!textObject) {
|
|
1572
|
+
let labelText = mask.maskName;
|
|
1082
1573
|
let textOptions = {
|
|
1083
1574
|
left: 0,
|
|
1084
1575
|
top: 0,
|
|
@@ -1093,18 +1584,20 @@ var ImageEditor = class {
|
|
|
1093
1584
|
};
|
|
1094
1585
|
if (this.options.label) {
|
|
1095
1586
|
if (typeof this.options.label.getText === "function") {
|
|
1096
|
-
|
|
1587
|
+
const masks = this.canvas ? this.canvas.getObjects().filter((object) => object.maskId) : [];
|
|
1588
|
+
const maskIndex = Math.max(0, masks.indexOf(mask));
|
|
1589
|
+
labelText = this.options.label.getText(mask, maskIndex);
|
|
1097
1590
|
}
|
|
1098
1591
|
if (this.options.label.textOptions) {
|
|
1099
1592
|
Object.assign(textOptions, this.options.label.textOptions);
|
|
1100
1593
|
}
|
|
1101
1594
|
}
|
|
1102
|
-
|
|
1595
|
+
textObject = new fabric.Text(labelText, textOptions);
|
|
1103
1596
|
}
|
|
1104
|
-
|
|
1105
|
-
mask.__label =
|
|
1106
|
-
this.canvas.add(
|
|
1107
|
-
this.canvas.bringToFront(
|
|
1597
|
+
textObject.maskLabel = true;
|
|
1598
|
+
mask.__label = textObject;
|
|
1599
|
+
this.canvas.add(textObject);
|
|
1600
|
+
this.canvas.bringToFront(textObject);
|
|
1108
1601
|
this._syncMaskLabel(mask);
|
|
1109
1602
|
}
|
|
1110
1603
|
/**
|
|
@@ -1115,20 +1608,20 @@ var ImageEditor = class {
|
|
|
1115
1608
|
_hideAllMaskLabels() {
|
|
1116
1609
|
if (!this.canvas)
|
|
1117
1610
|
return;
|
|
1118
|
-
const
|
|
1119
|
-
const labels =
|
|
1120
|
-
labels.forEach((
|
|
1611
|
+
const canvasObjects = this.canvas.getObjects();
|
|
1612
|
+
const labels = canvasObjects.filter((object) => object.maskLabel);
|
|
1613
|
+
labels.forEach((label) => {
|
|
1121
1614
|
try {
|
|
1122
|
-
if (
|
|
1123
|
-
this.canvas.remove(
|
|
1124
|
-
} catch (
|
|
1615
|
+
if (canvasObjects.includes(label))
|
|
1616
|
+
this.canvas.remove(label);
|
|
1617
|
+
} catch (error) {
|
|
1125
1618
|
}
|
|
1126
1619
|
});
|
|
1127
|
-
|
|
1128
|
-
if (
|
|
1620
|
+
canvasObjects.forEach((object) => {
|
|
1621
|
+
if (object.maskId && object.__label) {
|
|
1129
1622
|
try {
|
|
1130
|
-
delete
|
|
1131
|
-
} catch (
|
|
1623
|
+
delete object.__label;
|
|
1624
|
+
} catch (error) {
|
|
1132
1625
|
}
|
|
1133
1626
|
}
|
|
1134
1627
|
});
|
|
@@ -1168,7 +1661,11 @@ var ImageEditor = class {
|
|
|
1168
1661
|
visible: true
|
|
1169
1662
|
});
|
|
1170
1663
|
mask.__label.setCoords();
|
|
1171
|
-
this.canvas.
|
|
1664
|
+
if (typeof this.canvas.requestRenderAll === "function") {
|
|
1665
|
+
this.canvas.requestRenderAll();
|
|
1666
|
+
} else {
|
|
1667
|
+
this.canvas.renderAll();
|
|
1668
|
+
}
|
|
1172
1669
|
}
|
|
1173
1670
|
/**
|
|
1174
1671
|
* Shows the label for the given mask, creating it if necessary and synchronizing its position.
|
|
@@ -1183,7 +1680,7 @@ var ImageEditor = class {
|
|
|
1183
1680
|
return;
|
|
1184
1681
|
if (!mask.__label)
|
|
1185
1682
|
this._createLabelForMask(mask);
|
|
1186
|
-
mask.__label.visible
|
|
1683
|
+
mask.__label.set({ visible: true });
|
|
1187
1684
|
this._syncMaskLabel(mask);
|
|
1188
1685
|
}
|
|
1189
1686
|
/**
|
|
@@ -1193,21 +1690,25 @@ var ImageEditor = class {
|
|
|
1193
1690
|
* @param {Array<Object>} selected - The currently selected objects (e.g. [mask] or []).
|
|
1194
1691
|
* @private
|
|
1195
1692
|
*/
|
|
1196
|
-
|
|
1197
|
-
const selectedMask = (selected || []).find((
|
|
1198
|
-
const masks = this.canvas.getObjects().filter((
|
|
1199
|
-
masks.forEach((
|
|
1200
|
-
if (
|
|
1201
|
-
if (
|
|
1693
|
+
_handleSelectionChanged(selected) {
|
|
1694
|
+
const selectedMask = (selected || []).find((object) => object.maskId);
|
|
1695
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1696
|
+
masks.forEach((mask) => {
|
|
1697
|
+
if (mask !== selectedMask) {
|
|
1698
|
+
if (mask.__label) {
|
|
1202
1699
|
try {
|
|
1203
|
-
this.canvas.remove(
|
|
1204
|
-
} catch (
|
|
1700
|
+
this.canvas.remove(mask.__label);
|
|
1701
|
+
} catch (error) {
|
|
1205
1702
|
}
|
|
1206
|
-
delete
|
|
1703
|
+
delete mask.__label;
|
|
1207
1704
|
}
|
|
1208
|
-
|
|
1705
|
+
const originalStrokeWidth = Number(mask.originalStrokeWidth);
|
|
1706
|
+
mask.set({
|
|
1707
|
+
stroke: mask.originalStroke || "#ccc",
|
|
1708
|
+
strokeWidth: Number.isFinite(originalStrokeWidth) ? originalStrokeWidth : 1
|
|
1709
|
+
});
|
|
1209
1710
|
} else {
|
|
1210
|
-
|
|
1711
|
+
mask.set({ stroke: "#ff0000", strokeWidth: 1 });
|
|
1211
1712
|
}
|
|
1212
1713
|
});
|
|
1213
1714
|
if (selectedMask)
|
|
@@ -1222,20 +1723,20 @@ var ImageEditor = class {
|
|
|
1222
1723
|
* @private
|
|
1223
1724
|
*/
|
|
1224
1725
|
_updateMaskList() {
|
|
1225
|
-
const
|
|
1226
|
-
if (!
|
|
1726
|
+
const maskListElement = document.getElementById(this.elements.maskList);
|
|
1727
|
+
if (!maskListElement)
|
|
1227
1728
|
return;
|
|
1228
|
-
|
|
1229
|
-
const masks = this.canvas.getObjects().filter((
|
|
1729
|
+
maskListElement.innerHTML = "";
|
|
1730
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1230
1731
|
masks.forEach((mask) => {
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1732
|
+
const listItemElement = document.createElement("li");
|
|
1733
|
+
listItemElement.className = "list-group-item mask-item";
|
|
1734
|
+
listItemElement.textContent = mask.maskName;
|
|
1735
|
+
listItemElement.onclick = () => {
|
|
1235
1736
|
this.canvas.setActiveObject(mask);
|
|
1236
|
-
this.
|
|
1737
|
+
this._handleSelectionChanged([mask]);
|
|
1237
1738
|
};
|
|
1238
|
-
|
|
1739
|
+
maskListElement.appendChild(listItemElement);
|
|
1239
1740
|
});
|
|
1240
1741
|
}
|
|
1241
1742
|
/**
|
|
@@ -1245,11 +1746,11 @@ var ImageEditor = class {
|
|
|
1245
1746
|
* @private
|
|
1246
1747
|
*/
|
|
1247
1748
|
_updateMaskListSelection(selectedMask) {
|
|
1248
|
-
const
|
|
1249
|
-
if (!
|
|
1749
|
+
const maskListElement = document.getElementById(this.elements.maskList);
|
|
1750
|
+
if (!maskListElement)
|
|
1250
1751
|
return;
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1752
|
+
const maskItems = maskListElement.querySelectorAll(".mask-item");
|
|
1753
|
+
maskItems.forEach((item) => {
|
|
1253
1754
|
const isSelected = !!selectedMask && item.textContent === selectedMask.maskName;
|
|
1254
1755
|
item.classList.toggle("active", isSelected);
|
|
1255
1756
|
});
|
|
@@ -1260,25 +1761,31 @@ var ImageEditor = class {
|
|
|
1260
1761
|
* @async
|
|
1261
1762
|
* @returns {Promise<void>} Resolves when merge and load are complete.
|
|
1262
1763
|
*/
|
|
1263
|
-
async
|
|
1764
|
+
async mergeMasks() {
|
|
1264
1765
|
if (!this.originalImage)
|
|
1265
1766
|
return;
|
|
1266
|
-
const masks = this.canvas.getObjects().filter((
|
|
1767
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1267
1768
|
if (!masks.length)
|
|
1268
1769
|
return;
|
|
1269
1770
|
this.canvas.discardActiveObject();
|
|
1270
1771
|
this.canvas.renderAll();
|
|
1271
1772
|
try {
|
|
1272
|
-
const
|
|
1273
|
-
this.
|
|
1773
|
+
const beforeJson = this._serializeCanvasState();
|
|
1774
|
+
const merged = await this.exportImageBase64({ exportImageArea: true, multiplier: this.options.exportMultiplier });
|
|
1775
|
+
this.removeAllMasks({ saveHistory: false });
|
|
1274
1776
|
await this.loadImage(merged);
|
|
1275
|
-
this.
|
|
1777
|
+
const afterJson = this._serializeCanvasState();
|
|
1778
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
1276
1779
|
} catch (err) {
|
|
1277
1780
|
this._reportError("merge error", err);
|
|
1278
|
-
if (this.canvasEl)
|
|
1279
|
-
this.canvasEl.style.visibility = "";
|
|
1280
1781
|
}
|
|
1281
1782
|
}
|
|
1783
|
+
/**
|
|
1784
|
+
* @deprecated Use mergeMasks() instead.
|
|
1785
|
+
*/
|
|
1786
|
+
async merge() {
|
|
1787
|
+
return this.mergeMasks();
|
|
1788
|
+
}
|
|
1282
1789
|
/**
|
|
1283
1790
|
* Triggers a JPEG image download of the current canvas (image plus masks if configured).
|
|
1284
1791
|
* The image area and multiplier are controlled by options.
|
|
@@ -1288,7 +1795,7 @@ var ImageEditor = class {
|
|
|
1288
1795
|
if (!this.originalImage)
|
|
1289
1796
|
return;
|
|
1290
1797
|
const exportImageArea = this.options.exportImageAreaByDefault;
|
|
1291
|
-
this.
|
|
1798
|
+
this.exportImageBase64({ exportImageArea, multiplier: this.options.exportMultiplier }).then((base64) => {
|
|
1292
1799
|
const link = document.createElement("a");
|
|
1293
1800
|
link.download = fileName;
|
|
1294
1801
|
link.href = base64;
|
|
@@ -1298,127 +1805,130 @@ var ImageEditor = class {
|
|
|
1298
1805
|
}).catch((err) => this._reportError("download error", err));
|
|
1299
1806
|
}
|
|
1300
1807
|
/**
|
|
1301
|
-
* Exports the image as a Base64-encoded
|
|
1808
|
+
* Exports the image as a Base64-encoded image data URL.
|
|
1302
1809
|
* Can export either the original, or the current view including masks (clipped/cropped).
|
|
1303
1810
|
* Will restore masks' state after temporary modifications for export.
|
|
1304
1811
|
* @async
|
|
1305
|
-
* @param {Object} [
|
|
1306
|
-
* @param {boolean} [
|
|
1307
|
-
* @param {number} [
|
|
1308
|
-
* @
|
|
1812
|
+
* @param {Object} [options={}] - Export options.
|
|
1813
|
+
* @param {boolean} [options.exportImageArea] - If true, exports only the image bounding area with masks cropped and blended.
|
|
1814
|
+
* @param {number} [options.multiplier=1] - Scaling multiplier for output (resolution).
|
|
1815
|
+
* @param {number} [options.quality=0.92] - Image quality between 0 and 1 for lossy formats.
|
|
1816
|
+
* @param {string} [options.fileType='jpeg'] - Output file type ('jpeg' | 'png' | 'webp').
|
|
1817
|
+
* @returns {Promise<string>} Promise resolving to an image data URL.
|
|
1309
1818
|
* @throws {Error} If there is no image loaded.
|
|
1310
1819
|
*/
|
|
1311
|
-
async
|
|
1820
|
+
async exportImageBase64(options = {}) {
|
|
1312
1821
|
if (!this.originalImage)
|
|
1313
1822
|
throw new Error("No image loaded");
|
|
1314
|
-
const exportImageArea = typeof
|
|
1315
|
-
const multiplier =
|
|
1823
|
+
const exportImageArea = typeof options.exportImageArea === "boolean" ? options.exportImageArea : this.options.exportImageAreaByDefault;
|
|
1824
|
+
const multiplier = options.multiplier || this.options.exportMultiplier || 1;
|
|
1825
|
+
const quality = this._normalizeQuality(options.quality ?? this.options.downsampleQuality);
|
|
1826
|
+
const format = this._normalizeImageFormat(options.fileType || options.format);
|
|
1316
1827
|
if (!exportImageArea) {
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1828
|
+
const masks2 = this.canvas.getObjects().filter((object) => object.maskId || object.maskLabel);
|
|
1829
|
+
const maskVisibilityBackups = masks2.map((mask) => ({ object: mask, visible: mask.visible }));
|
|
1830
|
+
try {
|
|
1831
|
+
masks2.forEach((mask) => {
|
|
1832
|
+
mask.set({ visible: false });
|
|
1833
|
+
});
|
|
1834
|
+
this.canvas.discardActiveObject();
|
|
1835
|
+
this.canvas.renderAll();
|
|
1836
|
+
this.originalImage.setCoords();
|
|
1837
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
1838
|
+
const { sx, sy, sw, sh } = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
1839
|
+
return await this._exportCanvasRegionToDataURL({
|
|
1840
|
+
sx,
|
|
1841
|
+
sy,
|
|
1842
|
+
sw,
|
|
1843
|
+
sh,
|
|
1844
|
+
multiplier,
|
|
1845
|
+
quality,
|
|
1846
|
+
format
|
|
1847
|
+
});
|
|
1848
|
+
} finally {
|
|
1849
|
+
maskVisibilityBackups.forEach((backup) => {
|
|
1850
|
+
try {
|
|
1851
|
+
backup.object.set({ visible: backup.visible });
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
this.canvas.renderAll();
|
|
1856
|
+
}
|
|
1328
1857
|
}
|
|
1329
|
-
const masks = this.canvas.getObjects().filter((
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
opacity:
|
|
1333
|
-
fill:
|
|
1334
|
-
strokeWidth:
|
|
1335
|
-
stroke:
|
|
1336
|
-
selectable:
|
|
1337
|
-
lockRotation:
|
|
1858
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1859
|
+
const maskStyleBackups = masks.map((mask) => ({
|
|
1860
|
+
object: mask,
|
|
1861
|
+
opacity: mask.opacity,
|
|
1862
|
+
fill: mask.fill,
|
|
1863
|
+
strokeWidth: mask.strokeWidth,
|
|
1864
|
+
stroke: mask.stroke,
|
|
1865
|
+
selectable: mask.selectable,
|
|
1866
|
+
lockRotation: mask.lockRotation
|
|
1338
1867
|
}));
|
|
1339
1868
|
let finalBase64;
|
|
1340
1869
|
try {
|
|
1341
|
-
masks.forEach((
|
|
1870
|
+
masks.forEach((mask) => this._removeLabelForMask(mask));
|
|
1342
1871
|
this.canvas.discardActiveObject();
|
|
1343
1872
|
this.canvas.renderAll();
|
|
1344
|
-
masks.forEach((
|
|
1345
|
-
|
|
1346
|
-
|
|
1873
|
+
masks.forEach((mask) => {
|
|
1874
|
+
mask.set({ opacity: 1, fill: "#000000", strokeWidth: 0, stroke: null, selectable: false });
|
|
1875
|
+
mask.setCoords();
|
|
1347
1876
|
});
|
|
1348
1877
|
this.canvas.renderAll();
|
|
1349
1878
|
this.originalImage.setCoords();
|
|
1350
|
-
const
|
|
1351
|
-
const sx =
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
multiplier
|
|
1361
|
-
});
|
|
1362
|
-
const img = new Image();
|
|
1363
|
-
img.onload = () => {
|
|
1364
|
-
try {
|
|
1365
|
-
const sxM = Math.round(sx * multiplier);
|
|
1366
|
-
const syM = Math.round(sy * multiplier);
|
|
1367
|
-
const swM = Math.round(sw * multiplier);
|
|
1368
|
-
const shM = Math.round(sh * multiplier);
|
|
1369
|
-
const oc = document.createElement("canvas");
|
|
1370
|
-
oc.width = swM;
|
|
1371
|
-
oc.height = shM;
|
|
1372
|
-
const ctx = oc.getContext("2d");
|
|
1373
|
-
ctx.drawImage(img, sxM, syM, swM, shM, 0, 0, swM, shM);
|
|
1374
|
-
const out = oc.toDataURL("image/jpeg", this.options.downsampleQuality);
|
|
1375
|
-
resolve(out);
|
|
1376
|
-
} catch (e) {
|
|
1377
|
-
reject(e);
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
|
-
img.onerror = reject;
|
|
1381
|
-
img.src = fullDataUrl;
|
|
1382
|
-
} catch (e) {
|
|
1383
|
-
reject(e);
|
|
1384
|
-
}
|
|
1879
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
1880
|
+
const { sx, sy, sw, sh } = this._getClampedCanvasRegion(imageBounds, { includePartialPixels: false });
|
|
1881
|
+
finalBase64 = await this._exportCanvasRegionToDataURL({
|
|
1882
|
+
sx,
|
|
1883
|
+
sy,
|
|
1884
|
+
sw,
|
|
1885
|
+
sh,
|
|
1886
|
+
multiplier,
|
|
1887
|
+
quality,
|
|
1888
|
+
format
|
|
1385
1889
|
});
|
|
1386
1890
|
} finally {
|
|
1387
|
-
|
|
1891
|
+
maskStyleBackups.forEach((backup) => {
|
|
1388
1892
|
try {
|
|
1389
|
-
|
|
1390
|
-
opacity:
|
|
1391
|
-
fill:
|
|
1392
|
-
strokeWidth:
|
|
1393
|
-
stroke:
|
|
1394
|
-
selectable:
|
|
1395
|
-
lockRotation:
|
|
1893
|
+
backup.object.set({
|
|
1894
|
+
opacity: backup.opacity,
|
|
1895
|
+
fill: backup.fill,
|
|
1896
|
+
strokeWidth: backup.strokeWidth,
|
|
1897
|
+
stroke: backup.stroke,
|
|
1898
|
+
selectable: backup.selectable,
|
|
1899
|
+
lockRotation: backup.lockRotation
|
|
1396
1900
|
});
|
|
1397
|
-
|
|
1398
|
-
} catch (
|
|
1901
|
+
backup.object.setCoords();
|
|
1902
|
+
} catch (error) {
|
|
1399
1903
|
}
|
|
1400
1904
|
});
|
|
1401
1905
|
this.canvas.renderAll();
|
|
1402
1906
|
}
|
|
1403
1907
|
return finalBase64;
|
|
1404
1908
|
}
|
|
1909
|
+
/**
|
|
1910
|
+
* @deprecated Use exportImageBase64() instead.
|
|
1911
|
+
*/
|
|
1912
|
+
async getImageBase64(options = {}) {
|
|
1913
|
+
return this.exportImageBase64(options);
|
|
1914
|
+
}
|
|
1405
1915
|
/**
|
|
1406
1916
|
* Exports the current canvas (with or without masks) as a File object.
|
|
1407
1917
|
* Allows you to choose whether to merge masks and specify file type (jpeg/png/webp).
|
|
1408
1918
|
*
|
|
1409
1919
|
* @async
|
|
1410
|
-
* @param {Object} [
|
|
1411
|
-
* @param {boolean} [
|
|
1412
|
-
* @param {string} [
|
|
1413
|
-
* @param {number} [
|
|
1414
|
-
* @param {number} [
|
|
1415
|
-
* @param {string} [
|
|
1920
|
+
* @param {Object} [options={}] - Export options.
|
|
1921
|
+
* @param {boolean} [options.mergeMask=true] - If true, export image area with masks merged; if false, export the plain image without masks.
|
|
1922
|
+
* @param {string} [options.fileType='jpeg'] - Output file type ('jpeg' | 'png' | 'webp'). Defaults to 'jpeg' on invalid input.
|
|
1923
|
+
* @param {number} [options.quality=0.92] - Image quality for lossy types (0-1, default based on options.downsampleQuality).
|
|
1924
|
+
* @param {number} [options.multiplier=1] - Output resolution multiplier.
|
|
1925
|
+
* @param {string} [options.fileName] - Optional file name (only used for download).
|
|
1416
1926
|
* @returns {Promise<File>} Resolves with the exported image as a File object.
|
|
1417
1927
|
*
|
|
1418
1928
|
* @example
|
|
1419
1929
|
* const file = await this.exportImageFile({ mergeMask: false, fileType: 'png' });
|
|
1420
1930
|
*/
|
|
1421
|
-
async exportImageFile(
|
|
1931
|
+
async exportImageFile(options = {}) {
|
|
1422
1932
|
if (!this.originalImage)
|
|
1423
1933
|
throw new Error("No image loaded");
|
|
1424
1934
|
const {
|
|
@@ -1427,60 +1937,116 @@ var ImageEditor = class {
|
|
|
1427
1937
|
quality = this.options.downsampleQuality ?? 0.92,
|
|
1428
1938
|
multiplier = this.options.exportMultiplier ?? 1,
|
|
1429
1939
|
fileName = this.options.defaultDownloadFileName ?? "exported_image.jpg"
|
|
1430
|
-
} =
|
|
1431
|
-
const
|
|
1432
|
-
"jpeg": "jpeg",
|
|
1433
|
-
"jpg": "jpeg",
|
|
1434
|
-
"image/jpeg": "jpeg",
|
|
1435
|
-
"png": "png",
|
|
1436
|
-
"image/png": "png",
|
|
1437
|
-
"webp": "webp",
|
|
1438
|
-
"image/webp": "webp"
|
|
1439
|
-
};
|
|
1440
|
-
const safeFileType = typeMapping[String(fileType).toLowerCase()] || "jpeg";
|
|
1940
|
+
} = options;
|
|
1941
|
+
const safeFileType = this._normalizeImageFormat(fileType);
|
|
1441
1942
|
let base64;
|
|
1442
1943
|
if (mergeMask) {
|
|
1443
|
-
base64 = await this.
|
|
1944
|
+
base64 = await this.exportImageBase64({
|
|
1444
1945
|
exportImageArea: true,
|
|
1445
|
-
multiplier
|
|
1946
|
+
multiplier,
|
|
1947
|
+
quality,
|
|
1948
|
+
fileType: safeFileType
|
|
1446
1949
|
});
|
|
1447
1950
|
} else {
|
|
1448
|
-
base64 = await this.
|
|
1951
|
+
base64 = await this.exportImageBase64({
|
|
1449
1952
|
exportImageArea: false,
|
|
1450
|
-
multiplier
|
|
1953
|
+
multiplier,
|
|
1954
|
+
quality,
|
|
1955
|
+
fileType: safeFileType
|
|
1451
1956
|
});
|
|
1452
1957
|
}
|
|
1453
1958
|
let imageDataUrl = base64;
|
|
1454
1959
|
if (!imageDataUrl.startsWith(`data:image/${safeFileType}`)) {
|
|
1455
1960
|
imageDataUrl = await new Promise((resolve, reject) => {
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1961
|
+
const imageElement = new window.Image();
|
|
1962
|
+
imageElement.crossOrigin = "Anonymous";
|
|
1963
|
+
imageElement.onload = () => {
|
|
1459
1964
|
try {
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
const
|
|
1466
|
-
resolve(
|
|
1467
|
-
} catch (
|
|
1468
|
-
reject(
|
|
1965
|
+
const offscreenCanvas = document.createElement("canvas");
|
|
1966
|
+
offscreenCanvas.width = imageElement.width;
|
|
1967
|
+
offscreenCanvas.height = imageElement.height;
|
|
1968
|
+
const context = offscreenCanvas.getContext("2d");
|
|
1969
|
+
context.drawImage(imageElement, 0, 0);
|
|
1970
|
+
const convertedDataUrl = offscreenCanvas.toDataURL(`image/${safeFileType}`, quality);
|
|
1971
|
+
resolve(convertedDataUrl);
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
reject(error);
|
|
1469
1974
|
}
|
|
1470
1975
|
};
|
|
1471
|
-
|
|
1472
|
-
|
|
1976
|
+
imageElement.onerror = reject;
|
|
1977
|
+
imageElement.src = base64;
|
|
1473
1978
|
});
|
|
1474
1979
|
}
|
|
1475
|
-
const
|
|
1980
|
+
const binaryString = atob(imageDataUrl.split(",")[1]);
|
|
1476
1981
|
const mime = `image/${safeFileType}`;
|
|
1477
|
-
let
|
|
1478
|
-
const
|
|
1479
|
-
while (
|
|
1480
|
-
|
|
1982
|
+
let byteIndex = binaryString.length;
|
|
1983
|
+
const bytes = new Uint8Array(byteIndex);
|
|
1984
|
+
while (byteIndex--) {
|
|
1985
|
+
bytes[byteIndex] = binaryString.charCodeAt(byteIndex);
|
|
1986
|
+
}
|
|
1987
|
+
return new File([bytes], fileName, { type: mime });
|
|
1988
|
+
}
|
|
1989
|
+
_clearMaskPlacementMemory() {
|
|
1990
|
+
this._lastMask = null;
|
|
1991
|
+
this._lastMaskInitialLeft = null;
|
|
1992
|
+
this._lastMaskInitialTop = null;
|
|
1993
|
+
this._lastMaskInitialWidth = null;
|
|
1994
|
+
}
|
|
1995
|
+
async _restoreStateAfterCropFailure(beforeJson, message, error) {
|
|
1996
|
+
this._reportError(message, error);
|
|
1997
|
+
if (this._cropRect && this.canvas)
|
|
1998
|
+
this._removeCropRect();
|
|
1999
|
+
this._cropRect = null;
|
|
2000
|
+
this._cropMode = false;
|
|
2001
|
+
if (this.canvas && this._prevSelectionSetting !== void 0) {
|
|
2002
|
+
this.canvas.selection = !!this._prevSelectionSetting;
|
|
2003
|
+
}
|
|
2004
|
+
this._prevSelectionSetting = void 0;
|
|
2005
|
+
if (beforeJson) {
|
|
2006
|
+
try {
|
|
2007
|
+
await this.loadFromState(beforeJson);
|
|
2008
|
+
} catch (restoreError) {
|
|
2009
|
+
this._reportError("applyCrop: rollback failed", restoreError);
|
|
2010
|
+
}
|
|
1481
2011
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
2012
|
+
this._updateUI();
|
|
2013
|
+
if (this.canvas)
|
|
2014
|
+
this.canvas.renderAll();
|
|
2015
|
+
}
|
|
2016
|
+
_restoreCropObjectState() {
|
|
2017
|
+
if (Array.isArray(this._cropPrevEvented)) {
|
|
2018
|
+
this._cropPrevEvented.forEach((state) => {
|
|
2019
|
+
try {
|
|
2020
|
+
state.object.set({
|
|
2021
|
+
evented: state.evented,
|
|
2022
|
+
selectable: state.selectable,
|
|
2023
|
+
visible: state.visible
|
|
2024
|
+
});
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
this._cropPrevEvented = null;
|
|
2030
|
+
}
|
|
2031
|
+
_removeCropRect() {
|
|
2032
|
+
if (!this._cropRect)
|
|
2033
|
+
return;
|
|
2034
|
+
try {
|
|
2035
|
+
if (this._cropHandlers && this._cropHandlers.length) {
|
|
2036
|
+
this._cropHandlers.forEach((targetHandlers) => {
|
|
2037
|
+
targetHandlers.handlers.forEach((handlerRecord) => {
|
|
2038
|
+
targetHandlers.target.off(handlerRecord.eventName, handlerRecord.handler);
|
|
2039
|
+
});
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
}
|
|
2044
|
+
try {
|
|
2045
|
+
this.canvas.remove(this._cropRect);
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
}
|
|
2048
|
+
this._cropRect = null;
|
|
2049
|
+
this._cropHandlers = [];
|
|
1484
2050
|
}
|
|
1485
2051
|
/**
|
|
1486
2052
|
* Enter crop mode: create a resizable/movable selection rect on top of the image.
|
|
@@ -1496,12 +2062,12 @@ var ImageEditor = class {
|
|
|
1496
2062
|
this.canvas.selection = false;
|
|
1497
2063
|
this.canvas.discardActiveObject();
|
|
1498
2064
|
this.originalImage.setCoords();
|
|
1499
|
-
const
|
|
2065
|
+
const imageBounds = this.originalImage.getBoundingRect(true, true);
|
|
1500
2066
|
const padding = this.options.crop && this.options.crop.padding ? this.options.crop.padding : 10;
|
|
1501
|
-
const left = Math.max(0, Math.floor(
|
|
1502
|
-
const top = Math.max(0, Math.floor(
|
|
1503
|
-
const width = Math.min(this.options.crop.minWidth || 50, Math.floor(
|
|
1504
|
-
const height = Math.min(this.options.crop.minHeight || 50, Math.floor(
|
|
2067
|
+
const left = Math.max(0, Math.floor(imageBounds.left + padding));
|
|
2068
|
+
const top = Math.max(0, Math.floor(imageBounds.top + padding));
|
|
2069
|
+
const width = Math.min(this.options.crop.minWidth || 50, Math.floor(imageBounds.width - padding * 2));
|
|
2070
|
+
const height = Math.min(this.options.crop.minHeight || 50, Math.floor(imageBounds.height - padding * 2));
|
|
1505
2071
|
const cropRect = new fabric.Rect({
|
|
1506
2072
|
left,
|
|
1507
2073
|
top,
|
|
@@ -1526,27 +2092,40 @@ var ImageEditor = class {
|
|
|
1526
2092
|
this.canvas.setActiveObject(cropRect);
|
|
1527
2093
|
this._cropRect = cropRect;
|
|
1528
2094
|
this._cropPrevEvented = [];
|
|
1529
|
-
this.
|
|
1530
|
-
|
|
1531
|
-
|
|
2095
|
+
const shouldHideMasks = !!(this.options.crop && this.options.crop.hideMasksDuringCrop);
|
|
2096
|
+
this.canvas.getObjects().forEach((object) => {
|
|
2097
|
+
if (object !== cropRect) {
|
|
2098
|
+
this._cropPrevEvented.push({ object, evented: object.evented, selectable: object.selectable, visible: object.visible });
|
|
1532
2099
|
try {
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2100
|
+
const updates = {
|
|
2101
|
+
evented: false,
|
|
2102
|
+
selectable: false
|
|
2103
|
+
};
|
|
2104
|
+
if (shouldHideMasks && (object.maskId || object.maskLabel))
|
|
2105
|
+
updates.visible = false;
|
|
2106
|
+
object.set(updates);
|
|
2107
|
+
} catch (error) {
|
|
1536
2108
|
}
|
|
1537
2109
|
}
|
|
1538
2110
|
});
|
|
1539
|
-
const
|
|
2111
|
+
const handleCropRectModified = () => {
|
|
1540
2112
|
try {
|
|
1541
2113
|
cropRect.setCoords();
|
|
1542
2114
|
this.canvas.requestRenderAll();
|
|
1543
|
-
} catch (
|
|
2115
|
+
} catch (error) {
|
|
1544
2116
|
}
|
|
1545
2117
|
};
|
|
1546
|
-
cropRect.on("modified",
|
|
1547
|
-
cropRect.on("moving",
|
|
1548
|
-
cropRect.on("scaling",
|
|
1549
|
-
this._cropHandlers.push({
|
|
2118
|
+
cropRect.on("modified", handleCropRectModified);
|
|
2119
|
+
cropRect.on("moving", handleCropRectModified);
|
|
2120
|
+
cropRect.on("scaling", handleCropRectModified);
|
|
2121
|
+
this._cropHandlers.push({
|
|
2122
|
+
target: cropRect,
|
|
2123
|
+
handlers: [
|
|
2124
|
+
{ eventName: "modified", handler: handleCropRectModified },
|
|
2125
|
+
{ eventName: "moving", handler: handleCropRectModified },
|
|
2126
|
+
{ eventName: "scaling", handler: handleCropRectModified }
|
|
2127
|
+
]
|
|
2128
|
+
});
|
|
1550
2129
|
this._updateUI();
|
|
1551
2130
|
this.canvas.renderAll();
|
|
1552
2131
|
}
|
|
@@ -1557,32 +2136,8 @@ var ImageEditor = class {
|
|
|
1557
2136
|
cancelCrop() {
|
|
1558
2137
|
if (!this.canvas || !this._cropMode)
|
|
1559
2138
|
return;
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
if (this._cropHandlers && this._cropHandlers.length) {
|
|
1563
|
-
this._cropHandlers.forEach((h) => {
|
|
1564
|
-
h.handlers.forEach((rec) => h.target.off(rec.evt, rec.fn));
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
} catch (e) {
|
|
1568
|
-
}
|
|
1569
|
-
try {
|
|
1570
|
-
this.canvas.remove(this._cropRect);
|
|
1571
|
-
} catch (e) {
|
|
1572
|
-
}
|
|
1573
|
-
this._cropRect = null;
|
|
1574
|
-
}
|
|
1575
|
-
if (Array.isArray(this._cropPrevEvented)) {
|
|
1576
|
-
this._cropPrevEvented.forEach((i) => {
|
|
1577
|
-
try {
|
|
1578
|
-
i.obj.evented = i.evented;
|
|
1579
|
-
i.obj.selectable = i.selectable;
|
|
1580
|
-
} catch (e) {
|
|
1581
|
-
}
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
this._cropPrevEvented = null;
|
|
1585
|
-
this._cropHandlers = [];
|
|
2139
|
+
this._removeCropRect();
|
|
2140
|
+
this._restoreCropObjectState();
|
|
1586
2141
|
this._cropMode = false;
|
|
1587
2142
|
this.canvas.selection = !!this._prevSelectionSetting;
|
|
1588
2143
|
this._prevSelectionSetting = void 0;
|
|
@@ -1600,134 +2155,92 @@ var ImageEditor = class {
|
|
|
1600
2155
|
return;
|
|
1601
2156
|
this._cropRect.setCoords();
|
|
1602
2157
|
const rectBounds = this._cropRect.getBoundingRect(true, true);
|
|
1603
|
-
const sx
|
|
1604
|
-
const
|
|
1605
|
-
|
|
1606
|
-
const sh = Math.max(1, Math.round(Math.min(rectBounds.height, this.canvas.getHeight() - sy)));
|
|
2158
|
+
const { sx, sy, sw, sh } = this._getClampedCanvasRegion(rectBounds);
|
|
2159
|
+
const shouldPreserveMasks = !!(this.options.crop && this.options.crop.preserveMasksAfterCrop);
|
|
2160
|
+
this._restoreCropObjectState();
|
|
1607
2161
|
let beforeJson = null;
|
|
1608
2162
|
try {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
}
|
|
1613
|
-
beforeJson = JSON.stringify(jsonObj);
|
|
1614
|
-
} catch (e) {
|
|
1615
|
-
this._reportWarning("applyCrop: could not serialize before state", e);
|
|
2163
|
+
beforeJson = this._serializeCanvasState();
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
this._reportWarning("applyCrop: could not serialize before state", error);
|
|
1616
2166
|
beforeJson = null;
|
|
1617
2167
|
}
|
|
2168
|
+
const preservedMasks = [];
|
|
1618
2169
|
try {
|
|
1619
|
-
const masks = this.canvas.getObjects().filter((
|
|
2170
|
+
const masks = this.canvas.getObjects().filter((object) => object.maskId);
|
|
1620
2171
|
if (masks && masks.length) {
|
|
1621
|
-
masks.forEach((
|
|
2172
|
+
masks.forEach((mask) => {
|
|
1622
2173
|
try {
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
this.
|
|
2174
|
+
mask.setCoords();
|
|
2175
|
+
const maskBounds = mask.getBoundingRect(true, true);
|
|
2176
|
+
const intersectsCrop = maskBounds.left < sx + sw && maskBounds.left + maskBounds.width > sx && maskBounds.top < sy + sh && maskBounds.top + maskBounds.height > sy;
|
|
2177
|
+
this._removeLabelForMask(mask);
|
|
2178
|
+
this.canvas.remove(mask);
|
|
2179
|
+
if (shouldPreserveMasks && intersectsCrop) {
|
|
2180
|
+
mask.set({
|
|
2181
|
+
left: (mask.left || 0) - sx,
|
|
2182
|
+
top: (mask.top || 0) - sy,
|
|
2183
|
+
visible: true
|
|
2184
|
+
});
|
|
2185
|
+
mask.setCoords();
|
|
2186
|
+
preservedMasks.push(mask);
|
|
2187
|
+
}
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
this._reportWarning("applyCrop: failed to remove mask", error);
|
|
1627
2190
|
}
|
|
1628
2191
|
});
|
|
1629
|
-
this.
|
|
1630
|
-
this._lastMaskInitialLeft = null;
|
|
1631
|
-
this._lastMaskInitialTop = null;
|
|
1632
|
-
this._lastMaskInitialWidth = null;
|
|
2192
|
+
this._clearMaskPlacementMemory();
|
|
1633
2193
|
this.canvas.discardActiveObject();
|
|
1634
2194
|
this.canvas.renderAll();
|
|
1635
2195
|
}
|
|
1636
|
-
} catch (
|
|
1637
|
-
this._reportWarning("applyCrop: error while removing masks",
|
|
1638
|
-
}
|
|
1639
|
-
try {
|
|
1640
|
-
if (this._cropRect) {
|
|
1641
|
-
try {
|
|
1642
|
-
if (this._cropHandlers && this._cropHandlers.length) {
|
|
1643
|
-
this._cropHandlers.forEach((h) => {
|
|
1644
|
-
h.handlers.forEach((rec) => h.target.off(rec.evt, rec.fn));
|
|
1645
|
-
});
|
|
1646
|
-
}
|
|
1647
|
-
} catch (e) {
|
|
1648
|
-
}
|
|
1649
|
-
try {
|
|
1650
|
-
this.canvas.remove(this._cropRect);
|
|
1651
|
-
} catch (e) {
|
|
1652
|
-
}
|
|
1653
|
-
this._cropRect = null;
|
|
1654
|
-
}
|
|
1655
|
-
} catch (e) {
|
|
2196
|
+
} catch (error) {
|
|
2197
|
+
this._reportWarning("applyCrop: error while removing masks", error);
|
|
1656
2198
|
}
|
|
2199
|
+
this._removeCropRect();
|
|
1657
2200
|
this._cropMode = false;
|
|
1658
2201
|
this.canvas.selection = !!this._prevSelectionSetting;
|
|
1659
2202
|
this._prevSelectionSetting = void 0;
|
|
1660
2203
|
let croppedBase64;
|
|
1661
2204
|
try {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
2205
|
+
croppedBase64 = await this._exportCanvasRegionToDataURL({
|
|
2206
|
+
sx,
|
|
2207
|
+
sy,
|
|
2208
|
+
sw,
|
|
2209
|
+
sh,
|
|
2210
|
+
multiplier: 1,
|
|
2211
|
+
quality: this._normalizeQuality(this.options.downsampleQuality),
|
|
2212
|
+
format: "jpeg"
|
|
1666
2213
|
});
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
img.onload = () => {
|
|
1670
|
-
try {
|
|
1671
|
-
const oc = document.createElement("canvas");
|
|
1672
|
-
oc.width = sw;
|
|
1673
|
-
oc.height = sh;
|
|
1674
|
-
const ctx = oc.getContext("2d");
|
|
1675
|
-
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
1676
|
-
const out = oc.toDataURL("image/jpeg", this.options.downsampleQuality || 0.92);
|
|
1677
|
-
resolve(out);
|
|
1678
|
-
} catch (err) {
|
|
1679
|
-
reject(err);
|
|
1680
|
-
}
|
|
1681
|
-
};
|
|
1682
|
-
img.onerror = (e) => reject(e);
|
|
1683
|
-
img.src = fullDataUrl;
|
|
1684
|
-
});
|
|
1685
|
-
} catch (e) {
|
|
1686
|
-
this._reportError("applyCrop: failed to create cropped image", e);
|
|
1687
|
-
this._updateUI();
|
|
2214
|
+
} catch (error) {
|
|
2215
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: failed to create cropped image", error);
|
|
1688
2216
|
return;
|
|
1689
2217
|
}
|
|
1690
2218
|
try {
|
|
1691
2219
|
await this.loadImage(croppedBase64);
|
|
2220
|
+
if (preservedMasks.length) {
|
|
2221
|
+
preservedMasks.forEach((mask) => {
|
|
2222
|
+
this._rebindMaskEvents(mask);
|
|
2223
|
+
this.canvas.add(mask);
|
|
2224
|
+
this.canvas.bringToFront(mask);
|
|
2225
|
+
});
|
|
2226
|
+
this._lastMask = preservedMasks[preservedMasks.length - 1];
|
|
2227
|
+
this.maskCounter = preservedMasks.reduce((max, mask) => Math.max(max, mask.maskId || 0), this.maskCounter);
|
|
2228
|
+
this._updateMaskList();
|
|
2229
|
+
this.canvas.renderAll();
|
|
2230
|
+
}
|
|
1692
2231
|
} catch (e) {
|
|
1693
|
-
this.
|
|
1694
|
-
this._updateUI();
|
|
2232
|
+
await this._restoreStateAfterCropFailure(beforeJson, "applyCrop: loadImage(croppedBase64) failed", e);
|
|
1695
2233
|
return;
|
|
1696
2234
|
}
|
|
1697
2235
|
let afterJson = null;
|
|
1698
2236
|
try {
|
|
1699
|
-
|
|
1700
|
-
if (Array.isArray(jsonObj2.objects)) {
|
|
1701
|
-
jsonObj2.objects = jsonObj2.objects.filter((o) => !o.isCropRect);
|
|
1702
|
-
}
|
|
1703
|
-
afterJson = JSON.stringify(jsonObj2);
|
|
2237
|
+
afterJson = this._serializeCanvasState();
|
|
1704
2238
|
} catch (e) {
|
|
1705
2239
|
this._reportWarning("applyCrop: failed to serialize after state", e);
|
|
1706
2240
|
afterJson = null;
|
|
1707
2241
|
}
|
|
1708
2242
|
try {
|
|
1709
|
-
|
|
1710
|
-
const cmd = new Command(
|
|
1711
|
-
() => {
|
|
1712
|
-
if (afterJson)
|
|
1713
|
-
self2.loadFromState(afterJson);
|
|
1714
|
-
},
|
|
1715
|
-
() => {
|
|
1716
|
-
if (beforeJson)
|
|
1717
|
-
self2.loadFromState(beforeJson);
|
|
1718
|
-
}
|
|
1719
|
-
);
|
|
1720
|
-
if (!this.historyManager)
|
|
1721
|
-
this.historyManager = new HistoryManager(this.maxHistorySize || 50);
|
|
1722
|
-
if (this.historyManager.currentIndex < this.historyManager.history.length - 1) {
|
|
1723
|
-
this.historyManager.history = this.historyManager.history.slice(0, this.historyManager.currentIndex + 1);
|
|
1724
|
-
}
|
|
1725
|
-
this.historyManager.history.push(cmd);
|
|
1726
|
-
if (this.historyManager.history.length > this.historyManager.maxSize) {
|
|
1727
|
-
this.historyManager.history.shift();
|
|
1728
|
-
} else {
|
|
1729
|
-
this.historyManager.currentIndex++;
|
|
1730
|
-
}
|
|
2243
|
+
this._pushStateTransition(beforeJson, afterJson);
|
|
1731
2244
|
} catch (e) {
|
|
1732
2245
|
this._reportWarning("applyCrop: failed to push history command", e);
|
|
1733
2246
|
}
|
|
@@ -1741,9 +2254,9 @@ var ImageEditor = class {
|
|
|
1741
2254
|
* @private
|
|
1742
2255
|
*/
|
|
1743
2256
|
_updateInputs() {
|
|
1744
|
-
const
|
|
1745
|
-
if (
|
|
1746
|
-
|
|
2257
|
+
const scaleInputElement = document.getElementById(this.elements.scaleRate);
|
|
2258
|
+
if (scaleInputElement)
|
|
2259
|
+
scaleInputElement.value = Math.round(this.currentScale * 100);
|
|
1747
2260
|
}
|
|
1748
2261
|
/**
|
|
1749
2262
|
* Updates the enabled/disabled state of various UI controls (buttons)
|
|
@@ -1751,43 +2264,45 @@ var ImageEditor = class {
|
|
|
1751
2264
|
* @private
|
|
1752
2265
|
*/
|
|
1753
2266
|
_updateUI() {
|
|
1754
|
-
const
|
|
1755
|
-
const masks =
|
|
2267
|
+
const hasImage = !!this.originalImage;
|
|
2268
|
+
const masks = hasImage ? this.canvas.getObjects().filter((object) => object.maskId) : [];
|
|
1756
2269
|
const hasMasks = masks.length > 0;
|
|
1757
|
-
const
|
|
1758
|
-
const hasSelectedMask =
|
|
1759
|
-
const
|
|
2270
|
+
const activeObject = this.canvas.getActiveObject();
|
|
2271
|
+
const hasSelectedMask = activeObject && activeObject.maskId;
|
|
2272
|
+
const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
|
|
1760
2273
|
const canUndo = this.historyManager?.canUndo();
|
|
1761
2274
|
const canRedo = this.historyManager?.canRedo();
|
|
1762
|
-
const
|
|
1763
|
-
if (
|
|
1764
|
-
for (const
|
|
1765
|
-
const
|
|
1766
|
-
if (!
|
|
2275
|
+
const isInCropMode = !!this._cropMode;
|
|
2276
|
+
if (isInCropMode) {
|
|
2277
|
+
for (const key of Object.keys(this.elements || {})) {
|
|
2278
|
+
const element = document.getElementById(this.elements[key]);
|
|
2279
|
+
if (!element)
|
|
1767
2280
|
continue;
|
|
1768
|
-
if (
|
|
1769
|
-
|
|
2281
|
+
if (key === "applyCropBtn" || key === "cancelCropBtn") {
|
|
2282
|
+
this._setDisabled(key, false);
|
|
1770
2283
|
} else {
|
|
1771
|
-
|
|
2284
|
+
this._setDisabled(key, true);
|
|
1772
2285
|
}
|
|
1773
2286
|
}
|
|
1774
2287
|
return;
|
|
1775
2288
|
}
|
|
1776
|
-
this._setDisabled("zoomInBtn", !
|
|
1777
|
-
this._setDisabled("zoomOutBtn", !
|
|
1778
|
-
this._setDisabled("rotateLeftBtn", !
|
|
1779
|
-
this._setDisabled("rotateRightBtn", !
|
|
1780
|
-
this._setDisabled("addMaskBtn", !
|
|
2289
|
+
this._setDisabled("zoomInBtn", !hasImage || this.isAnimating || this.currentScale >= this.options.maxScale);
|
|
2290
|
+
this._setDisabled("zoomOutBtn", !hasImage || this.isAnimating || this.currentScale <= this.options.minScale);
|
|
2291
|
+
this._setDisabled("rotateLeftBtn", !hasImage || this.isAnimating);
|
|
2292
|
+
this._setDisabled("rotateRightBtn", !hasImage || this.isAnimating);
|
|
2293
|
+
this._setDisabled("addMaskBtn", !hasImage || this.isAnimating);
|
|
1781
2294
|
this._setDisabled("removeMaskBtn", !hasSelectedMask || this.isAnimating);
|
|
1782
2295
|
this._setDisabled("removeAllMasksBtn", !hasMasks || this.isAnimating);
|
|
1783
|
-
this._setDisabled("mergeBtn", !
|
|
1784
|
-
this._setDisabled("downloadBtn", !
|
|
1785
|
-
this._setDisabled("resetBtn", !
|
|
1786
|
-
this._setDisabled("undoBtn", !
|
|
1787
|
-
this._setDisabled("redoBtn", !
|
|
1788
|
-
this._setDisabled("cropBtn", !
|
|
2296
|
+
this._setDisabled("mergeBtn", !hasImage || !hasMasks || this.isAnimating);
|
|
2297
|
+
this._setDisabled("downloadBtn", !hasImage || this.isAnimating);
|
|
2298
|
+
this._setDisabled("resetBtn", !hasImage || isDefaultTransform || this.isAnimating);
|
|
2299
|
+
this._setDisabled("undoBtn", !hasImage || this.isAnimating || !canUndo);
|
|
2300
|
+
this._setDisabled("redoBtn", !hasImage || this.isAnimating || !canRedo);
|
|
2301
|
+
this._setDisabled("cropBtn", !hasImage || this.isAnimating);
|
|
1789
2302
|
this._setDisabled("applyCropBtn", true);
|
|
1790
2303
|
this._setDisabled("cancelCropBtn", true);
|
|
2304
|
+
this._setDisabled("imageInput", this.isAnimating);
|
|
2305
|
+
this._setDisabled("uploadArea", this.isAnimating);
|
|
1791
2306
|
}
|
|
1792
2307
|
/**
|
|
1793
2308
|
* Enables or disables a specific UI element (typically a button) by its key.
|
|
@@ -1797,9 +2312,27 @@ var ImageEditor = class {
|
|
|
1797
2312
|
* @private
|
|
1798
2313
|
*/
|
|
1799
2314
|
_setDisabled(key, disabled) {
|
|
1800
|
-
const
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
2315
|
+
const element = document.getElementById(this.elements[key]);
|
|
2316
|
+
if (!element)
|
|
2317
|
+
return;
|
|
2318
|
+
if ("disabled" in element) {
|
|
2319
|
+
element.disabled = !!disabled;
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (disabled) {
|
|
2323
|
+
element.setAttribute("aria-disabled", "true");
|
|
2324
|
+
element.style.pointerEvents = "none";
|
|
2325
|
+
} else {
|
|
2326
|
+
element.removeAttribute("aria-disabled");
|
|
2327
|
+
element.style.pointerEvents = "";
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
_isElementDisabled(element) {
|
|
2331
|
+
if (!element)
|
|
2332
|
+
return false;
|
|
2333
|
+
if ("disabled" in element)
|
|
2334
|
+
return !!element.disabled;
|
|
2335
|
+
return element.getAttribute("aria-disabled") === "true";
|
|
1803
2336
|
}
|
|
1804
2337
|
/**
|
|
1805
2338
|
* Automatically display and hide placeholders and containers based on the current image content
|
|
@@ -1816,16 +2349,16 @@ var ImageEditor = class {
|
|
|
1816
2349
|
* @private
|
|
1817
2350
|
*/
|
|
1818
2351
|
_setPlaceholderVisible(show) {
|
|
1819
|
-
if (!this.
|
|
2352
|
+
if (!this.placeholderElement)
|
|
1820
2353
|
return;
|
|
1821
2354
|
if (show) {
|
|
1822
|
-
this.
|
|
1823
|
-
this.
|
|
1824
|
-
this.
|
|
2355
|
+
this.placeholderElement.classList.remove("d-none");
|
|
2356
|
+
this.placeholderElement.classList.add("d-flex");
|
|
2357
|
+
this.containerElement.classList.add("d-none");
|
|
1825
2358
|
} else {
|
|
1826
|
-
this.
|
|
1827
|
-
this.
|
|
1828
|
-
this.
|
|
2359
|
+
this.placeholderElement.classList.remove("d-flex");
|
|
2360
|
+
this.placeholderElement.classList.add("d-none");
|
|
2361
|
+
this.containerElement.classList.remove("d-none");
|
|
1829
2362
|
}
|
|
1830
2363
|
}
|
|
1831
2364
|
/**
|
|
@@ -1835,19 +2368,19 @@ var ImageEditor = class {
|
|
|
1835
2368
|
*/
|
|
1836
2369
|
dispose() {
|
|
1837
2370
|
try {
|
|
1838
|
-
for (const key in this.
|
|
1839
|
-
const handlers = this.
|
|
1840
|
-
const
|
|
1841
|
-
if (!
|
|
2371
|
+
for (const key in this._handlersByElementKey || {}) {
|
|
2372
|
+
const handlers = this._handlersByElementKey[key] || [];
|
|
2373
|
+
const element = document.getElementById(this.elements[key]);
|
|
2374
|
+
if (!element)
|
|
1842
2375
|
continue;
|
|
1843
|
-
handlers.forEach((
|
|
2376
|
+
handlers.forEach((handlerRecord) => {
|
|
1844
2377
|
try {
|
|
1845
|
-
|
|
1846
|
-
} catch (
|
|
2378
|
+
element.removeEventListener(handlerRecord.eventName, handlerRecord.handler);
|
|
2379
|
+
} catch (error) {
|
|
1847
2380
|
}
|
|
1848
2381
|
});
|
|
1849
2382
|
}
|
|
1850
|
-
} catch (
|
|
2383
|
+
} catch (error) {
|
|
1851
2384
|
}
|
|
1852
2385
|
if (this._cropRect) {
|
|
1853
2386
|
try {
|
|
@@ -1856,16 +2389,22 @@ var ImageEditor = class {
|
|
|
1856
2389
|
}
|
|
1857
2390
|
this._cropRect = null;
|
|
1858
2391
|
}
|
|
2392
|
+
if (this.containerElement && this._containerOriginalOverflow !== void 0) {
|
|
2393
|
+
try {
|
|
2394
|
+
this.containerElement.style.overflow = this._containerOriginalOverflow;
|
|
2395
|
+
} catch (e) {
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
1859
2398
|
if (this.canvas) {
|
|
1860
2399
|
try {
|
|
1861
2400
|
this.canvas.dispose();
|
|
1862
2401
|
} catch (e) {
|
|
1863
2402
|
}
|
|
1864
2403
|
this.canvas = null;
|
|
1865
|
-
this.
|
|
2404
|
+
this.canvasElement = null;
|
|
1866
2405
|
this.isImageLoadedToCanvas = false;
|
|
1867
2406
|
}
|
|
1868
|
-
this.
|
|
2407
|
+
this._handlersByElementKey = {};
|
|
1869
2408
|
}
|
|
1870
2409
|
};
|
|
1871
2410
|
var AnimationQueue = class {
|
|
@@ -1932,6 +2471,13 @@ var HistoryManager = class {
|
|
|
1932
2471
|
this.history = [];
|
|
1933
2472
|
this.currentIndex = -1;
|
|
1934
2473
|
this.maxSize = maxSize;
|
|
2474
|
+
this.pending = Promise.resolve();
|
|
2475
|
+
}
|
|
2476
|
+
enqueue(task) {
|
|
2477
|
+
const run = this.pending.then(task, task);
|
|
2478
|
+
this.pending = run.catch(() => {
|
|
2479
|
+
});
|
|
2480
|
+
return run;
|
|
1935
2481
|
}
|
|
1936
2482
|
/**
|
|
1937
2483
|
* Executes a new command and pushes it onto the history stack.
|
|
@@ -1942,6 +2488,16 @@ var HistoryManager = class {
|
|
|
1942
2488
|
*/
|
|
1943
2489
|
execute(command) {
|
|
1944
2490
|
command.execute();
|
|
2491
|
+
this.push(command);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Pushes an already-applied command onto the history stack.
|
|
2495
|
+
* Truncates any "future" history when branching.
|
|
2496
|
+
*
|
|
2497
|
+
* @param {Command} command The command to push.
|
|
2498
|
+
* @returns {void}
|
|
2499
|
+
*/
|
|
2500
|
+
push(command) {
|
|
1945
2501
|
if (this.currentIndex < this.history.length - 1) {
|
|
1946
2502
|
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
1947
2503
|
}
|
|
@@ -1974,10 +2530,13 @@ var HistoryManager = class {
|
|
|
1974
2530
|
* @returns {void}
|
|
1975
2531
|
*/
|
|
1976
2532
|
undo() {
|
|
1977
|
-
|
|
1978
|
-
this.
|
|
1979
|
-
|
|
1980
|
-
|
|
2533
|
+
return this.enqueue(async () => {
|
|
2534
|
+
if (this.currentIndex >= 0) {
|
|
2535
|
+
const index = this.currentIndex;
|
|
2536
|
+
await this.history[index].undo();
|
|
2537
|
+
this.currentIndex = index - 1;
|
|
2538
|
+
}
|
|
2539
|
+
});
|
|
1981
2540
|
}
|
|
1982
2541
|
/**
|
|
1983
2542
|
* Redoes the next command in history if possible.
|
|
@@ -1985,10 +2544,13 @@ var HistoryManager = class {
|
|
|
1985
2544
|
* @returns {void}
|
|
1986
2545
|
*/
|
|
1987
2546
|
redo() {
|
|
1988
|
-
|
|
1989
|
-
this.currentIndex
|
|
1990
|
-
|
|
1991
|
-
|
|
2547
|
+
return this.enqueue(async () => {
|
|
2548
|
+
if (this.currentIndex < this.history.length - 1) {
|
|
2549
|
+
const index = this.currentIndex + 1;
|
|
2550
|
+
await this.history[index].execute();
|
|
2551
|
+
this.currentIndex = index;
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
1992
2554
|
}
|
|
1993
2555
|
};
|
|
1994
2556
|
var image_editor_default = ImageEditor;
|