@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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file image-editor.js
3
3
  * @module image-editor
4
- * @version 1.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
- (function (root, factory) {
16
- if (typeof define === 'function' && define.amd) {
17
- // AMD / RequireJS
18
- define([], factory)
19
- } else if (typeof module === 'object' && module.exports) {
20
- // CommonJS / Node / webpack (target=commonjs)
21
- module.exports = factory()
22
- } else {
23
- // Browser normal <script> method, hanging to the global
24
- root.ImageEditor = factory()
25
- }
26
- })(typeof self !== 'undefined' ? self : this, function () {
27
- 'use strict'
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='#ffffff'] - The canvas background color.
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: '#ffffff',
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
- ...options
120
- };
121
- this.options.label = {
122
- getText: (mask, maskIndex) => mask.maskName,
123
- textOptions: {
124
- fontSize: 12,
125
- fill: '#fff',
126
- backgroundColor: 'rgba(0,0,0,0.7)',
127
- padding: 2,
128
- fontFamily: "monospace",
129
- fontWeight: "bold",
130
- selectable: false,
131
- evented: false,
132
- originX: 'left',
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
- this.options.crop = {
137
- minWidth: 100,
138
- minHeight: 100,
139
- padding: 10,
140
- hideMasksDuringCrop: true,
141
- preserveMasksAfterCrop: true,
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 => console.error('applyCrop failed', 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) => { console.error(`[ImageEditor: fileReadError]`, 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
- fabric.Image.fromURL(loadSrc, (fimg) => {
406
- this.canvas.discardActiveObject();
407
- this._hideAllMaskLabels();
408
- this.canvas.clear();
409
- this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
410
-
411
- fimg.set({ originX: 'left', originY: 'top', selectable: false, evented: false });
412
-
413
- const imgW = fimg.width;
414
- const imgH = fimg.height;
415
-
416
- const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || this.options.canvasWidth) : this.options.canvasWidth;
417
- const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || this.options.canvasHeight) : this.options.canvasHeight;
418
-
419
- if (this.options.fitImageToCanvas) {
420
- // Fit into current canvas (shrink only)
421
- const cw = Math.max(this.options.canvasWidth, minW);
422
- const ch = Math.max(this.options.canvasHeight, minH);
423
- this._setCanvasSizeInt(cw, ch);
424
- const fitScale = Math.min(cw / imgW, ch / imgH, 1);
425
- fimg.set({ left: 0, top: 0 });
426
- fimg.scale(fitScale);
427
- this.baseImageScale = fimg.scaleX || 1;
428
- } else if (this.options.expandCanvasToImage) {
429
- // Expand canvas so that it fully contains the image
430
- const cw = Math.max(minW, Math.floor(imgW));
431
- const ch = Math.max(minH, Math.floor(imgH));
432
- this._setCanvasSizeInt(cw, ch);
433
- fimg.set({ left: 0, top: 0 });
434
- fimg.scale(1);
435
- this.baseImageScale = 1;
436
- } else {
437
- // Keep existing canvas size and center the image
438
- const cw = Math.max(this.options.canvasWidth, minW);
439
- const ch = Math.max(this.options.canvasHeight, minH);
440
- this._setCanvasSizeInt(cw, ch);
441
- const fitScale = Math.min(cw / imgW, ch / imgH, 1);
442
- fimg.set({ left: (cw - imgW * fitScale) / 2, top: (ch - imgH * fitScale) / 2 });
443
- fimg.scale(fitScale);
444
- this.baseImageScale = fimg.scaleX || 1;
445
- }
446
- // Put the image onto the canvas
447
- this.originalImage = fimg;
448
- this.canvas.add(fimg);
449
- this.canvas.sendToBack(fimg);
450
-
451
- // Reset mask placement memory
452
- this._lastMaskInitialLeft = null;
453
- this._lastMaskInitialTop = null;
454
- this._lastMaskInitialWidth = null;
455
-
456
- this.maskCounter = 0;
457
- this.currentScale = 1;
458
- this.currentRotation = 0;
459
-
460
- this._updateInputs();
461
- this._updateMaskList();
462
- this._updateUI();
463
- this.canvas.renderAll();
464
- this.isImageLoadedToCanvas = true;
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
- if (typeof this.onImageLoaded === 'function') {
467
- this.onImageLoaded();
468
- }
469
- }, { crossOrigin: 'anonymous' });
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
- this.originalImage instanceof fabric.Image &&
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
- console.error('reset() failed', err);
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
- this._hideAllMaskLabels();
787
- const objs = this.canvas.getObjects();
788
- this.originalImage = objs.find(o => o.type === 'image' && !o.maskId) || null;
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
- const masks = objs.filter(o => o.maskId);
794
- this.maskCounter = masks.reduce((max, m) =>
795
- Math.max(max, m.maskId), 0);
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
- this.canvas.renderAll();
798
- this._updateMaskList();
799
- this._updateUI();
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
- console.error('loadFromState() failed', e);
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
- console.warn('saveState: failed to save canvas snapshot', err);
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) { /* ignore */ }
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
- console.error('merge error', err);
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 => console.error('download error', 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
- // Remove labels, deselect
1372
- masks.forEach(m => this._removeLabelForMask(m));
1373
- this.canvas.discardActiveObject();
1374
- this.canvas.renderAll();
1375
-
1376
- // Set masks to opaque black no border
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
- const img = new Image();
1401
- img.onload = () => {
1402
- try {
1403
- const sxM = Math.round(sx * multiplier);
1404
- const syM = Math.round(sy * multiplier);
1405
- const swM = Math.round(sw * multiplier);
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
- const oc = document.createElement('canvas');
1409
- oc.width = swM;
1410
- oc.height = shM;
1411
- const ctx = oc.getContext('2d');
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
- ctx.drawImage(img, sxM, syM, swM, shM, 0, 0, swM, shM);
1414
- const out = oc.toDataURL('image/jpeg', this.options.downsampleQuality);
1415
- resolve(out);
1416
- } catch (e) { reject(e); }
1417
- };
1418
- img.onerror = reject;
1419
- img.src = fullDataUrl;
1420
- } catch (e) { reject(e); }
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
- // Restore masks
1424
- masksBackup.forEach(b => {
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) { /* ignore */ }
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) { /* ignore */ }
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
- console.warn('applyCrop: could not serialize before state', e);
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
- console.warn('applyCrop: failed to remove mask', err);
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
- console.warn('applyCrop: error while removing masks', e);
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) { /* ignore */ }
1701
- try { this.canvas.remove(this._cropRect); } catch (e) { /* ignore */ }
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) { /* ignore */ }
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
- console.error('applyCrop: failed to create cropped image', e);
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
- console.error('applyCrop: loadImage(croppedBase64) failed', e);
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
- console.warn('applyCrop: failed to serialize after state', e);
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
- console.warn('applyCrop: failed to push history command', e);
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
- return ImageEditor
2101
- })
2210
+ export { ImageEditor };
2211
+ export default ImageEditor;