@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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file image-editor.js
3
3
  * @module image-editor
4
- * @version 1.2.0
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
  *
@@ -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
- ...options
122
- };
123
- this.options.label = {
124
- getText: (mask, maskIndex) => mask.maskName,
125
- textOptions: {
126
- fontSize: 12,
127
- fill: '#fff',
128
- backgroundColor: 'rgba(0,0,0,0.7)',
129
- padding: 2,
130
- fontFamily: "monospace",
131
- fontWeight: "bold",
132
- selectable: false,
133
- evented: false,
134
- originX: 'left',
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
- this.options.crop = {
139
- minWidth: 100,
140
- minHeight: 100,
141
- padding: 10,
142
- hideMasksDuringCrop: true,
143
- preserveMasksAfterCrop: true,
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 => console.error('applyCrop failed', 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) => { console.error(`[ImageEditor: fileReadError]`, 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
- fabric.Image.fromURL(loadSrc, (fimg) => {
408
- this.canvas.discardActiveObject();
409
- this._hideAllMaskLabels();
410
- this.canvas.clear();
411
- this.canvas.setBackgroundColor(this.options.backgroundColor, this.canvas.renderAll.bind(this.canvas));
412
-
413
- fimg.set({ originX: 'left', originY: 'top', selectable: false, evented: false });
414
-
415
- const imgW = fimg.width;
416
- const imgH = fimg.height;
417
-
418
- const minW = this.containerEl ? Math.floor(this.containerEl.clientWidth || this.options.canvasWidth) : this.options.canvasWidth;
419
- const minH = this.containerEl ? Math.floor(this.containerEl.clientHeight || this.options.canvasHeight) : this.options.canvasHeight;
420
-
421
- if (this.options.fitImageToCanvas) {
422
- // Fit into current canvas (shrink only) and ensure canvas does not exceed container
423
- const cw = Math.max(1, Math.min(this.options.canvasWidth, minW) - 1)
424
- const ch = Math.max(1, Math.min(this.options.canvasHeight, minH) - 1);
425
- this._setCanvasSizeInt(cw, ch);
426
- const fitScale = Math.min(cw / imgW, ch / imgH, 1);
427
- fimg.set({ left: 0, top: 0 });
428
- fimg.scale(fitScale);
429
- this.baseImageScale = fimg.scaleX || 1;
430
- } else if (this.options.coverImageToCanvas) {
431
- // Cover canvas: scale to cover, allowing overflow (at least one side fits)
432
- const cw = Math.max(this.options.canvasWidth, minW);
433
- const ch = Math.max(this.options.canvasHeight, minH);
434
- this._setCanvasSizeInt(cw, ch);
435
- const coverScale = Math.min(1, Math.max(cw / imgW, ch / imgH));
436
- fimg.set({ left: 0, top: 0 });
437
- fimg.scale(coverScale);
438
- this.baseImageScale = fimg.scaleX || 1;
439
- } else if (this.options.expandCanvasToImage) {
440
- // Expand canvas so that it fully contains the image
441
- const cw = Math.max(minW, Math.floor(imgW));
442
- const ch = Math.max(minH, Math.floor(imgH));
443
- this._setCanvasSizeInt(cw, ch);
444
- fimg.set({ left: 0, top: 0 });
445
- fimg.scale(1);
446
- this.baseImageScale = 1;
447
- } else {
448
- // Keep existing canvas size and center the image
449
- const cw = Math.max(this.options.canvasWidth, minW);
450
- const ch = Math.max(this.options.canvasHeight, minH);
451
- this._setCanvasSizeInt(cw, ch);
452
- const fitScale = Math.min(cw / imgW, ch / imgH, 1);
453
- fimg.set({ left: 0, top: 0 });
454
- fimg.scale(fitScale);
455
- this.baseImageScale = fimg.scaleX || 1;
456
- }
457
- // Put the image onto the canvas
458
- this.originalImage = fimg;
459
- this.canvas.add(fimg);
460
- this.canvas.sendToBack(fimg);
461
-
462
- // Reset mask placement memory
463
- this._lastMaskInitialLeft = null;
464
- this._lastMaskInitialTop = null;
465
- this._lastMaskInitialWidth = null;
466
-
467
- this.maskCounter = 0;
468
- this.currentScale = 1;
469
- this.currentRotation = 0;
470
-
471
- this._updateInputs();
472
- this._updateMaskList();
473
- this._updateUI();
474
- this.canvas.renderAll();
475
- 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
+ }
476
539
 
477
- if (typeof this.onImageLoaded === 'function') {
478
- this.onImageLoaded();
479
- }
480
- }, { crossOrigin: 'anonymous' });
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
- this.originalImage instanceof fabric.Image &&
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
- console.error('reset() failed', err);
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
- this._hideAllMaskLabels();
798
- const objs = this.canvas.getObjects();
799
- this.originalImage = objs.find(o => o.type === 'image' && !o.maskId) || null;
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
- const masks = objs.filter(o => o.maskId);
805
- this.maskCounter = masks.reduce((max, m) =>
806
- 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
+ }
807
873
 
808
- this.canvas.renderAll();
809
- this._updateMaskList();
810
- 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
+ }
811
892
  });
812
893
 
813
894
  } catch (e) {
814
- console.error('loadFromState() failed', e);
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
- console.warn('saveState: failed to save canvas snapshot', err);
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) { /* ignore */ }
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
- console.error('merge error', err);
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 => console.error('download error', 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
- // Remove labels, deselect
1383
- masks.forEach(m => this._removeLabelForMask(m));
1384
- this.canvas.discardActiveObject();
1385
- this.canvas.renderAll();
1386
-
1387
- // Set masks to opaque black no border
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
- const img = new Image();
1412
- img.onload = () => {
1413
- try {
1414
- const sxM = Math.round(sx * multiplier);
1415
- const syM = Math.round(sy * multiplier);
1416
- const swM = Math.round(sw * multiplier);
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
- const oc = document.createElement('canvas');
1420
- oc.width = swM;
1421
- oc.height = shM;
1422
- 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
+ });
1423
1504
 
1424
- ctx.drawImage(img, sxM, syM, swM, shM, 0, 0, swM, shM);
1425
- const out = oc.toDataURL('image/jpeg', this.options.downsampleQuality);
1426
- resolve(out);
1427
- } catch (e) { reject(e); }
1428
- };
1429
- img.onerror = reject;
1430
- img.src = fullDataUrl;
1431
- } catch (e) { reject(e); }
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
- // Restore masks
1435
- masksBackup.forEach(b => {
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) { /* ignore */ }
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) { /* ignore */ }
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
- console.warn('applyCrop: could not serialize before state', e);
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
- console.warn('applyCrop: failed to remove mask', err);
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
- console.warn('applyCrop: error while removing masks', e);
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) { /* ignore */ }
1712
- 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; }
1713
1812
  this._cropRect = null;
1714
1813
  }
1715
- } catch (e) { /* ignore */ }
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
- console.error('applyCrop: failed to create cropped image', e);
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
- console.error('applyCrop: loadImage(croppedBase64) failed', e);
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
- console.warn('applyCrop: failed to serialize after state', e);
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
- console.warn('applyCrop: failed to push history command', e);
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
- return ImageEditor
2112
- })
2210
+ export { ImageEditor };
2211
+ export default ImageEditor;