@bensitu/image-editor 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -20
- package/README.md +234 -208
- package/dist/image-editor.esm.js +2004 -0
- package/dist/image-editor.esm.js.map +7 -0
- package/dist/image-editor.esm.min.js +3 -3
- package/dist/image-editor.esm.min.js.map +4 -4
- package/dist/image-editor.esm.min.mjs +14 -0
- package/dist/image-editor.esm.min.mjs.map +7 -0
- package/dist/image-editor.esm.mjs +2004 -0
- package/dist/image-editor.esm.mjs.map +7 -0
- package/dist/image-editor.js +2001 -0
- package/dist/image-editor.js.map +7 -0
- package/dist/image-editor.min.js +3 -3
- package/dist/image-editor.min.js.map +4 -4
- package/image-editor.d.ts +181 -0
- package/package.json +84 -71
- package/src/browser.js +11 -0
- package/src/esm.js +9 -0
- package/src/image-editor.js +325 -226
package/src/image-editor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file image-editor.js
|
|
3
3
|
* @module image-editor
|
|
4
|
-
* @version 1.2.
|
|
4
|
+
* @version 1.2.1
|
|
5
5
|
* @author Ben Situ
|
|
6
6
|
* @license MIT
|
|
7
7
|
* @description Lightweight canvas-based image editor with masking/transform/export support.
|
|
@@ -12,19 +12,30 @@
|
|
|
12
12
|
* See the license files for details.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
let fabric = null;
|
|
16
|
+
|
|
17
|
+
function getGlobalScope() {
|
|
18
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
19
|
+
if (typeof self !== 'undefined') return self;
|
|
20
|
+
if (typeof window !== 'undefined') return window;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getGlobalFabric() {
|
|
25
|
+
const scope = getGlobalScope();
|
|
26
|
+
return scope && scope.fabric ? scope.fabric : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setFabric(fabricInstance) {
|
|
30
|
+
fabric = fabricInstance || getGlobalFabric();
|
|
31
|
+
return fabric;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureFabric() {
|
|
35
|
+
if (!fabric) setFabric();
|
|
36
|
+
return fabric;
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
/**
|
|
29
40
|
* ImageEditor
|
|
30
41
|
*
|
|
@@ -70,17 +81,39 @@
|
|
|
70
81
|
* @param {string|null} [options.initialImageBase64=null] - Base64 string to auto-load as initial image, if any.
|
|
71
82
|
* @param {string} [options.defaultDownloadFileName='edited_image.jpg'] - Default file name for downloads.
|
|
72
83
|
* @param {function} [options.onImageLoaded] - Optional callback to invoke after an image loads.
|
|
84
|
+
* @param {function} [options.onError] - Optional callback for recoverable internal errors.
|
|
85
|
+
* @param {function} [options.onWarning] - Optional callback for recoverable internal warnings.
|
|
73
86
|
*
|
|
74
87
|
* @constructor
|
|
75
88
|
*/
|
|
76
89
|
class ImageEditor {
|
|
77
90
|
constructor(options = {}) {
|
|
78
|
-
// Verify that fabric.js is present
|
|
79
|
-
this._fabricLoaded = typeof fabric !== 'undefined';
|
|
80
|
-
if (!this._fabricLoaded) {
|
|
81
|
-
console.error('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');
|
|
82
|
-
}
|
|
83
91
|
// Default options (can be overridden via ctor param)
|
|
92
|
+
const defaultLabel = {
|
|
93
|
+
getText: (mask) => mask.maskName,
|
|
94
|
+
textOptions: {
|
|
95
|
+
fontSize: 12,
|
|
96
|
+
fill: '#fff',
|
|
97
|
+
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
98
|
+
padding: 2,
|
|
99
|
+
fontFamily: 'monospace',
|
|
100
|
+
fontWeight: 'bold',
|
|
101
|
+
selectable: false,
|
|
102
|
+
evented: false,
|
|
103
|
+
originX: 'left',
|
|
104
|
+
originY: 'top'
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const defaultCrop = {
|
|
108
|
+
minWidth: 100,
|
|
109
|
+
minHeight: 100,
|
|
110
|
+
padding: 10,
|
|
111
|
+
hideMasksDuringCrop: true,
|
|
112
|
+
preserveMasksAfterCrop: false,
|
|
113
|
+
allowRotationOfCropRect: false
|
|
114
|
+
};
|
|
115
|
+
const userLabel = options.label || {};
|
|
116
|
+
const userCrop = options.crop || {};
|
|
84
117
|
this.options = {
|
|
85
118
|
canvasWidth: 800,
|
|
86
119
|
canvasHeight: 600,
|
|
@@ -117,32 +150,29 @@
|
|
|
117
150
|
initialImageBase64: null, // Provide a base64 'data:image/...' string here if you want auto-load
|
|
118
151
|
|
|
119
152
|
defaultDownloadFileName: 'edited_image.jpg',
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
originY: 'top',
|
|
153
|
+
onError: null,
|
|
154
|
+
onWarning: null,
|
|
155
|
+
|
|
156
|
+
...options,
|
|
157
|
+
label: {
|
|
158
|
+
...defaultLabel,
|
|
159
|
+
...userLabel,
|
|
160
|
+
textOptions: {
|
|
161
|
+
...defaultLabel.textOptions,
|
|
162
|
+
...(userLabel.textOptions || {})
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
crop: {
|
|
166
|
+
...defaultCrop,
|
|
167
|
+
...userCrop
|
|
136
168
|
}
|
|
137
169
|
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
allowRotationOfCropRect: false
|
|
145
|
-
};
|
|
170
|
+
|
|
171
|
+
// Verify that fabric.js is present
|
|
172
|
+
this._fabricLoaded = !!ensureFabric();
|
|
173
|
+
if (!this._fabricLoaded) {
|
|
174
|
+
this._reportError('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');
|
|
175
|
+
}
|
|
146
176
|
|
|
147
177
|
// Runtime state
|
|
148
178
|
this.canvas = null;
|
|
@@ -162,6 +192,7 @@
|
|
|
162
192
|
|
|
163
193
|
this._boundHandlers = {};
|
|
164
194
|
|
|
195
|
+
this._lastMask = null;
|
|
165
196
|
this._lastMaskInitialLeft = null;
|
|
166
197
|
this._lastMaskInitialTop = null;
|
|
167
198
|
this._lastMaskInitialWidth = null;
|
|
@@ -241,6 +272,28 @@
|
|
|
241
272
|
}
|
|
242
273
|
}
|
|
243
274
|
|
|
275
|
+
_reportError(message, error = null) {
|
|
276
|
+
const handler = this.options && this.options.onError;
|
|
277
|
+
if (typeof handler !== 'function') return;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
handler(error, message);
|
|
281
|
+
} catch {
|
|
282
|
+
// Ignore observer failures so editor recovery paths remain stable.
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_reportWarning(message, error = null) {
|
|
287
|
+
const handler = this.options && this.options.onWarning;
|
|
288
|
+
if (typeof handler !== 'function') return;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
handler(error, message);
|
|
292
|
+
} catch {
|
|
293
|
+
// Ignore observer failures so editor recovery paths remain stable.
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
244
297
|
/**
|
|
245
298
|
* Canvas setup helpers
|
|
246
299
|
* @private
|
|
@@ -338,7 +391,7 @@
|
|
|
338
391
|
|
|
339
392
|
// Crop bindings (optional: bound only if element IDs exist in elements)
|
|
340
393
|
this._bindIfExists('cropBtn', 'click', () => this.enterCropMode());
|
|
341
|
-
this._bindIfExists('applyCropBtn', 'click', () => { this.applyCrop().catch(e =>
|
|
394
|
+
this._bindIfExists('applyCropBtn', 'click', () => { this.applyCrop().catch(e => this._reportError('applyCrop failed', e)); });
|
|
342
395
|
this._bindIfExists('cancelCropBtn', 'click', () => this.cancelCrop());
|
|
343
396
|
}
|
|
344
397
|
|
|
@@ -370,7 +423,7 @@
|
|
|
370
423
|
if (!file || !file.type.startsWith('image/')) return;
|
|
371
424
|
const reader = new FileReader();
|
|
372
425
|
reader.onload = (e) => this.loadImage(e.target.result);
|
|
373
|
-
reader.onerror = (e) => {
|
|
426
|
+
reader.onerror = (e) => { this._reportError('Image file could not be read', e); };
|
|
374
427
|
reader.readAsDataURL(file);
|
|
375
428
|
}
|
|
376
429
|
|
|
@@ -381,6 +434,7 @@
|
|
|
381
434
|
*/
|
|
382
435
|
async loadImage(base64) {
|
|
383
436
|
if (!this._fabricLoaded) return;
|
|
437
|
+
if (!this.canvas) return;
|
|
384
438
|
if (!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/')) return;
|
|
385
439
|
|
|
386
440
|
this._setPlaceholderVisible(false);
|
|
@@ -404,80 +458,91 @@
|
|
|
404
458
|
}
|
|
405
459
|
|
|
406
460
|
// Create fabric.Image from URL
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
fabric.Image.fromURL(loadSrc, (fimg) => {
|
|
463
|
+
try {
|
|
464
|
+
if (!fimg) throw new Error('Image could not be loaded');
|
|
465
|
+
|
|
466
|
+
this.canvas.discardActiveObject();
|
|
467
|
+
this._hideAllMaskLabels();
|
|
468
|
+
this.canvas.clear();
|
|
469
|
+
this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
|
|
470
|
+
|
|
471
|
+
fimg.set({ originX: 'left', originY: 'top', selectable: false, evented: false });
|
|
472
|
+
|
|
473
|
+
const imgW = fimg.width;
|
|
474
|
+
const imgH = fimg.height;
|
|
475
|
+
|
|
476
|
+
const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || this.options.canvasWidth) : this.options.canvasWidth;
|
|
477
|
+
const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || this.options.canvasHeight) : this.options.canvasHeight;
|
|
478
|
+
|
|
479
|
+
if (this.options.fitImageToCanvas) {
|
|
480
|
+
// Fit into current canvas (shrink only) and ensure canvas does not exceed container
|
|
481
|
+
const cw = Math.max(1, Math.min(this.options.canvasWidth, minW) - 1)
|
|
482
|
+
const ch = Math.max(1, Math.min(this.options.canvasHeight, minH) - 1);
|
|
483
|
+
this._setCanvasSizeInt(cw, ch);
|
|
484
|
+
const fitScale = Math.min(cw / imgW, ch / imgH, 1);
|
|
485
|
+
fimg.set({ left: 0, top: 0 });
|
|
486
|
+
fimg.scale(fitScale);
|
|
487
|
+
this.baseImageScale = fimg.scaleX || 1;
|
|
488
|
+
} else if (this.options.coverImageToCanvas) {
|
|
489
|
+
// Cover canvas: scale to cover, allowing overflow (at least one side fits)
|
|
490
|
+
const cw = Math.max(this.options.canvasWidth, minW);
|
|
491
|
+
const ch = Math.max(this.options.canvasHeight, minH);
|
|
492
|
+
this._setCanvasSizeInt(cw, ch);
|
|
493
|
+
const coverScale = Math.min(1, Math.max(cw / imgW, ch / imgH));
|
|
494
|
+
fimg.set({ left: 0, top: 0 });
|
|
495
|
+
fimg.scale(coverScale);
|
|
496
|
+
this.baseImageScale = fimg.scaleX || 1;
|
|
497
|
+
} else if (this.options.expandCanvasToImage) {
|
|
498
|
+
// Expand canvas so that it fully contains the image
|
|
499
|
+
const cw = Math.max(minW, Math.floor(imgW));
|
|
500
|
+
const ch = Math.max(minH, Math.floor(imgH));
|
|
501
|
+
this._setCanvasSizeInt(cw, ch);
|
|
502
|
+
fimg.set({ left: 0, top: 0 });
|
|
503
|
+
fimg.scale(1);
|
|
504
|
+
this.baseImageScale = 1;
|
|
505
|
+
} else {
|
|
506
|
+
// Keep existing canvas size and center the image
|
|
507
|
+
const cw = Math.max(this.options.canvasWidth, minW);
|
|
508
|
+
const ch = Math.max(this.options.canvasHeight, minH);
|
|
509
|
+
this._setCanvasSizeInt(cw, ch);
|
|
510
|
+
const fitScale = Math.min(cw / imgW, ch / imgH, 1);
|
|
511
|
+
fimg.set({ left: 0, top: 0 });
|
|
512
|
+
fimg.scale(fitScale);
|
|
513
|
+
this.baseImageScale = fimg.scaleX || 1;
|
|
514
|
+
}
|
|
515
|
+
// Put the image onto the canvas
|
|
516
|
+
this.originalImage = fimg;
|
|
517
|
+
this.canvas.add(fimg);
|
|
518
|
+
this.canvas.sendToBack(fimg);
|
|
519
|
+
|
|
520
|
+
// Reset mask placement memory
|
|
521
|
+
this._lastMask = null;
|
|
522
|
+
this._lastMaskInitialLeft = null;
|
|
523
|
+
this._lastMaskInitialTop = null;
|
|
524
|
+
this._lastMaskInitialWidth = null;
|
|
525
|
+
|
|
526
|
+
this.maskCounter = 0;
|
|
527
|
+
this.currentScale = 1;
|
|
528
|
+
this.currentRotation = 0;
|
|
529
|
+
|
|
530
|
+
this._updateInputs();
|
|
531
|
+
this._updateMaskList();
|
|
532
|
+
this.isImageLoadedToCanvas = true;
|
|
533
|
+
this._updateUI();
|
|
534
|
+
this.canvas.renderAll();
|
|
535
|
+
|
|
536
|
+
if (typeof this.onImageLoaded === 'function') {
|
|
537
|
+
this.onImageLoaded();
|
|
538
|
+
}
|
|
476
539
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
540
|
+
resolve();
|
|
541
|
+
} catch (err) {
|
|
542
|
+
reject(err);
|
|
543
|
+
}
|
|
544
|
+
}, { crossOrigin: 'anonymous' });
|
|
545
|
+
});
|
|
481
546
|
}
|
|
482
547
|
|
|
483
548
|
/**
|
|
@@ -485,9 +550,11 @@
|
|
|
485
550
|
* @returns {boolean} true if loaded, false if not
|
|
486
551
|
*/
|
|
487
552
|
isImageLoaded() {
|
|
553
|
+
const fabricInstance = ensureFabric();
|
|
488
554
|
return !!(
|
|
489
555
|
this.originalImage &&
|
|
490
|
-
|
|
556
|
+
fabricInstance &&
|
|
557
|
+
this.originalImage instanceof fabricInstance.Image &&
|
|
491
558
|
this.originalImage.width > 0 &&
|
|
492
559
|
this.originalImage.height > 0
|
|
493
560
|
);
|
|
@@ -777,7 +844,7 @@
|
|
|
777
844
|
this.saveState();
|
|
778
845
|
})
|
|
779
846
|
.catch(err => {
|
|
780
|
-
|
|
847
|
+
this._reportError('reset() failed', err);
|
|
781
848
|
});
|
|
782
849
|
}
|
|
783
850
|
|
|
@@ -794,24 +861,38 @@
|
|
|
794
861
|
: jsonString;
|
|
795
862
|
|
|
796
863
|
this.canvas.loadFromJSON(json, () => {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
this.originalImage.set({ originX: 'left', originY: 'top', selectable: false, evented: false, hasControls: false, hoverCursor: 'default' });
|
|
802
|
-
this.canvas.sendToBack(this.originalImage);
|
|
864
|
+
try {
|
|
865
|
+
this._hideAllMaskLabels();
|
|
866
|
+
const objs = this.canvas.getObjects();
|
|
867
|
+
this.originalImage = objs.find(o => o.type === 'image' && !o.maskId) || null;
|
|
803
868
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
869
|
+
if (this.originalImage) {
|
|
870
|
+
this.originalImage.set({ originX: 'left', originY: 'top', selectable: false, evented: false, hasControls: false, hoverCursor: 'default' });
|
|
871
|
+
this.canvas.sendToBack(this.originalImage);
|
|
872
|
+
}
|
|
807
873
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
874
|
+
const masks = objs.filter(o => o.maskId);
|
|
875
|
+
this.maskCounter = masks.reduce((max, m) =>
|
|
876
|
+
Math.max(max, m.maskId), 0);
|
|
877
|
+
this._lastMask = masks.length ? masks[masks.length - 1] : null;
|
|
878
|
+
if (!this._lastMask) {
|
|
879
|
+
this._lastMaskInitialLeft = null;
|
|
880
|
+
this._lastMaskInitialTop = null;
|
|
881
|
+
this._lastMaskInitialWidth = null;
|
|
882
|
+
}
|
|
883
|
+
this.isImageLoadedToCanvas = !!this.originalImage;
|
|
884
|
+
|
|
885
|
+
this.canvas.renderAll();
|
|
886
|
+
this._updateMaskList();
|
|
887
|
+
this._updatePlaceholderStatus();
|
|
888
|
+
this._updateUI();
|
|
889
|
+
} catch (callbackError) {
|
|
890
|
+
this._reportError('loadFromState() failed', callbackError);
|
|
891
|
+
}
|
|
811
892
|
});
|
|
812
893
|
|
|
813
894
|
} catch (e) {
|
|
814
|
-
|
|
895
|
+
this._reportError('loadFromState() failed', e);
|
|
815
896
|
}
|
|
816
897
|
}
|
|
817
898
|
|
|
@@ -853,7 +934,7 @@
|
|
|
853
934
|
}
|
|
854
935
|
this._updateUI();
|
|
855
936
|
} catch (err) {
|
|
856
|
-
|
|
937
|
+
this._reportWarning('saveState: failed to save canvas snapshot', err);
|
|
857
938
|
}
|
|
858
939
|
}
|
|
859
940
|
|
|
@@ -979,7 +1060,7 @@
|
|
|
979
1060
|
...cfg.styles
|
|
980
1061
|
});
|
|
981
1062
|
break;
|
|
982
|
-
case 'polygon':
|
|
1063
|
+
case 'polygon': {
|
|
983
1064
|
let polyPoints = cfg.points || [];
|
|
984
1065
|
if (Array.isArray(polyPoints) && polyPoints.length && typeof polyPoints[0] === 'object') {
|
|
985
1066
|
// Ensure numeric {x,y} objects for fabric.Polygon
|
|
@@ -993,6 +1074,7 @@
|
|
|
993
1074
|
...cfg.styles
|
|
994
1075
|
});
|
|
995
1076
|
break;
|
|
1077
|
+
}
|
|
996
1078
|
case 'rect':
|
|
997
1079
|
default:
|
|
998
1080
|
mask = new fabric.Rect({
|
|
@@ -1066,6 +1148,15 @@
|
|
|
1066
1148
|
if (!active || !active.maskId) return;
|
|
1067
1149
|
this._removeLabelForMask(active);
|
|
1068
1150
|
this.canvas.remove(active);
|
|
1151
|
+
if (this._lastMask === active) {
|
|
1152
|
+
const masks = this.canvas.getObjects().filter(o => o.maskId);
|
|
1153
|
+
this._lastMask = masks.length ? masks[masks.length - 1] : null;
|
|
1154
|
+
if (!this._lastMask) {
|
|
1155
|
+
this._lastMaskInitialLeft = null;
|
|
1156
|
+
this._lastMaskInitialTop = null;
|
|
1157
|
+
this._lastMaskInitialWidth = null;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1069
1160
|
this.canvas.discardActiveObject();
|
|
1070
1161
|
this._updateMaskList();
|
|
1071
1162
|
this._updateUI();
|
|
@@ -1082,6 +1173,7 @@
|
|
|
1082
1173
|
masks.forEach(m => this._removeLabelForMask(m));
|
|
1083
1174
|
masks.forEach(m => this.canvas.remove(m));
|
|
1084
1175
|
this.canvas.discardActiveObject();
|
|
1176
|
+
this._lastMask = null;
|
|
1085
1177
|
this._lastMaskInitialLeft = null;
|
|
1086
1178
|
this._lastMaskInitialTop = null;
|
|
1087
1179
|
this._lastMaskInitialWidth = null;
|
|
@@ -1105,8 +1197,8 @@
|
|
|
1105
1197
|
if (objs.includes(mask.__label)) {
|
|
1106
1198
|
this.canvas.remove(mask.__label);
|
|
1107
1199
|
}
|
|
1108
|
-
} catch (e) {
|
|
1109
|
-
try { delete mask.__label; } catch (e) { }
|
|
1200
|
+
} catch (e) { void e; }
|
|
1201
|
+
try { delete mask.__label; } catch (e) { void e; }
|
|
1110
1202
|
}
|
|
1111
1203
|
}
|
|
1112
1204
|
|
|
@@ -1169,9 +1261,9 @@
|
|
|
1169
1261
|
labels.forEach(l => {
|
|
1170
1262
|
try {
|
|
1171
1263
|
if (objs.includes(l)) this.canvas.remove(l);
|
|
1172
|
-
} catch (e) { }
|
|
1264
|
+
} catch (e) { void e; }
|
|
1173
1265
|
});
|
|
1174
|
-
objs.forEach(o => { if (o.maskId && o.__label) { try { delete o.__label; } catch (e) { } } });
|
|
1266
|
+
objs.forEach(o => { if (o.maskId && o.__label) { try { delete o.__label; } catch (e) { void e; } } });
|
|
1175
1267
|
}
|
|
1176
1268
|
|
|
1177
1269
|
/**
|
|
@@ -1241,7 +1333,7 @@
|
|
|
1241
1333
|
masks.forEach(m => {
|
|
1242
1334
|
if (m !== selectedMask) {
|
|
1243
1335
|
if (m.__label) {
|
|
1244
|
-
try { this.canvas.remove(m.__label); } catch (e) { }
|
|
1336
|
+
try { this.canvas.remove(m.__label); } catch (e) { void e; }
|
|
1245
1337
|
delete m.__label;
|
|
1246
1338
|
}
|
|
1247
1339
|
m.set({ stroke: '#ccc', strokeWidth: 1 });
|
|
@@ -1312,7 +1404,7 @@
|
|
|
1312
1404
|
await this.loadImage(merged);
|
|
1313
1405
|
this.saveState();
|
|
1314
1406
|
} catch (err) {
|
|
1315
|
-
|
|
1407
|
+
this._reportError('merge error', err);
|
|
1316
1408
|
if (this.canvasEl) this.canvasEl.style.visibility = '';
|
|
1317
1409
|
}
|
|
1318
1410
|
}
|
|
@@ -1334,7 +1426,7 @@
|
|
|
1334
1426
|
link.click();
|
|
1335
1427
|
document.body.removeChild(link);
|
|
1336
1428
|
})
|
|
1337
|
-
.catch(err =>
|
|
1429
|
+
.catch(err => this._reportError('download error', err));
|
|
1338
1430
|
}
|
|
1339
1431
|
|
|
1340
1432
|
/**
|
|
@@ -1379,74 +1471,77 @@
|
|
|
1379
1471
|
lockRotation: m.lockRotation
|
|
1380
1472
|
}));
|
|
1381
1473
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
masks.forEach(m => {
|
|
1389
|
-
m.set({ opacity: 1, fill: '#000000', strokeWidth: 0, stroke: null, selectable: false });
|
|
1390
|
-
m.setCoords();
|
|
1391
|
-
});
|
|
1392
|
-
this.canvas.renderAll();
|
|
1393
|
-
|
|
1394
|
-
// Compute integer bounding box for image
|
|
1395
|
-
this.originalImage.setCoords();
|
|
1396
|
-
const imgBr = this.originalImage.getBoundingRect(true, true);
|
|
1397
|
-
const sx = Math.max(0, Math.round(imgBr.left));
|
|
1398
|
-
const sy = Math.max(0, Math.round(imgBr.top));
|
|
1399
|
-
const sw = Math.max(1, Math.round(imgBr.width));
|
|
1400
|
-
const sh = Math.max(1, Math.round(imgBr.height));
|
|
1401
|
-
|
|
1402
|
-
// Crop precisely in offscreen canvas
|
|
1403
|
-
const finalBase64 = await new Promise((resolve, reject) => {
|
|
1404
|
-
try {
|
|
1405
|
-
const fullDataUrl = this.canvas.toDataURL({
|
|
1406
|
-
format: 'jpeg',
|
|
1407
|
-
quality: this.options.downsampleQuality,
|
|
1408
|
-
multiplier: multiplier
|
|
1409
|
-
});
|
|
1474
|
+
let finalBase64;
|
|
1475
|
+
try {
|
|
1476
|
+
// Remove labels, deselect
|
|
1477
|
+
masks.forEach(m => this._removeLabelForMask(m));
|
|
1478
|
+
this.canvas.discardActiveObject();
|
|
1479
|
+
this.canvas.renderAll();
|
|
1410
1480
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
const shM = Math.round(sh * multiplier);
|
|
1481
|
+
// Set masks to opaque black no border
|
|
1482
|
+
masks.forEach(m => {
|
|
1483
|
+
m.set({ opacity: 1, fill: '#000000', strokeWidth: 0, stroke: null, selectable: false });
|
|
1484
|
+
m.setCoords();
|
|
1485
|
+
});
|
|
1486
|
+
this.canvas.renderAll();
|
|
1418
1487
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1488
|
+
// Compute integer bounding box for image
|
|
1489
|
+
this.originalImage.setCoords();
|
|
1490
|
+
const imgBr = this.originalImage.getBoundingRect(true, true);
|
|
1491
|
+
const sx = Math.max(0, Math.round(imgBr.left));
|
|
1492
|
+
const sy = Math.max(0, Math.round(imgBr.top));
|
|
1493
|
+
const sw = Math.max(1, Math.round(imgBr.width));
|
|
1494
|
+
const sh = Math.max(1, Math.round(imgBr.height));
|
|
1495
|
+
|
|
1496
|
+
// Crop precisely in offscreen canvas
|
|
1497
|
+
finalBase64 = await new Promise((resolve, reject) => {
|
|
1498
|
+
try {
|
|
1499
|
+
const fullDataUrl = this.canvas.toDataURL({
|
|
1500
|
+
format: 'jpeg',
|
|
1501
|
+
quality: this.options.downsampleQuality,
|
|
1502
|
+
multiplier: multiplier
|
|
1503
|
+
});
|
|
1423
1504
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1505
|
+
const img = new Image();
|
|
1506
|
+
img.onload = () => {
|
|
1507
|
+
try {
|
|
1508
|
+
const sxM = Math.round(sx * multiplier);
|
|
1509
|
+
const syM = Math.round(sy * multiplier);
|
|
1510
|
+
const swM = Math.round(sw * multiplier);
|
|
1511
|
+
const shM = Math.round(sh * multiplier);
|
|
1512
|
+
|
|
1513
|
+
const oc = document.createElement('canvas');
|
|
1514
|
+
oc.width = swM;
|
|
1515
|
+
oc.height = shM;
|
|
1516
|
+
const ctx = oc.getContext('2d');
|
|
1517
|
+
|
|
1518
|
+
ctx.drawImage(img, sxM, syM, swM, shM, 0, 0, swM, shM);
|
|
1519
|
+
const out = oc.toDataURL('image/jpeg', this.options.downsampleQuality);
|
|
1520
|
+
resolve(out);
|
|
1521
|
+
} catch (e) { reject(e); }
|
|
1522
|
+
};
|
|
1523
|
+
img.onerror = reject;
|
|
1524
|
+
img.src = fullDataUrl;
|
|
1525
|
+
} catch (e) { reject(e); }
|
|
1526
|
+
});
|
|
1527
|
+
} finally {
|
|
1528
|
+
masksBackup.forEach(b => {
|
|
1529
|
+
try {
|
|
1530
|
+
b.obj.set({
|
|
1531
|
+
opacity: b.opacity,
|
|
1532
|
+
fill: b.fill,
|
|
1533
|
+
strokeWidth: b.strokeWidth,
|
|
1534
|
+
stroke: b.stroke,
|
|
1535
|
+
selectable: b.selectable,
|
|
1536
|
+
lockRotation: b.lockRotation
|
|
1537
|
+
});
|
|
1538
|
+
b.obj.setCoords();
|
|
1539
|
+
} catch (e) { void e; }
|
|
1540
|
+
});
|
|
1433
1541
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
try {
|
|
1437
|
-
b.obj.set({
|
|
1438
|
-
opacity: b.opacity,
|
|
1439
|
-
fill: b.fill,
|
|
1440
|
-
strokeWidth: b.strokeWidth,
|
|
1441
|
-
stroke: b.stroke,
|
|
1442
|
-
selectable: b.selectable,
|
|
1443
|
-
lockRotation: b.lockRotation
|
|
1444
|
-
});
|
|
1445
|
-
b.obj.setCoords();
|
|
1446
|
-
} catch (e) { }
|
|
1447
|
-
});
|
|
1542
|
+
this.canvas.renderAll();
|
|
1543
|
+
}
|
|
1448
1544
|
|
|
1449
|
-
this.canvas.renderAll();
|
|
1450
1545
|
return finalBase64;
|
|
1451
1546
|
}
|
|
1452
1547
|
|
|
@@ -1595,12 +1690,12 @@
|
|
|
1595
1690
|
this.canvas.getObjects().forEach(o => {
|
|
1596
1691
|
if (o !== cropRect) {
|
|
1597
1692
|
this._cropPrevEvented.push({ obj: o, evented: o.evented, selectable: o.selectable });
|
|
1598
|
-
try { o.evented = false; o.selectable = false; } catch (e) {
|
|
1693
|
+
try { o.evented = false; o.selectable = false; } catch (e) { void e; }
|
|
1599
1694
|
}
|
|
1600
1695
|
});
|
|
1601
1696
|
|
|
1602
1697
|
// When the crop rect changes, re-render
|
|
1603
|
-
const onModified = () => { try { cropRect.setCoords(); this.canvas.requestRenderAll(); } catch (e) { } };
|
|
1698
|
+
const onModified = () => { try { cropRect.setCoords(); this.canvas.requestRenderAll(); } catch (e) { void e; } };
|
|
1604
1699
|
cropRect.on('modified', onModified);
|
|
1605
1700
|
cropRect.on('moving', onModified);
|
|
1606
1701
|
cropRect.on('scaling', onModified);
|
|
@@ -1626,15 +1721,15 @@
|
|
|
1626
1721
|
h.handlers.forEach(rec => h.target.off(rec.evt, rec.fn));
|
|
1627
1722
|
});
|
|
1628
1723
|
}
|
|
1629
|
-
} catch (e) {
|
|
1724
|
+
} catch (e) { void e; }
|
|
1630
1725
|
|
|
1631
|
-
try { this.canvas.remove(this._cropRect); } catch (e) { }
|
|
1726
|
+
try { this.canvas.remove(this._cropRect); } catch (e) { void e; }
|
|
1632
1727
|
this._cropRect = null;
|
|
1633
1728
|
}
|
|
1634
1729
|
// restore evented/selectable flags
|
|
1635
1730
|
if (Array.isArray(this._cropPrevEvented)) {
|
|
1636
1731
|
this._cropPrevEvented.forEach(i => {
|
|
1637
|
-
try { i.obj.evented = i.evented; i.obj.selectable = i.selectable; } catch (e) { }
|
|
1732
|
+
try { i.obj.evented = i.evented; i.obj.selectable = i.selectable; } catch (e) { void e; }
|
|
1638
1733
|
});
|
|
1639
1734
|
}
|
|
1640
1735
|
this._cropPrevEvented = null;
|
|
@@ -1676,7 +1771,7 @@
|
|
|
1676
1771
|
}
|
|
1677
1772
|
beforeJson = JSON.stringify(jsonObj);
|
|
1678
1773
|
} catch (e) {
|
|
1679
|
-
|
|
1774
|
+
this._reportWarning('applyCrop: could not serialize before state', e);
|
|
1680
1775
|
beforeJson = null;
|
|
1681
1776
|
}
|
|
1682
1777
|
|
|
@@ -1690,14 +1785,18 @@
|
|
|
1690
1785
|
this._removeLabelForMask(m);
|
|
1691
1786
|
this.canvas.remove(m);
|
|
1692
1787
|
} catch (err) {
|
|
1693
|
-
|
|
1788
|
+
this._reportWarning('applyCrop: failed to remove mask', err);
|
|
1694
1789
|
}
|
|
1695
1790
|
});
|
|
1791
|
+
this._lastMask = null;
|
|
1792
|
+
this._lastMaskInitialLeft = null;
|
|
1793
|
+
this._lastMaskInitialTop = null;
|
|
1794
|
+
this._lastMaskInitialWidth = null;
|
|
1696
1795
|
this.canvas.discardActiveObject();
|
|
1697
1796
|
this.canvas.renderAll();
|
|
1698
1797
|
}
|
|
1699
1798
|
} catch (e) {
|
|
1700
|
-
|
|
1799
|
+
this._reportWarning('applyCrop: error while removing masks', e);
|
|
1701
1800
|
}
|
|
1702
1801
|
|
|
1703
1802
|
try {
|
|
@@ -1708,11 +1807,11 @@
|
|
|
1708
1807
|
h.handlers.forEach(rec => h.target.off(rec.evt, rec.fn));
|
|
1709
1808
|
});
|
|
1710
1809
|
}
|
|
1711
|
-
} catch (e) {
|
|
1712
|
-
try { this.canvas.remove(this._cropRect); } catch (e) {
|
|
1810
|
+
} catch (e) { void e; }
|
|
1811
|
+
try { this.canvas.remove(this._cropRect); } catch (e) { void e; }
|
|
1713
1812
|
this._cropRect = null;
|
|
1714
1813
|
}
|
|
1715
|
-
} catch (e) {
|
|
1814
|
+
} catch (e) { void e; }
|
|
1716
1815
|
|
|
1717
1816
|
// End crop mode
|
|
1718
1817
|
this._cropMode = false;
|
|
@@ -1747,7 +1846,7 @@
|
|
|
1747
1846
|
img.src = fullDataUrl;
|
|
1748
1847
|
});
|
|
1749
1848
|
} catch (e) {
|
|
1750
|
-
|
|
1849
|
+
this._reportError('applyCrop: failed to create cropped image', e);
|
|
1751
1850
|
this._updateUI();
|
|
1752
1851
|
return;
|
|
1753
1852
|
}
|
|
@@ -1756,7 +1855,7 @@
|
|
|
1756
1855
|
try {
|
|
1757
1856
|
await this.loadImage(croppedBase64);
|
|
1758
1857
|
} catch (e) {
|
|
1759
|
-
|
|
1858
|
+
this._reportError('applyCrop: loadImage(croppedBase64) failed', e);
|
|
1760
1859
|
this._updateUI();
|
|
1761
1860
|
return;
|
|
1762
1861
|
}
|
|
@@ -1770,7 +1869,7 @@
|
|
|
1770
1869
|
}
|
|
1771
1870
|
afterJson = JSON.stringify(jsonObj2);
|
|
1772
1871
|
} catch (e) {
|
|
1773
|
-
|
|
1872
|
+
this._reportWarning('applyCrop: failed to serialize after state', e);
|
|
1774
1873
|
afterJson = null;
|
|
1775
1874
|
}
|
|
1776
1875
|
|
|
@@ -1795,7 +1894,7 @@
|
|
|
1795
1894
|
this.historyManager.currentIndex++;
|
|
1796
1895
|
}
|
|
1797
1896
|
} catch (e) {
|
|
1798
|
-
|
|
1897
|
+
this._reportWarning('applyCrop: failed to push history command', e);
|
|
1799
1898
|
}
|
|
1800
1899
|
|
|
1801
1900
|
// Final UI update
|
|
@@ -1915,18 +2014,18 @@
|
|
|
1915
2014
|
const el = document.getElementById(this.elements[key]);
|
|
1916
2015
|
if (!el) continue;
|
|
1917
2016
|
handlers.forEach(h => {
|
|
1918
|
-
try { el.removeEventListener(h.event, h.handler); } catch (e) { }
|
|
2017
|
+
try { el.removeEventListener(h.event, h.handler); } catch (e) { void e; }
|
|
1919
2018
|
});
|
|
1920
2019
|
}
|
|
1921
|
-
} catch (e) { }
|
|
2020
|
+
} catch (e) { void e; }
|
|
1922
2021
|
|
|
1923
2022
|
if (this._cropRect) {
|
|
1924
|
-
try { this.canvas.remove(this._cropRect); } catch (e) { }
|
|
2023
|
+
try { this.canvas.remove(this._cropRect); } catch (e) { void e; }
|
|
1925
2024
|
this._cropRect = null;
|
|
1926
2025
|
}
|
|
1927
2026
|
|
|
1928
2027
|
if (this.canvas) {
|
|
1929
|
-
try { this.canvas.dispose(); } catch (e) { }
|
|
2028
|
+
try { this.canvas.dispose(); } catch (e) { void e; }
|
|
1930
2029
|
this.canvas = null;
|
|
1931
2030
|
this.canvasEl = null;
|
|
1932
2031
|
this.isImageLoadedToCanvas = false;
|
|
@@ -2108,5 +2207,5 @@
|
|
|
2108
2207
|
}
|
|
2109
2208
|
}
|
|
2110
2209
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2210
|
+
export { ImageEditor };
|
|
2211
|
+
export default ImageEditor;
|