@bensitu/image-editor 1.1.2 → 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 -207
- 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 +329 -219
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.1
|
|
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
|
*
|
|
@@ -45,7 +56,7 @@
|
|
|
45
56
|
* @param {Object} [options={}] - Customization options to override defaults.
|
|
46
57
|
* @param {number} [options.canvasWidth=800] - The initial canvas width in pixels.
|
|
47
58
|
* @param {number} [options.canvasHeight=600] - The initial canvas height in pixels.
|
|
48
|
-
* @param {string} [options.backgroundColor='
|
|
59
|
+
* @param {string} [options.backgroundColor='transparent'] - The canvas background color.
|
|
49
60
|
* @param {number} [options.animationDuration=300] - Duration in ms for scale/rotate animations.
|
|
50
61
|
* @param {number} [options.minScale=0.1] - Minimum image scaling factor.
|
|
51
62
|
* @param {number} [options.maxScale=5.0] - Maximum image scaling factor.
|
|
@@ -53,6 +64,7 @@
|
|
|
53
64
|
* @param {number} [options.rotationStep=90] - Rotation step in degrees.
|
|
54
65
|
* @param {boolean} [options.expandCanvasToImage=true] - If true, expands the canvas to fit image/mask.
|
|
55
66
|
* @param {boolean} [options.fitImageToCanvas=false] - If true, fits loaded image inside canvas.
|
|
67
|
+
* @param {boolean} [options.coverImageToCanvas=false] - If true, scales image to cover canvas (at least one side fits, allowing overflow).
|
|
56
68
|
* @param {boolean} [options.downsampleOnLoad=true] - Whether to downsample very large images on load.
|
|
57
69
|
* @param {number} [options.downsampleMaxWidth=4000] - Max width for downsampling.
|
|
58
70
|
* @param {number} [options.downsampleMaxHeight=3000] - Max height for downsampling.
|
|
@@ -69,21 +81,43 @@
|
|
|
69
81
|
* @param {string|null} [options.initialImageBase64=null] - Base64 string to auto-load as initial image, if any.
|
|
70
82
|
* @param {string} [options.defaultDownloadFileName='edited_image.jpg'] - Default file name for downloads.
|
|
71
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.
|
|
72
86
|
*
|
|
73
87
|
* @constructor
|
|
74
88
|
*/
|
|
75
89
|
class ImageEditor {
|
|
76
90
|
constructor(options = {}) {
|
|
77
|
-
// Verify that fabric.js is present
|
|
78
|
-
this._fabricLoaded = typeof fabric !== 'undefined';
|
|
79
|
-
if (!this._fabricLoaded) {
|
|
80
|
-
console.error('fabric.js is not loaded. Please include fabric.js first. Initialization will be aborted.');
|
|
81
|
-
}
|
|
82
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 || {};
|
|
83
117
|
this.options = {
|
|
84
118
|
canvasWidth: 800,
|
|
85
119
|
canvasHeight: 600,
|
|
86
|
-
backgroundColor: '
|
|
120
|
+
backgroundColor: 'transparent',
|
|
87
121
|
|
|
88
122
|
animationDuration: 300,
|
|
89
123
|
minScale: 0.1,
|
|
@@ -93,6 +127,7 @@
|
|
|
93
127
|
|
|
94
128
|
expandCanvasToImage: true,
|
|
95
129
|
fitImageToCanvas: false,
|
|
130
|
+
coverImageToCanvas: false,
|
|
96
131
|
|
|
97
132
|
downsampleOnLoad: true,
|
|
98
133
|
downsampleMaxWidth: 4000,
|
|
@@ -115,32 +150,29 @@
|
|
|
115
150
|
initialImageBase64: null, // Provide a base64 'data:image/...' string here if you want auto-load
|
|
116
151
|
|
|
117
152
|
defaultDownloadFileName: 'edited_image.jpg',
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
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
|
|
134
168
|
}
|
|
135
169
|
};
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
allowRotationOfCropRect: false
|
|
143
|
-
};
|
|
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
|
+
}
|
|
144
176
|
|
|
145
177
|
// Runtime state
|
|
146
178
|
this.canvas = null;
|
|
@@ -160,6 +192,7 @@
|
|
|
160
192
|
|
|
161
193
|
this._boundHandlers = {};
|
|
162
194
|
|
|
195
|
+
this._lastMask = null;
|
|
163
196
|
this._lastMaskInitialLeft = null;
|
|
164
197
|
this._lastMaskInitialTop = null;
|
|
165
198
|
this._lastMaskInitialWidth = null;
|
|
@@ -239,6 +272,28 @@
|
|
|
239
272
|
}
|
|
240
273
|
}
|
|
241
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
|
+
|
|
242
297
|
/**
|
|
243
298
|
* Canvas setup helpers
|
|
244
299
|
* @private
|
|
@@ -336,7 +391,7 @@
|
|
|
336
391
|
|
|
337
392
|
// Crop bindings (optional: bound only if element IDs exist in elements)
|
|
338
393
|
this._bindIfExists('cropBtn', 'click', () => this.enterCropMode());
|
|
339
|
-
this._bindIfExists('applyCropBtn', 'click', () => { this.applyCrop().catch(e =>
|
|
394
|
+
this._bindIfExists('applyCropBtn', 'click', () => { this.applyCrop().catch(e => this._reportError('applyCrop failed', e)); });
|
|
340
395
|
this._bindIfExists('cancelCropBtn', 'click', () => this.cancelCrop());
|
|
341
396
|
}
|
|
342
397
|
|
|
@@ -368,7 +423,7 @@
|
|
|
368
423
|
if (!file || !file.type.startsWith('image/')) return;
|
|
369
424
|
const reader = new FileReader();
|
|
370
425
|
reader.onload = (e) => this.loadImage(e.target.result);
|
|
371
|
-
reader.onerror = (e) => {
|
|
426
|
+
reader.onerror = (e) => { this._reportError('Image file could not be read', e); };
|
|
372
427
|
reader.readAsDataURL(file);
|
|
373
428
|
}
|
|
374
429
|
|
|
@@ -379,6 +434,7 @@
|
|
|
379
434
|
*/
|
|
380
435
|
async loadImage(base64) {
|
|
381
436
|
if (!this._fabricLoaded) return;
|
|
437
|
+
if (!this.canvas) return;
|
|
382
438
|
if (!base64 || typeof base64 !== 'string' || !base64.startsWith('data:image/')) return;
|
|
383
439
|
|
|
384
440
|
this._setPlaceholderVisible(false);
|
|
@@ -402,71 +458,91 @@
|
|
|
402
458
|
}
|
|
403
459
|
|
|
404
460
|
// Create fabric.Image from URL
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
465
539
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
540
|
+
resolve();
|
|
541
|
+
} catch (err) {
|
|
542
|
+
reject(err);
|
|
543
|
+
}
|
|
544
|
+
}, { crossOrigin: 'anonymous' });
|
|
545
|
+
});
|
|
470
546
|
}
|
|
471
547
|
|
|
472
548
|
/**
|
|
@@ -474,9 +550,11 @@
|
|
|
474
550
|
* @returns {boolean} true if loaded, false if not
|
|
475
551
|
*/
|
|
476
552
|
isImageLoaded() {
|
|
553
|
+
const fabricInstance = ensureFabric();
|
|
477
554
|
return !!(
|
|
478
555
|
this.originalImage &&
|
|
479
|
-
|
|
556
|
+
fabricInstance &&
|
|
557
|
+
this.originalImage instanceof fabricInstance.Image &&
|
|
480
558
|
this.originalImage.width > 0 &&
|
|
481
559
|
this.originalImage.height > 0
|
|
482
560
|
);
|
|
@@ -766,7 +844,7 @@
|
|
|
766
844
|
this.saveState();
|
|
767
845
|
})
|
|
768
846
|
.catch(err => {
|
|
769
|
-
|
|
847
|
+
this._reportError('reset() failed', err);
|
|
770
848
|
});
|
|
771
849
|
}
|
|
772
850
|
|
|
@@ -783,24 +861,38 @@
|
|
|
783
861
|
: jsonString;
|
|
784
862
|
|
|
785
863
|
this.canvas.loadFromJSON(json, () => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
this.originalImage.set({ originX: 'left', originY: 'top', selectable: false, evented: false, hasControls: false, hoverCursor: 'default' });
|
|
791
|
-
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;
|
|
792
868
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
+
}
|
|
796
873
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
+
}
|
|
800
892
|
});
|
|
801
893
|
|
|
802
894
|
} catch (e) {
|
|
803
|
-
|
|
895
|
+
this._reportError('loadFromState() failed', e);
|
|
804
896
|
}
|
|
805
897
|
}
|
|
806
898
|
|
|
@@ -842,7 +934,7 @@
|
|
|
842
934
|
}
|
|
843
935
|
this._updateUI();
|
|
844
936
|
} catch (err) {
|
|
845
|
-
|
|
937
|
+
this._reportWarning('saveState: failed to save canvas snapshot', err);
|
|
846
938
|
}
|
|
847
939
|
}
|
|
848
940
|
|
|
@@ -968,7 +1060,7 @@
|
|
|
968
1060
|
...cfg.styles
|
|
969
1061
|
});
|
|
970
1062
|
break;
|
|
971
|
-
case 'polygon':
|
|
1063
|
+
case 'polygon': {
|
|
972
1064
|
let polyPoints = cfg.points || [];
|
|
973
1065
|
if (Array.isArray(polyPoints) && polyPoints.length && typeof polyPoints[0] === 'object') {
|
|
974
1066
|
// Ensure numeric {x,y} objects for fabric.Polygon
|
|
@@ -982,6 +1074,7 @@
|
|
|
982
1074
|
...cfg.styles
|
|
983
1075
|
});
|
|
984
1076
|
break;
|
|
1077
|
+
}
|
|
985
1078
|
case 'rect':
|
|
986
1079
|
default:
|
|
987
1080
|
mask = new fabric.Rect({
|
|
@@ -1055,6 +1148,15 @@
|
|
|
1055
1148
|
if (!active || !active.maskId) return;
|
|
1056
1149
|
this._removeLabelForMask(active);
|
|
1057
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
|
+
}
|
|
1058
1160
|
this.canvas.discardActiveObject();
|
|
1059
1161
|
this._updateMaskList();
|
|
1060
1162
|
this._updateUI();
|
|
@@ -1071,6 +1173,7 @@
|
|
|
1071
1173
|
masks.forEach(m => this._removeLabelForMask(m));
|
|
1072
1174
|
masks.forEach(m => this.canvas.remove(m));
|
|
1073
1175
|
this.canvas.discardActiveObject();
|
|
1176
|
+
this._lastMask = null;
|
|
1074
1177
|
this._lastMaskInitialLeft = null;
|
|
1075
1178
|
this._lastMaskInitialTop = null;
|
|
1076
1179
|
this._lastMaskInitialWidth = null;
|
|
@@ -1094,8 +1197,8 @@
|
|
|
1094
1197
|
if (objs.includes(mask.__label)) {
|
|
1095
1198
|
this.canvas.remove(mask.__label);
|
|
1096
1199
|
}
|
|
1097
|
-
} catch (e) {
|
|
1098
|
-
try { delete mask.__label; } catch (e) { }
|
|
1200
|
+
} catch (e) { void e; }
|
|
1201
|
+
try { delete mask.__label; } catch (e) { void e; }
|
|
1099
1202
|
}
|
|
1100
1203
|
}
|
|
1101
1204
|
|
|
@@ -1158,9 +1261,9 @@
|
|
|
1158
1261
|
labels.forEach(l => {
|
|
1159
1262
|
try {
|
|
1160
1263
|
if (objs.includes(l)) this.canvas.remove(l);
|
|
1161
|
-
} catch (e) { }
|
|
1264
|
+
} catch (e) { void e; }
|
|
1162
1265
|
});
|
|
1163
|
-
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; } } });
|
|
1164
1267
|
}
|
|
1165
1268
|
|
|
1166
1269
|
/**
|
|
@@ -1230,7 +1333,7 @@
|
|
|
1230
1333
|
masks.forEach(m => {
|
|
1231
1334
|
if (m !== selectedMask) {
|
|
1232
1335
|
if (m.__label) {
|
|
1233
|
-
try { this.canvas.remove(m.__label); } catch (e) { }
|
|
1336
|
+
try { this.canvas.remove(m.__label); } catch (e) { void e; }
|
|
1234
1337
|
delete m.__label;
|
|
1235
1338
|
}
|
|
1236
1339
|
m.set({ stroke: '#ccc', strokeWidth: 1 });
|
|
@@ -1301,7 +1404,7 @@
|
|
|
1301
1404
|
await this.loadImage(merged);
|
|
1302
1405
|
this.saveState();
|
|
1303
1406
|
} catch (err) {
|
|
1304
|
-
|
|
1407
|
+
this._reportError('merge error', err);
|
|
1305
1408
|
if (this.canvasEl) this.canvasEl.style.visibility = '';
|
|
1306
1409
|
}
|
|
1307
1410
|
}
|
|
@@ -1323,7 +1426,7 @@
|
|
|
1323
1426
|
link.click();
|
|
1324
1427
|
document.body.removeChild(link);
|
|
1325
1428
|
})
|
|
1326
|
-
.catch(err =>
|
|
1429
|
+
.catch(err => this._reportError('download error', err));
|
|
1327
1430
|
}
|
|
1328
1431
|
|
|
1329
1432
|
/**
|
|
@@ -1368,74 +1471,77 @@
|
|
|
1368
1471
|
lockRotation: m.lockRotation
|
|
1369
1472
|
}));
|
|
1370
1473
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
masks.forEach(m => {
|
|
1378
|
-
m.set({ opacity: 1, fill: '#000000', strokeWidth: 0, stroke: null, selectable: false });
|
|
1379
|
-
m.setCoords();
|
|
1380
|
-
});
|
|
1381
|
-
this.canvas.renderAll();
|
|
1382
|
-
|
|
1383
|
-
// Compute integer bounding box for image
|
|
1384
|
-
this.originalImage.setCoords();
|
|
1385
|
-
const imgBr = this.originalImage.getBoundingRect(true, true);
|
|
1386
|
-
const sx = Math.max(0, Math.round(imgBr.left));
|
|
1387
|
-
const sy = Math.max(0, Math.round(imgBr.top));
|
|
1388
|
-
const sw = Math.max(1, Math.round(imgBr.width));
|
|
1389
|
-
const sh = Math.max(1, Math.round(imgBr.height));
|
|
1390
|
-
|
|
1391
|
-
// Crop precisely in offscreen canvas
|
|
1392
|
-
const finalBase64 = await new Promise((resolve, reject) => {
|
|
1393
|
-
try {
|
|
1394
|
-
const fullDataUrl = this.canvas.toDataURL({
|
|
1395
|
-
format: 'jpeg',
|
|
1396
|
-
quality: this.options.downsampleQuality,
|
|
1397
|
-
multiplier: multiplier
|
|
1398
|
-
});
|
|
1474
|
+
let finalBase64;
|
|
1475
|
+
try {
|
|
1476
|
+
// Remove labels, deselect
|
|
1477
|
+
masks.forEach(m => this._removeLabelForMask(m));
|
|
1478
|
+
this.canvas.discardActiveObject();
|
|
1479
|
+
this.canvas.renderAll();
|
|
1399
1480
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
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();
|
|
1407
1487
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
+
});
|
|
1412
1504
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
+
});
|
|
1422
1541
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
try {
|
|
1426
|
-
b.obj.set({
|
|
1427
|
-
opacity: b.opacity,
|
|
1428
|
-
fill: b.fill,
|
|
1429
|
-
strokeWidth: b.strokeWidth,
|
|
1430
|
-
stroke: b.stroke,
|
|
1431
|
-
selectable: b.selectable,
|
|
1432
|
-
lockRotation: b.lockRotation
|
|
1433
|
-
});
|
|
1434
|
-
b.obj.setCoords();
|
|
1435
|
-
} catch (e) { }
|
|
1436
|
-
});
|
|
1542
|
+
this.canvas.renderAll();
|
|
1543
|
+
}
|
|
1437
1544
|
|
|
1438
|
-
this.canvas.renderAll();
|
|
1439
1545
|
return finalBase64;
|
|
1440
1546
|
}
|
|
1441
1547
|
|
|
@@ -1584,12 +1690,12 @@
|
|
|
1584
1690
|
this.canvas.getObjects().forEach(o => {
|
|
1585
1691
|
if (o !== cropRect) {
|
|
1586
1692
|
this._cropPrevEvented.push({ obj: o, evented: o.evented, selectable: o.selectable });
|
|
1587
|
-
try { o.evented = false; o.selectable = false; } catch (e) {
|
|
1693
|
+
try { o.evented = false; o.selectable = false; } catch (e) { void e; }
|
|
1588
1694
|
}
|
|
1589
1695
|
});
|
|
1590
1696
|
|
|
1591
1697
|
// When the crop rect changes, re-render
|
|
1592
|
-
const onModified = () => { try { cropRect.setCoords(); this.canvas.requestRenderAll(); } catch (e) { } };
|
|
1698
|
+
const onModified = () => { try { cropRect.setCoords(); this.canvas.requestRenderAll(); } catch (e) { void e; } };
|
|
1593
1699
|
cropRect.on('modified', onModified);
|
|
1594
1700
|
cropRect.on('moving', onModified);
|
|
1595
1701
|
cropRect.on('scaling', onModified);
|
|
@@ -1615,15 +1721,15 @@
|
|
|
1615
1721
|
h.handlers.forEach(rec => h.target.off(rec.evt, rec.fn));
|
|
1616
1722
|
});
|
|
1617
1723
|
}
|
|
1618
|
-
} catch (e) {
|
|
1724
|
+
} catch (e) { void e; }
|
|
1619
1725
|
|
|
1620
|
-
try { this.canvas.remove(this._cropRect); } catch (e) { }
|
|
1726
|
+
try { this.canvas.remove(this._cropRect); } catch (e) { void e; }
|
|
1621
1727
|
this._cropRect = null;
|
|
1622
1728
|
}
|
|
1623
1729
|
// restore evented/selectable flags
|
|
1624
1730
|
if (Array.isArray(this._cropPrevEvented)) {
|
|
1625
1731
|
this._cropPrevEvented.forEach(i => {
|
|
1626
|
-
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; }
|
|
1627
1733
|
});
|
|
1628
1734
|
}
|
|
1629
1735
|
this._cropPrevEvented = null;
|
|
@@ -1665,7 +1771,7 @@
|
|
|
1665
1771
|
}
|
|
1666
1772
|
beforeJson = JSON.stringify(jsonObj);
|
|
1667
1773
|
} catch (e) {
|
|
1668
|
-
|
|
1774
|
+
this._reportWarning('applyCrop: could not serialize before state', e);
|
|
1669
1775
|
beforeJson = null;
|
|
1670
1776
|
}
|
|
1671
1777
|
|
|
@@ -1679,14 +1785,18 @@
|
|
|
1679
1785
|
this._removeLabelForMask(m);
|
|
1680
1786
|
this.canvas.remove(m);
|
|
1681
1787
|
} catch (err) {
|
|
1682
|
-
|
|
1788
|
+
this._reportWarning('applyCrop: failed to remove mask', err);
|
|
1683
1789
|
}
|
|
1684
1790
|
});
|
|
1791
|
+
this._lastMask = null;
|
|
1792
|
+
this._lastMaskInitialLeft = null;
|
|
1793
|
+
this._lastMaskInitialTop = null;
|
|
1794
|
+
this._lastMaskInitialWidth = null;
|
|
1685
1795
|
this.canvas.discardActiveObject();
|
|
1686
1796
|
this.canvas.renderAll();
|
|
1687
1797
|
}
|
|
1688
1798
|
} catch (e) {
|
|
1689
|
-
|
|
1799
|
+
this._reportWarning('applyCrop: error while removing masks', e);
|
|
1690
1800
|
}
|
|
1691
1801
|
|
|
1692
1802
|
try {
|
|
@@ -1697,11 +1807,11 @@
|
|
|
1697
1807
|
h.handlers.forEach(rec => h.target.off(rec.evt, rec.fn));
|
|
1698
1808
|
});
|
|
1699
1809
|
}
|
|
1700
|
-
} catch (e) {
|
|
1701
|
-
try { this.canvas.remove(this._cropRect); } catch (e) {
|
|
1810
|
+
} catch (e) { void e; }
|
|
1811
|
+
try { this.canvas.remove(this._cropRect); } catch (e) { void e; }
|
|
1702
1812
|
this._cropRect = null;
|
|
1703
1813
|
}
|
|
1704
|
-
} catch (e) {
|
|
1814
|
+
} catch (e) { void e; }
|
|
1705
1815
|
|
|
1706
1816
|
// End crop mode
|
|
1707
1817
|
this._cropMode = false;
|
|
@@ -1736,7 +1846,7 @@
|
|
|
1736
1846
|
img.src = fullDataUrl;
|
|
1737
1847
|
});
|
|
1738
1848
|
} catch (e) {
|
|
1739
|
-
|
|
1849
|
+
this._reportError('applyCrop: failed to create cropped image', e);
|
|
1740
1850
|
this._updateUI();
|
|
1741
1851
|
return;
|
|
1742
1852
|
}
|
|
@@ -1745,7 +1855,7 @@
|
|
|
1745
1855
|
try {
|
|
1746
1856
|
await this.loadImage(croppedBase64);
|
|
1747
1857
|
} catch (e) {
|
|
1748
|
-
|
|
1858
|
+
this._reportError('applyCrop: loadImage(croppedBase64) failed', e);
|
|
1749
1859
|
this._updateUI();
|
|
1750
1860
|
return;
|
|
1751
1861
|
}
|
|
@@ -1759,7 +1869,7 @@
|
|
|
1759
1869
|
}
|
|
1760
1870
|
afterJson = JSON.stringify(jsonObj2);
|
|
1761
1871
|
} catch (e) {
|
|
1762
|
-
|
|
1872
|
+
this._reportWarning('applyCrop: failed to serialize after state', e);
|
|
1763
1873
|
afterJson = null;
|
|
1764
1874
|
}
|
|
1765
1875
|
|
|
@@ -1784,7 +1894,7 @@
|
|
|
1784
1894
|
this.historyManager.currentIndex++;
|
|
1785
1895
|
}
|
|
1786
1896
|
} catch (e) {
|
|
1787
|
-
|
|
1897
|
+
this._reportWarning('applyCrop: failed to push history command', e);
|
|
1788
1898
|
}
|
|
1789
1899
|
|
|
1790
1900
|
// Final UI update
|
|
@@ -1904,18 +2014,18 @@
|
|
|
1904
2014
|
const el = document.getElementById(this.elements[key]);
|
|
1905
2015
|
if (!el) continue;
|
|
1906
2016
|
handlers.forEach(h => {
|
|
1907
|
-
try { el.removeEventListener(h.event, h.handler); } catch (e) { }
|
|
2017
|
+
try { el.removeEventListener(h.event, h.handler); } catch (e) { void e; }
|
|
1908
2018
|
});
|
|
1909
2019
|
}
|
|
1910
|
-
} catch (e) { }
|
|
2020
|
+
} catch (e) { void e; }
|
|
1911
2021
|
|
|
1912
2022
|
if (this._cropRect) {
|
|
1913
|
-
try { this.canvas.remove(this._cropRect); } catch (e) { }
|
|
2023
|
+
try { this.canvas.remove(this._cropRect); } catch (e) { void e; }
|
|
1914
2024
|
this._cropRect = null;
|
|
1915
2025
|
}
|
|
1916
2026
|
|
|
1917
2027
|
if (this.canvas) {
|
|
1918
|
-
try { this.canvas.dispose(); } catch (e) { }
|
|
2028
|
+
try { this.canvas.dispose(); } catch (e) { void e; }
|
|
1919
2029
|
this.canvas = null;
|
|
1920
2030
|
this.canvasEl = null;
|
|
1921
2031
|
this.isImageLoadedToCanvas = false;
|
|
@@ -2097,5 +2207,5 @@
|
|
|
2097
2207
|
}
|
|
2098
2208
|
}
|
|
2099
2209
|
|
|
2100
|
-
|
|
2101
|
-
|
|
2210
|
+
export { ImageEditor };
|
|
2211
|
+
export default ImageEditor;
|