@bensitu/image-editor 2.0.0 → 2.1.0

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.
Files changed (83) hide show
  1. package/README.md +118 -16
  2. package/dist/cjs/index.cjs +1800 -330
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/animation/animation-queue.js +16 -9
  5. package/dist/esm/animation/animation-queue.js.map +1 -1
  6. package/dist/esm/core/default-options.js +216 -9
  7. package/dist/esm/core/default-options.js.map +1 -1
  8. package/dist/esm/core/operation-guard.js +28 -0
  9. package/dist/esm/core/operation-guard.js.map +1 -1
  10. package/dist/esm/core/public-types.js.map +1 -1
  11. package/dist/esm/core/state-serializer.js +5 -4
  12. package/dist/esm/core/state-serializer.js.map +1 -1
  13. package/dist/esm/crop/crop-controller.js +4 -2
  14. package/dist/esm/crop/crop-controller.js.map +1 -1
  15. package/dist/esm/export/export-service.js +21 -10
  16. package/dist/esm/export/export-service.js.map +1 -1
  17. package/dist/esm/fabric/fabric-animation.js +56 -4
  18. package/dist/esm/fabric/fabric-animation.js.map +1 -1
  19. package/dist/esm/image/image-loader.js +9 -16
  20. package/dist/esm/image/image-loader.js.map +1 -1
  21. package/dist/esm/image/image-resampler.js +7 -2
  22. package/dist/esm/image/image-resampler.js.map +1 -1
  23. package/dist/esm/image/layout-manager.js +2 -20
  24. package/dist/esm/image/layout-manager.js.map +1 -1
  25. package/dist/esm/image/transform-controller.js.map +1 -1
  26. package/dist/esm/image-editor.js +383 -47
  27. package/dist/esm/image-editor.js.map +1 -1
  28. package/dist/esm/mask/mask-factory.js +53 -29
  29. package/dist/esm/mask/mask-factory.js.map +1 -1
  30. package/dist/esm/mask/mask-list.js +9 -3
  31. package/dist/esm/mask/mask-list.js.map +1 -1
  32. package/dist/esm/mosaic/mosaic-controller.js +670 -0
  33. package/dist/esm/mosaic/mosaic-controller.js.map +1 -0
  34. package/dist/esm/mosaic/mosaic-geometry.js +81 -0
  35. package/dist/esm/mosaic/mosaic-geometry.js.map +1 -0
  36. package/dist/esm/mosaic/mosaic-pixelate.js +71 -0
  37. package/dist/esm/mosaic/mosaic-pixelate.js.map +1 -0
  38. package/dist/esm/ui/dom-bindings.js +10 -3
  39. package/dist/esm/ui/dom-bindings.js.map +1 -1
  40. package/dist/esm/utils/number.js.map +1 -1
  41. package/dist/types/animation/animation-queue.d.ts.map +1 -1
  42. package/dist/types/core/default-options.d.ts +34 -6
  43. package/dist/types/core/default-options.d.ts.map +1 -1
  44. package/dist/types/core/errors.d.ts +1 -1
  45. package/dist/types/core/operation-guard.d.ts +2 -0
  46. package/dist/types/core/operation-guard.d.ts.map +1 -1
  47. package/dist/types/core/public-types.d.ts +123 -13
  48. package/dist/types/core/public-types.d.ts.map +1 -1
  49. package/dist/types/core/state-serializer.d.ts +3 -1
  50. package/dist/types/core/state-serializer.d.ts.map +1 -1
  51. package/dist/types/crop/crop-controller.d.ts.map +1 -1
  52. package/dist/types/export/export-service.d.ts.map +1 -1
  53. package/dist/types/fabric/fabric-animation.d.ts.map +1 -1
  54. package/dist/types/image/image-loader.d.ts +2 -4
  55. package/dist/types/image/image-loader.d.ts.map +1 -1
  56. package/dist/types/image/image-resampler.d.ts +1 -1
  57. package/dist/types/image/image-resampler.d.ts.map +1 -1
  58. package/dist/types/image/layout-manager.d.ts +5 -49
  59. package/dist/types/image/layout-manager.d.ts.map +1 -1
  60. package/dist/types/image/transform-controller.d.ts +1 -2
  61. package/dist/types/image/transform-controller.d.ts.map +1 -1
  62. package/dist/types/image-editor.d.ts +20 -9
  63. package/dist/types/image-editor.d.ts.map +1 -1
  64. package/dist/types/index.d.cts +1 -1
  65. package/dist/types/index.d.cts.map +1 -1
  66. package/dist/types/index.d.ts +1 -1
  67. package/dist/types/index.d.ts.map +1 -1
  68. package/dist/types/mask/mask-factory.d.ts +24 -21
  69. package/dist/types/mask/mask-factory.d.ts.map +1 -1
  70. package/dist/types/mask/mask-list.d.ts.map +1 -1
  71. package/dist/types/mosaic/mosaic-controller.d.ts +82 -0
  72. package/dist/types/mosaic/mosaic-controller.d.ts.map +1 -0
  73. package/dist/types/mosaic/mosaic-geometry.d.ts +29 -0
  74. package/dist/types/mosaic/mosaic-geometry.d.ts.map +1 -0
  75. package/dist/types/mosaic/mosaic-pixelate.d.ts +23 -0
  76. package/dist/types/mosaic/mosaic-pixelate.d.ts.map +1 -0
  77. package/dist/types/ui/dom-bindings.d.ts +3 -1
  78. package/dist/types/ui/dom-bindings.d.ts.map +1 -1
  79. package/dist/types/utils/number.d.ts +1 -2
  80. package/dist/types/utils/number.d.ts.map +1 -1
  81. package/dist/umd/image-editor.umd.js +1 -1
  82. package/dist/umd/image-editor.umd.js.map +1 -1
  83. package/package.json +1 -1
@@ -1,15 +1,16 @@
1
1
  import { AnimationQueue } from './animation/animation-queue.js';
2
2
  import { reportError, reportWarning } from './core/callback-reporter.js';
3
- import { resolveOptions } from './core/default-options.js';
3
+ import { areResolvedMosaicConfigsEqual, cloneResolvedMosaicConfig, getInvalidMosaicConfigFields, isLayoutMode, mergeMosaicConfigPatch, resolveOptions, } from './core/default-options.js';
4
4
  import { OperationGuard } from './core/operation-guard.js';
5
5
  import { loadFromState as loadFromStateImpl, saveState as saveStateImpl, } from './core/state-serializer.js';
6
6
  import { Command, HistoryManager } from './history/history-manager.js';
7
7
  import { detectFabric } from './fabric/fabric-adapter.js';
8
8
  import { isMaskObject } from './core/public-types.js';
9
9
  import { applyCrop as applyCropImpl, cancelCrop as cancelCropImpl, enterCropMode as enterCropModeImpl, } from './crop/crop-controller.js';
10
+ import { enterMosaicMode as enterMosaicModeImpl, exitMosaicMode as exitMosaicModeImpl, updateMosaicPreview, } from './mosaic/mosaic-controller.js';
10
11
  import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
11
12
  import { loadImage as loadImageImpl } from './image/image-loader.js';
12
- import { ViewportCache, applyCanvasDimensions, computeScrollableCanvasSize, detectLayoutConflict, measureScrollbarSize, } from './image/layout-manager.js';
13
+ import { ViewportCache, applyCanvasDimensions, computeScrollableCanvasSize, measureScrollbarSize, } from './image/layout-manager.js';
13
14
  import { TransformController } from './image/transform-controller.js';
14
15
  import { createMask as createMaskImpl, removeAllMasks as removeAllMasksImpl, removeSelectedMask as removeSelectedMaskImpl, } from './mask/mask-factory.js';
15
16
  import { createLabelForMask, hideAllMaskLabels, removeLabelForMask, showLabelForMask, syncMaskLabel, } from './mask/mask-label-manager.js';
@@ -20,8 +21,8 @@ import { setPlaceholderVisible as setPlaceholderVisibleImpl } from './ui/visibil
20
21
  import { inferImageMimeType, readFileAsDataUrl, resetFileInput } from './utils/file.js';
21
22
  import { detectSourceMimeType } from './image/image-resampler.js';
22
23
  const LAYOUT_EPSILON = 0.5;
23
- const INTERNAL_OPERATION_TOKEN = Symbol.for('ImageEditorInternalOperation');
24
- const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol.for('ImageEditorAllowDuringAnimationQueue');
24
+ const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
25
+ const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol('ImageEditorAllowDuringAnimationQueue');
25
26
  const CROP_MODE_CONTROL_KEYS = [
26
27
  'scalePercentageInput',
27
28
  'rotateLeftDegreesInput',
@@ -42,9 +43,85 @@ const CROP_MODE_CONTROL_KEYS = [
42
43
  'enterCropModeButton',
43
44
  'applyCropButton',
44
45
  'cancelCropButton',
46
+ 'enterMosaicModeButton',
47
+ 'exitMosaicModeButton',
48
+ 'mosaicBrushSizeInput',
49
+ 'mosaicBlockSizeInput',
45
50
  ];
46
51
  const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
47
52
  const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
53
+ const MOSAIC_MODE_CONTROL_KEYS = [
54
+ 'scalePercentageInput',
55
+ 'rotateLeftDegreesInput',
56
+ 'rotateRightDegreesInput',
57
+ 'rotateLeftButton',
58
+ 'rotateRightButton',
59
+ 'createMaskButton',
60
+ 'removeSelectedMaskButton',
61
+ 'removeAllMasksButton',
62
+ 'mergeMasksButton',
63
+ 'downloadImageButton',
64
+ 'zoomInButton',
65
+ 'zoomOutButton',
66
+ 'resetImageTransformButton',
67
+ 'undoButton',
68
+ 'redoButton',
69
+ 'imageInput',
70
+ 'enterCropModeButton',
71
+ 'applyCropButton',
72
+ 'cancelCropButton',
73
+ 'enterMosaicModeButton',
74
+ 'exitMosaicModeButton',
75
+ 'mosaicBrushSizeInput',
76
+ 'mosaicBlockSizeInput',
77
+ ];
78
+ const MOSAIC_MODE_ENABLED_KEYS = [
79
+ 'exitMosaicModeButton',
80
+ 'mosaicBrushSizeInput',
81
+ 'mosaicBlockSizeInput',
82
+ ];
83
+ const MOSAIC_SESSION_ALLOWED_OPERATIONS = new Set([
84
+ 'exitMosaicMode',
85
+ 'applyMosaic',
86
+ 'setMosaicConfig',
87
+ 'resetMosaicConfig',
88
+ 'setMosaicBrushSize',
89
+ 'setMosaicBlockSize',
90
+ 'saveState',
91
+ ]);
92
+ const SCROLLBAR_SETTLE_EPSILON = 1;
93
+ const IMAGE_EDITOR_OPERATIONS = new Set([
94
+ 'init',
95
+ 'loadImage',
96
+ 'loadFromState',
97
+ 'saveState',
98
+ 'scaleImage',
99
+ 'rotateImage',
100
+ 'resetImageTransform',
101
+ 'createMask',
102
+ 'removeSelectedMask',
103
+ 'removeAllMasks',
104
+ 'mergeMasks',
105
+ 'enterCropMode',
106
+ 'applyCrop',
107
+ 'cancelCrop',
108
+ 'enterMosaicMode',
109
+ 'exitMosaicMode',
110
+ 'applyMosaic',
111
+ 'setMosaicConfig',
112
+ 'resetMosaicConfig',
113
+ 'setMosaicBrushSize',
114
+ 'setMosaicBlockSize',
115
+ 'undo',
116
+ 'redo',
117
+ 'exportImageBase64',
118
+ 'exportImageFile',
119
+ 'downloadImage',
120
+ 'dispose',
121
+ ]);
122
+ function isImageEditorOperation(value) {
123
+ return value !== null && IMAGE_EDITOR_OPERATIONS.has(value);
124
+ }
48
125
  export class ImageEditor {
49
126
  constructor(fabricModuleOrOptions = {}, options = {}) {
50
127
  var _a;
@@ -66,6 +143,24 @@ export class ImageEditor {
66
143
  writable: true,
67
144
  value: void 0
68
145
  });
146
+ Object.defineProperty(this, "currentLayoutMode", {
147
+ enumerable: true,
148
+ configurable: true,
149
+ writable: true,
150
+ value: 'expand'
151
+ });
152
+ Object.defineProperty(this, "defaultMosaicConfig", {
153
+ enumerable: true,
154
+ configurable: true,
155
+ writable: true,
156
+ value: void 0
157
+ });
158
+ Object.defineProperty(this, "currentMosaicConfig", {
159
+ enumerable: true,
160
+ configurable: true,
161
+ writable: true,
162
+ value: void 0
163
+ });
69
164
  Object.defineProperty(this, "canvas", {
70
165
  enumerable: true,
71
166
  configurable: true,
@@ -204,6 +299,12 @@ export class ImageEditor {
204
299
  writable: true,
205
300
  value: null
206
301
  });
302
+ Object.defineProperty(this, "mosaicSession", {
303
+ enumerable: true,
304
+ configurable: true,
305
+ writable: true,
306
+ value: null
307
+ });
207
308
  Object.defineProperty(this, "domBindings", {
208
309
  enumerable: true,
209
310
  configurable: true,
@@ -244,9 +345,15 @@ export class ImageEditor {
244
345
  this.fabricModule = (_a = detected.fabric) !== null && _a !== void 0 ? _a : {};
245
346
  this.isFabricLoaded = detected.isFabricLoaded;
246
347
  this.options = resolveOptions(detected.options);
247
- const layoutConflict = detectLayoutConflict(this.options);
248
- if (layoutConflict) {
249
- reportWarning(this.options, null, layoutConflict.message);
348
+ this.currentLayoutMode = this.options.layoutMode;
349
+ this.defaultMosaicConfig = this.options.defaultMosaicConfig;
350
+ this.currentMosaicConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
351
+ const rawDefaultLayoutMode = detected.options
352
+ .defaultLayoutMode;
353
+ if (rawDefaultLayoutMode !== undefined && !isLayoutMode(rawDefaultLayoutMode)) {
354
+ reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported defaultLayoutMode ` +
355
+ `${JSON.stringify(rawDefaultLayoutMode)}. ` +
356
+ 'Expected "fit", "cover", or "expand".'), 'Invalid defaultLayoutMode fell back to "expand".');
250
357
  }
251
358
  this.operationGuard = new OperationGuard();
252
359
  this.animQueue = new AnimationQueue();
@@ -288,10 +395,14 @@ export class ImageEditor {
288
395
  enterCropModeButton: 'enterCropModeButton',
289
396
  applyCropButton: 'applyCropButton',
290
397
  cancelCropButton: 'cancelCropButton',
398
+ enterMosaicModeButton: 'enterMosaicModeButton',
399
+ exitMosaicModeButton: 'exitMosaicModeButton',
400
+ mosaicBrushSizeInput: 'mosaicBrushSizeInput',
401
+ mosaicBlockSizeInput: 'mosaicBlockSizeInput',
291
402
  uploadArea: 'uploadArea',
292
403
  };
293
404
  this.elements = { ...defaults, ...idMap };
294
- this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed);
405
+ this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed, () => { var _a, _b; return (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document; });
295
406
  this.initCanvas();
296
407
  this.transformController = new TransformController(this.buildTransformContext());
297
408
  this.bindDomEvents();
@@ -442,6 +553,26 @@ export class ImageEditor {
442
553
  this.bindElementIfExists('cancelCropButton', 'click', () => {
443
554
  this.cancelCrop();
444
555
  });
556
+ this.bindElementIfExists('enterMosaicModeButton', 'click', () => {
557
+ this.enterMosaicMode();
558
+ });
559
+ this.bindElementIfExists('exitMosaicModeButton', 'click', () => {
560
+ this.exitMosaicMode();
561
+ });
562
+ const bindMosaicSizeInput = (key, applyValue) => {
563
+ const handler = (event) => {
564
+ const parsed = parseFloat(event.target.value);
565
+ applyValue(parsed);
566
+ };
567
+ this.bindElementIfExists(key, 'input', handler);
568
+ this.bindElementIfExists(key, 'change', handler);
569
+ };
570
+ bindMosaicSizeInput('mosaicBrushSizeInput', (value) => {
571
+ this.setMosaicBrushSize(value);
572
+ });
573
+ bindMosaicSizeInput('mosaicBlockSizeInput', (value) => {
574
+ this.setMosaicBlockSize(value);
575
+ });
445
576
  }
446
577
  bindElementIfExists(key, event, handler) {
447
578
  var _a;
@@ -477,6 +608,9 @@ export class ImageEditor {
477
608
  }
478
609
  }
479
610
  async loadImage(base64, options = {}) {
611
+ return this.loadImageInternal(base64, options);
612
+ }
613
+ async loadImageInternal(base64, options = {}) {
480
614
  if (!this.isFabricLoaded || !this.canvas)
481
615
  return;
482
616
  if (this.isDisposed)
@@ -496,7 +630,7 @@ export class ImageEditor {
496
630
  const loadImageContext = {
497
631
  fabric: this.fabricModule,
498
632
  canvas: this.canvas,
499
- options: this.options,
633
+ options: this.getRuntimeOptions(),
500
634
  containerElement: this.containerElement,
501
635
  placeholderElement: this.placeholderElement,
502
636
  viewportCache: this.viewportCache,
@@ -588,6 +722,11 @@ export class ImageEditor {
588
722
  !CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
589
723
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
590
724
  }
725
+ if (this.mosaicSession &&
726
+ !this.operationGuard.isOwnOperation(token) &&
727
+ !MOSAIC_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
728
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while mosaic mode is active.`);
729
+ }
591
730
  if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
592
731
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
593
732
  }
@@ -597,10 +736,17 @@ export class ImageEditor {
597
736
  this.assertIdleForOperation(operationName, options);
598
737
  return true;
599
738
  }
600
- catch {
739
+ catch (error) {
740
+ if (!this.isExpectedIdleGuardError(error, operationName)) {
741
+ throw error;
742
+ }
601
743
  return false;
602
744
  }
603
745
  }
746
+ isExpectedIdleGuardError(error, operationName) {
747
+ return (error instanceof Error &&
748
+ error.message.startsWith(`[ImageEditor] Cannot run "${operationName}" `));
749
+ }
604
750
  assertCanQueueAnimation(operationName, options) {
605
751
  this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
606
752
  }
@@ -612,17 +758,26 @@ export class ImageEditor {
612
758
  ((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
613
759
  }
614
760
  isBusy() {
615
- return this.operationGuard.isBusy() || this.animQueue.isBusy() || this.cropSession !== null;
761
+ return (this.operationGuard.isBusy() ||
762
+ this.animQueue.isBusy() ||
763
+ this.cropSession !== null ||
764
+ this.mosaicSession !== null);
616
765
  }
617
766
  setLayoutMode(mode) {
618
- if (mode !== 'fit' && mode !== 'cover' && mode !== 'expand') {
767
+ if (!isLayoutMode(mode)) {
619
768
  reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported layout mode ${JSON.stringify(mode)}. ` +
620
769
  'Expected "fit", "cover", or "expand".'), 'Ignored invalid layout mode.');
621
770
  return;
622
771
  }
623
- this.options.fitImageToCanvas = mode === 'fit';
624
- this.options.coverImageToCanvas = mode === 'cover';
625
- this.options.expandCanvasToImage = mode === 'expand';
772
+ this.currentLayoutMode = mode;
773
+ }
774
+ getRuntimeOptions() {
775
+ if (this.currentLayoutMode === this.options.layoutMode)
776
+ return this.options;
777
+ return Object.freeze({
778
+ ...this.options,
779
+ layoutMode: this.currentLayoutMode,
780
+ });
626
781
  }
627
782
  buildCallbackContext(operation, isInternalOperation = false) {
628
783
  return { operation, isInternalOperation };
@@ -631,7 +786,7 @@ export class ImageEditor {
631
786
  const internal = this.getInternalOperationToken(options);
632
787
  const activeOperation = this.operationGuard.activeOperationName();
633
788
  if (internal && activeOperation) {
634
- return this.buildCallbackContext(activeOperation, true);
789
+ return this.buildCallbackContext(isImageEditorOperation(activeOperation) ? activeOperation : fallback, true);
635
790
  }
636
791
  return this.buildCallbackContext(fallback, false);
637
792
  }
@@ -698,6 +853,7 @@ export class ImageEditor {
698
853
  currentRotation: this.currentRotation,
699
854
  isBusy: this.isBusy(),
700
855
  isCropMode: this.cropSession !== null,
856
+ isMosaicMode: this.mosaicSession !== null,
701
857
  canUndo: this.historyManager.canUndo(),
702
858
  canRedo: this.historyManager.canRedo(),
703
859
  canvasWidth,
@@ -785,7 +941,7 @@ export class ImageEditor {
785
941
  const boundingRect = this.originalImage.getBoundingRect();
786
942
  const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
787
943
  const viewport = this.measureLayoutViewport(scrollbarSize);
788
- if (this.options.fitImageToCanvas || this.options.coverImageToCanvas) {
944
+ if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
789
945
  const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
790
946
  this.setCanvasSizePx(canvasSize.width, canvasSize.height);
791
947
  return;
@@ -807,14 +963,14 @@ export class ImageEditor {
807
963
  const canvasH = Math.ceil(this.canvas.getHeight());
808
964
  const clipsImage = boundingRect.width > canvasW + LAYOUT_EPSILON ||
809
965
  boundingRect.height > canvasH + LAYOUT_EPSILON;
810
- if (this.options.fitImageToCanvas || this.options.coverImageToCanvas) {
966
+ if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
811
967
  const staleOverflowWidth = canvasW > viewport.width + LAYOUT_EPSILON &&
812
968
  boundingRect.width <= viewport.width + LAYOUT_EPSILON;
813
969
  const staleOverflowHeight = canvasH > viewport.height + LAYOUT_EPSILON &&
814
970
  boundingRect.height <= viewport.height + LAYOUT_EPSILON;
815
971
  return clipsImage || staleOverflowWidth || staleOverflowHeight;
816
972
  }
817
- if (this.options.expandCanvasToImage) {
973
+ if (this.currentLayoutMode === 'expand') {
818
974
  const expectedW = Math.max(viewport.width, Math.ceil(boundingRect.width));
819
975
  const expectedH = Math.max(viewport.height, Math.ceil(boundingRect.height));
820
976
  return (Math.abs(canvasW - expectedW) > LAYOUT_EPSILON ||
@@ -822,6 +978,33 @@ export class ImageEditor {
822
978
  }
823
979
  return clipsImage;
824
980
  }
981
+ settleFitCoverScrollbarsAfterStateRestore() {
982
+ if (!this.canvas ||
983
+ !this.containerElement ||
984
+ (this.currentLayoutMode !== 'fit' && this.currentLayoutMode !== 'cover')) {
985
+ return;
986
+ }
987
+ const canvasW = Math.ceil(this.canvas.getWidth());
988
+ const canvasH = Math.ceil(this.canvas.getHeight());
989
+ if (canvasW <= 1 || canvasH <= 1)
990
+ return;
991
+ const clientW = Math.floor(this.containerElement.clientWidth || 0);
992
+ const clientH = Math.floor(this.containerElement.clientHeight || 0);
993
+ if (clientW <= 0 || clientH <= 0)
994
+ return;
995
+ const scrollW = Math.ceil(this.containerElement.scrollWidth || 0);
996
+ const scrollH = Math.ceil(this.containerElement.scrollHeight || 0);
997
+ const hasHorizontalScrollbar = scrollW > clientW + LAYOUT_EPSILON;
998
+ const hasVerticalScrollbar = scrollH > clientH + LAYOUT_EPSILON;
999
+ if (!hasHorizontalScrollbar && !hasVerticalScrollbar)
1000
+ return;
1001
+ const nudgeWidth = hasVerticalScrollbar && Math.abs(canvasW - clientW) <= SCROLLBAR_SETTLE_EPSILON;
1002
+ const nudgeHeight = hasHorizontalScrollbar && Math.abs(canvasH - clientH) <= SCROLLBAR_SETTLE_EPSILON;
1003
+ if (!nudgeWidth && !nudgeHeight)
1004
+ return;
1005
+ this.setCanvasSizePx(nudgeWidth ? canvasW - 1 : canvasW, nudgeHeight ? canvasH - 1 : canvasH);
1006
+ this.setCanvasSizePx(canvasW, canvasH);
1007
+ }
825
1008
  captureImageDisplayGeometry() {
826
1009
  if (!this.canvas || !this.originalImage)
827
1010
  return null;
@@ -886,11 +1069,7 @@ export class ImageEditor {
886
1069
  afterTransformSnap: () => {
887
1070
  if (this.isDisposed || !this.canvas || !this.originalImage)
888
1071
  return;
889
- if (this.options.expandCanvasToImage ||
890
- this.options.coverImageToCanvas ||
891
- this.options.fitImageToCanvas) {
892
- this.updateCanvasSizeToImageBounds();
893
- }
1072
+ this.updateCanvasSizeToImageBounds();
894
1073
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
895
1074
  this.canvas
896
1075
  .getObjects()
@@ -1058,14 +1237,13 @@ export class ImageEditor {
1058
1237
  this.currentImageMimeType = null;
1059
1238
  }
1060
1239
  this.isImageLoadedToCanvas = !!this.originalImage;
1061
- if (this.originalImage &&
1062
- (this.options.expandCanvasToImage ||
1063
- this.options.coverImageToCanvas ||
1064
- this.options.fitImageToCanvas) &&
1065
- this.shouldNormalizeCanvasSizeAfterStateRestore()) {
1240
+ if (this.originalImage && this.shouldNormalizeCanvasSizeAfterStateRestore()) {
1066
1241
  this.updateCanvasSizeToImageBounds();
1067
1242
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
1068
1243
  }
1244
+ if (this.originalImage) {
1245
+ this.settleFitCoverScrollbarsAfterStateRestore();
1246
+ }
1069
1247
  const restoredMasks = restoredState.objects.filter(isMaskObject);
1070
1248
  this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
1071
1249
  restoredMasks.forEach((maskObject) => {
@@ -1125,16 +1303,12 @@ export class ImageEditor {
1125
1303
  if (after === before) {
1126
1304
  return;
1127
1305
  }
1128
- let executedOnce = false;
1129
1306
  const cmd = new Command(async () => {
1130
- if (executedOnce) {
1131
- await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
1132
- }
1133
- executedOnce = true;
1307
+ await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
1134
1308
  }, async () => {
1135
1309
  await this.loadFromStateInternal(before, this.withAnimationQueueBypass());
1136
1310
  });
1137
- this.historyManager.execute(cmd);
1311
+ this.historyManager.push(cmd);
1138
1312
  this.lastSnapshot = after;
1139
1313
  }
1140
1314
  catch (error) {
@@ -1249,7 +1423,7 @@ export class ImageEditor {
1249
1423
  return {
1250
1424
  fabric: this.fabricModule,
1251
1425
  canvas: this.canvas,
1252
- options: this.options,
1426
+ options: this.getRuntimeOptions(),
1253
1427
  getLastMask: () => this.lastMask,
1254
1428
  setLastMask: (maskObject) => {
1255
1429
  this.lastMask = maskObject;
@@ -1450,7 +1624,7 @@ export class ImageEditor {
1450
1624
  containerElement: this.containerElement,
1451
1625
  loadImage: async (base64, providedOptions) => {
1452
1626
  const geometry = this.captureImageDisplayGeometry();
1453
- await this.loadImage(base64, this.withInternalOperationOptions(operationToken, providedOptions));
1627
+ await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
1454
1628
  this.restoreMergedImageDisplayGeometry(geometry);
1455
1629
  },
1456
1630
  saveState: () => this.captureSnapshotInternal(),
@@ -1463,8 +1637,9 @@ export class ImageEditor {
1463
1637
  }
1464
1638
  captureSnapshotInternal() {
1465
1639
  var _a;
1466
- if (!this.canvas)
1467
- return '';
1640
+ if (!this.canvas) {
1641
+ throw new Error('[ImageEditor] Cannot capture canvas snapshot before init or after dispose.');
1642
+ }
1468
1643
  const activeMask = this.getActiveMaskForSnapshot();
1469
1644
  this.hideAllMaskLabels();
1470
1645
  return saveStateImpl({
@@ -1483,9 +1658,134 @@ export class ImageEditor {
1483
1658
  const activeObject = this.canvas.getActiveObject();
1484
1659
  if (activeObject && isMaskObject(activeObject))
1485
1660
  return activeObject;
1486
- return ((_a = this.canvas
1661
+ const labeledMasks = this.canvas
1487
1662
  .getObjects()
1488
- .find((object) => isMaskObject(object) && !!object.labelObject)) !== null && _a !== void 0 ? _a : null);
1663
+ .filter((object) => isMaskObject(object) && !!object.labelObject);
1664
+ return labeledMasks.length === 1 ? ((_a = labeledMasks[0]) !== null && _a !== void 0 ? _a : null) : null;
1665
+ }
1666
+ enterMosaicMode() {
1667
+ if (!this.canvas || !this.originalImage)
1668
+ return;
1669
+ if (this.mosaicSession)
1670
+ return;
1671
+ if (!this.isImageLoaded())
1672
+ return;
1673
+ if (!this.canRunIdleOperation('enterMosaicMode'))
1674
+ return;
1675
+ enterMosaicModeImpl(this.buildMosaicControllerContext());
1676
+ this.updateInputs();
1677
+ this.updateUi();
1678
+ const callbackContext = this.buildCallbackContext('enterMosaicMode', false);
1679
+ this.emitBusyChangeIfChanged(callbackContext);
1680
+ this.emitImageChanged(callbackContext);
1681
+ }
1682
+ exitMosaicMode() {
1683
+ if (!this.canvas || !this.mosaicSession)
1684
+ return;
1685
+ if (!this.canRunIdleOperation('exitMosaicMode'))
1686
+ return;
1687
+ exitMosaicModeImpl(this.buildMosaicControllerContext());
1688
+ this.updateInputs();
1689
+ this.updateUi();
1690
+ const callbackContext = this.buildCallbackContext('exitMosaicMode', false);
1691
+ this.emitBusyChangeIfChanged(callbackContext);
1692
+ this.emitImageChanged(callbackContext);
1693
+ }
1694
+ isMosaicMode() {
1695
+ return this.mosaicSession !== null;
1696
+ }
1697
+ getMosaicConfig() {
1698
+ return cloneResolvedMosaicConfig(this.currentMosaicConfig);
1699
+ }
1700
+ setMosaicConfig(config) {
1701
+ this.applyMosaicConfigPatch(config, 'setMosaicConfig');
1702
+ }
1703
+ resetMosaicConfig() {
1704
+ if (this.isDisposed)
1705
+ return;
1706
+ const nextConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
1707
+ if (areResolvedMosaicConfigsEqual(this.currentMosaicConfig, nextConfig))
1708
+ return;
1709
+ this.currentMosaicConfig = nextConfig;
1710
+ if (this.mosaicSession && this.canvas) {
1711
+ updateMosaicPreview(this.buildMosaicControllerContext());
1712
+ }
1713
+ this.updateInputs();
1714
+ this.updateUi();
1715
+ this.emitImageChanged(this.buildCallbackContext('resetMosaicConfig', false));
1716
+ }
1717
+ setMosaicBrushSize(size) {
1718
+ this.applyMosaicConfigPatch({ brushSize: size }, 'setMosaicBrushSize');
1719
+ }
1720
+ setMosaicBlockSize(size) {
1721
+ this.applyMosaicConfigPatch({ blockSize: size }, 'setMosaicBlockSize');
1722
+ }
1723
+ applyMosaicConfigPatch(config, operation) {
1724
+ if (this.isDisposed)
1725
+ return;
1726
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
1727
+ reportWarning(this.options, new TypeError('[ImageEditor] Invalid Mosaic config object.'), 'Ignored invalid Mosaic config.');
1728
+ return;
1729
+ }
1730
+ const invalidFields = getInvalidMosaicConfigFields(config);
1731
+ if (invalidFields.length > 0) {
1732
+ reportWarning(this.options, new TypeError(`[ImageEditor] Ignored invalid Mosaic config field(s): ` +
1733
+ `${invalidFields.join(', ')}.`), 'Ignored invalid Mosaic config fields.');
1734
+ }
1735
+ const nextConfig = mergeMosaicConfigPatch(this.currentMosaicConfig, config);
1736
+ if (areResolvedMosaicConfigsEqual(this.currentMosaicConfig, nextConfig))
1737
+ return;
1738
+ this.currentMosaicConfig = nextConfig;
1739
+ if (this.mosaicSession && this.canvas) {
1740
+ updateMosaicPreview(this.buildMosaicControllerContext());
1741
+ }
1742
+ this.updateInputs();
1743
+ this.updateUi();
1744
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
1745
+ }
1746
+ buildMosaicControllerContext() {
1747
+ return {
1748
+ fabric: this.fabricModule,
1749
+ canvas: this.canvas,
1750
+ options: this.options,
1751
+ historyManager: this.historyManager,
1752
+ getMosaicConfig: () => cloneResolvedMosaicConfig(this.currentMosaicConfig),
1753
+ isImageLoaded: () => this.isImageLoaded(),
1754
+ getOriginalImage: () => this.originalImage,
1755
+ setOriginalImage: (image) => {
1756
+ this.originalImage = image;
1757
+ },
1758
+ getCurrentImageMimeType: () => this.currentImageMimeType,
1759
+ setCurrentImageMimeType: (mimeType) => {
1760
+ this.currentImageMimeType = mimeType;
1761
+ },
1762
+ getLastSnapshot: () => this.lastSnapshot,
1763
+ setLastSnapshot: (snapshot) => {
1764
+ this.lastSnapshot = snapshot;
1765
+ },
1766
+ captureSnapshot: () => this.captureSnapshotInternal(),
1767
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withAnimationQueueBypass()),
1768
+ updateUi: () => {
1769
+ this.updateUi();
1770
+ },
1771
+ updateInputs: () => {
1772
+ this.updateInputs();
1773
+ },
1774
+ hideAllMaskLabels: () => {
1775
+ this.hideAllMaskLabels();
1776
+ },
1777
+ emitImageChanged: (context) => {
1778
+ this.emitImageChanged(context);
1779
+ },
1780
+ emitBusyChangeIfChanged: (context) => {
1781
+ this.emitBusyChangeIfChanged(context);
1782
+ },
1783
+ buildCallbackContext: (operation, isInternal) => this.buildCallbackContext(operation, isInternal),
1784
+ getMosaicSession: () => this.mosaicSession,
1785
+ setMosaicSession: (session) => {
1786
+ this.mosaicSession = session;
1787
+ },
1788
+ };
1489
1789
  }
1490
1790
  enterCropMode() {
1491
1791
  if (!this.canvas || !this.originalImage)
@@ -1558,7 +1858,7 @@ export class ImageEditor {
1558
1858
  },
1559
1859
  saveState: () => this.captureSnapshotInternal(),
1560
1860
  loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
1561
- loadImage: (base64, providedOptions) => this.loadImage(base64, this.withInternalOperationOptions(operationToken, providedOptions)),
1861
+ loadImage: (base64, providedOptions) => this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {})),
1562
1862
  getMaskCounter: () => this.maskCounter,
1563
1863
  setMaskCounter: (n) => {
1564
1864
  this.maskCounter = n;
@@ -1570,13 +1870,28 @@ export class ImageEditor {
1570
1870
  }
1571
1871
  updateInputs() {
1572
1872
  const scaleId = this.elements.scalePercentageInput;
1573
- if (!scaleId)
1574
- return;
1575
- const scaleInputElement = document.getElementById(scaleId);
1576
- if (scaleInputElement)
1577
- scaleInputElement.value = String(Math.round(this.currentScale * 100));
1873
+ if (scaleId) {
1874
+ const scaleInputElement = document.getElementById(scaleId);
1875
+ if (scaleInputElement) {
1876
+ scaleInputElement.value = String(Math.round(this.currentScale * 100));
1877
+ }
1878
+ }
1879
+ const mosaicConfig = this.getMosaicConfig();
1880
+ const mosaicBrushSizeInputId = this.elements.mosaicBrushSizeInput;
1881
+ if (mosaicBrushSizeInputId) {
1882
+ const brushInput = document.getElementById(mosaicBrushSizeInputId);
1883
+ if (brushInput)
1884
+ brushInput.value = String(mosaicConfig.brushSize);
1885
+ }
1886
+ const mosaicBlockSizeInputId = this.elements.mosaicBlockSizeInput;
1887
+ if (mosaicBlockSizeInputId) {
1888
+ const blockInput = document.getElementById(mosaicBlockSizeInputId);
1889
+ if (blockInput)
1890
+ blockInput.value = String(mosaicConfig.blockSize);
1891
+ }
1578
1892
  }
1579
1893
  updateUi() {
1894
+ var _a;
1580
1895
  if (!this.canvas)
1581
1896
  return;
1582
1897
  const hasImage = !!this.originalImage;
@@ -1588,13 +1903,22 @@ export class ImageEditor {
1588
1903
  const canUndo = this.historyManager.canUndo();
1589
1904
  const canRedo = this.historyManager.canRedo();
1590
1905
  const isInCropMode = this.cropSession !== null;
1906
+ const isInMosaicMode = this.mosaicSession !== null;
1591
1907
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
1908
+ const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
1592
1909
  if (isInCropMode) {
1593
1910
  CROP_MODE_CONTROL_KEYS.forEach((key) => {
1594
1911
  this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
1595
1912
  });
1596
1913
  return;
1597
1914
  }
1915
+ if (isInMosaicMode) {
1916
+ MOSAIC_MODE_CONTROL_KEYS.forEach((key) => {
1917
+ this.setControlEnabled(key, !isBusy && !isMosaicApplying && MOSAIC_MODE_ENABLED_KEYS.includes(key));
1918
+ });
1919
+ this.setControlEnabled('imageInput', false);
1920
+ return;
1921
+ }
1598
1922
  this.setControlEnabled('scalePercentageInput', hasImage && !isBusy);
1599
1923
  this.setControlEnabled('rotateLeftDegreesInput', hasImage && !isBusy);
1600
1924
  this.setControlEnabled('rotateRightDegreesInput', hasImage && !isBusy);
@@ -1611,6 +1935,10 @@ export class ImageEditor {
1611
1935
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
1612
1936
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
1613
1937
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
1938
+ this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
1939
+ this.setControlEnabled('exitMosaicModeButton', false);
1940
+ this.setControlEnabled('mosaicBrushSizeInput', !this.isDisposed);
1941
+ this.setControlEnabled('mosaicBlockSizeInput', !this.isDisposed);
1614
1942
  this.setControlEnabled('imageInput', !isBusy);
1615
1943
  this.setControlEnabled('applyCropButton', false);
1616
1944
  this.setControlEnabled('cancelCropButton', false);
@@ -1708,6 +2036,14 @@ export class ImageEditor {
1708
2036
  }
1709
2037
  this.cropSession = null;
1710
2038
  }
2039
+ if (this.mosaicSession && this.canvas) {
2040
+ try {
2041
+ exitMosaicModeImpl(this.buildMosaicControllerContext());
2042
+ }
2043
+ catch {
2044
+ }
2045
+ this.mosaicSession = null;
2046
+ }
1711
2047
  if (this.canvas) {
1712
2048
  try {
1713
2049
  void Promise.resolve(this.canvas.dispose()).catch(() => {