@bensitu/image-editor 2.1.0 → 2.3.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 (118) hide show
  1. package/README.md +305 -95
  2. package/dist/cjs/index.cjs +3460 -843
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/annotation/annotation-lock.js +7 -0
  5. package/dist/esm/annotation/annotation-lock.js.map +1 -0
  6. package/dist/esm/annotation/annotation-manager.js +217 -0
  7. package/dist/esm/annotation/annotation-manager.js.map +1 -0
  8. package/dist/esm/annotation/annotation-style.js +50 -0
  9. package/dist/esm/annotation/annotation-style.js.map +1 -0
  10. package/dist/esm/annotation/draw-controller.js +114 -0
  11. package/dist/esm/annotation/draw-controller.js.map +1 -0
  12. package/dist/esm/annotation/text-controller.js +234 -0
  13. package/dist/esm/annotation/text-controller.js.map +1 -0
  14. package/dist/esm/core/default-options.js +240 -9
  15. package/dist/esm/core/default-options.js.map +1 -1
  16. package/dist/esm/core/editor-object-kind.js +37 -0
  17. package/dist/esm/core/editor-object-kind.js.map +1 -0
  18. package/dist/esm/core/errors.js +19 -0
  19. package/dist/esm/core/errors.js.map +1 -1
  20. package/dist/esm/core/layer-order.js +100 -0
  21. package/dist/esm/core/layer-order.js.map +1 -0
  22. package/dist/esm/core/public-types.js +34 -1
  23. package/dist/esm/core/public-types.js.map +1 -1
  24. package/dist/esm/core/state-serializer.js +112 -24
  25. package/dist/esm/core/state-serializer.js.map +1 -1
  26. package/dist/esm/crop/crop-controller.js +220 -10
  27. package/dist/esm/crop/crop-controller.js.map +1 -1
  28. package/dist/esm/export/export-format.js.map +1 -1
  29. package/dist/esm/export/export-service.js +157 -168
  30. package/dist/esm/export/export-service.js.map +1 -1
  31. package/dist/esm/export/overlay-merge-service.js +75 -0
  32. package/dist/esm/export/overlay-merge-service.js.map +1 -0
  33. package/dist/esm/history/history-manager.js +2 -2
  34. package/dist/esm/history/history-manager.js.map +1 -1
  35. package/dist/esm/image/image-loader.js +20 -51
  36. package/dist/esm/image/image-loader.js.map +1 -1
  37. package/dist/esm/image/transform-controller.js +42 -0
  38. package/dist/esm/image/transform-controller.js.map +1 -1
  39. package/dist/esm/image-editor.js +1200 -72
  40. package/dist/esm/image-editor.js.map +1 -1
  41. package/dist/esm/index.js +1 -1
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/mask/mask-factory.js +39 -14
  44. package/dist/esm/mask/mask-factory.js.map +1 -1
  45. package/dist/esm/mask/mask-label-manager.js +2 -0
  46. package/dist/esm/mask/mask-label-manager.js.map +1 -1
  47. package/dist/esm/mask/mask-list.js.map +1 -1
  48. package/dist/esm/mask/mask-style.js.map +1 -1
  49. package/dist/esm/mosaic/mosaic-controller.js +24 -28
  50. package/dist/esm/mosaic/mosaic-controller.js.map +1 -1
  51. package/dist/esm/utils/file.js +10 -0
  52. package/dist/esm/utils/file.js.map +1 -1
  53. package/dist/esm/utils/image-element-loader.js +55 -0
  54. package/dist/esm/utils/image-element-loader.js.map +1 -0
  55. package/dist/esm/utils/pointer.js +28 -0
  56. package/dist/esm/utils/pointer.js.map +1 -0
  57. package/dist/types/annotation/annotation-lock.d.ts +12 -0
  58. package/dist/types/annotation/annotation-lock.d.ts.map +1 -0
  59. package/dist/types/annotation/annotation-manager.d.ts +33 -0
  60. package/dist/types/annotation/annotation-manager.d.ts.map +1 -0
  61. package/dist/types/annotation/annotation-style.d.ts +13 -0
  62. package/dist/types/annotation/annotation-style.d.ts.map +1 -0
  63. package/dist/types/annotation/draw-controller.d.ts +43 -0
  64. package/dist/types/annotation/draw-controller.d.ts.map +1 -0
  65. package/dist/types/annotation/text-controller.d.ts +47 -0
  66. package/dist/types/annotation/text-controller.d.ts.map +1 -0
  67. package/dist/types/core/default-options.d.ts +14 -2
  68. package/dist/types/core/default-options.d.ts.map +1 -1
  69. package/dist/types/core/editor-object-kind.d.ts +29 -0
  70. package/dist/types/core/editor-object-kind.d.ts.map +1 -0
  71. package/dist/types/core/errors.d.ts +11 -1
  72. package/dist/types/core/errors.d.ts.map +1 -1
  73. package/dist/types/core/layer-order.d.ts +21 -0
  74. package/dist/types/core/layer-order.d.ts.map +1 -0
  75. package/dist/types/core/public-types.d.ts +272 -56
  76. package/dist/types/core/public-types.d.ts.map +1 -1
  77. package/dist/types/core/state-serializer.d.ts +34 -5
  78. package/dist/types/core/state-serializer.d.ts.map +1 -1
  79. package/dist/types/crop/crop-controller.d.ts +18 -14
  80. package/dist/types/crop/crop-controller.d.ts.map +1 -1
  81. package/dist/types/export/export-format.d.ts +9 -40
  82. package/dist/types/export/export-format.d.ts.map +1 -1
  83. package/dist/types/export/export-service.d.ts +45 -41
  84. package/dist/types/export/export-service.d.ts.map +1 -1
  85. package/dist/types/export/overlay-merge-service.d.ts +38 -0
  86. package/dist/types/export/overlay-merge-service.d.ts.map +1 -0
  87. package/dist/types/history/history-manager.d.ts +11 -14
  88. package/dist/types/history/history-manager.d.ts.map +1 -1
  89. package/dist/types/image/image-loader.d.ts +27 -22
  90. package/dist/types/image/image-loader.d.ts.map +1 -1
  91. package/dist/types/image/image-resampler.d.ts +1 -1
  92. package/dist/types/image/transform-controller.d.ts +19 -14
  93. package/dist/types/image/transform-controller.d.ts.map +1 -1
  94. package/dist/types/image-editor.d.ts +93 -15
  95. package/dist/types/image-editor.d.ts.map +1 -1
  96. package/dist/types/index.d.cts +3 -3
  97. package/dist/types/index.d.cts.map +1 -1
  98. package/dist/types/index.d.ts +3 -3
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/mask/mask-factory.d.ts.map +1 -1
  101. package/dist/types/mask/mask-label-manager.d.ts +10 -9
  102. package/dist/types/mask/mask-label-manager.d.ts.map +1 -1
  103. package/dist/types/mask/mask-list.d.ts +11 -12
  104. package/dist/types/mask/mask-list.d.ts.map +1 -1
  105. package/dist/types/mask/mask-style.d.ts +19 -20
  106. package/dist/types/mask/mask-style.d.ts.map +1 -1
  107. package/dist/types/mosaic/mosaic-controller.d.ts +3 -3
  108. package/dist/types/mosaic/mosaic-controller.d.ts.map +1 -1
  109. package/dist/types/ui/visibility-state.d.ts +2 -2
  110. package/dist/types/utils/file.d.ts +13 -0
  111. package/dist/types/utils/file.d.ts.map +1 -1
  112. package/dist/types/utils/image-element-loader.d.ts +19 -0
  113. package/dist/types/utils/image-element-loader.d.ts.map +1 -0
  114. package/dist/types/utils/pointer.d.ts +16 -0
  115. package/dist/types/utils/pointer.d.ts.map +1 -0
  116. package/dist/umd/image-editor.umd.js +1 -1
  117. package/dist/umd/image-editor.umd.js.map +1 -1
  118. package/package.json +1 -1
@@ -1,14 +1,20 @@
1
1
  import { AnimationQueue } from './animation/animation-queue.js';
2
2
  import { reportError, reportWarning } from './core/callback-reporter.js';
3
- import { areResolvedMosaicConfigsEqual, cloneResolvedMosaicConfig, getInvalidMosaicConfigFields, isLayoutMode, mergeMosaicConfigPatch, resolveOptions, } from './core/default-options.js';
3
+ import { areResolvedMosaicConfigsEqual, areResolvedDrawConfigsEqual, areResolvedTextAnnotationConfigsEqual, cloneResolvedMosaicConfig, cloneResolvedDrawConfig, cloneResolvedTextAnnotationConfig, getInvalidDrawConfigFields, getInvalidMosaicConfigFields, getInvalidTextAnnotationConfigFields, isLayoutMode, mergeDrawConfigPatch, mergeMosaicConfigPatch, mergeTextAnnotationConfigPatch, 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
- import { isMaskObject } from './core/public-types.js';
9
- import { applyCrop as applyCropImpl, cancelCrop as cancelCropImpl, enterCropMode as enterCropModeImpl, } from './crop/crop-controller.js';
8
+ import { isAnnotationObject, isDrawAnnotationObject, isEditableOverlayObject, isMaskObject, isTextAnnotationObject, } from './core/public-types.js';
9
+ import { getAnnotations as getAnnotationsImpl, removeAllAnnotations as removeAllAnnotationsImpl, removeAnnotationObjects, removeSelectedAnnotation as removeSelectedAnnotationImpl, renderAnnotationList, updateAnnotation as updateAnnotationImpl, updateAnnotationListSelection, updateSelectedAnnotation as updateSelectedAnnotationImpl, } from './annotation/annotation-manager.js';
10
+ import { attachTextEditingHandlersToAnnotations, createTextAnnotation as createTextAnnotationImpl, enterTextMode as enterTextModeImpl, exitTextMode as exitTextModeImpl, finalizeActiveTextEditing, } from './annotation/text-controller.js';
11
+ import { enterDrawMode as enterDrawModeImpl, exitDrawMode as exitDrawModeImpl, updateDrawBrush, } from './annotation/draw-controller.js';
12
+ import { isAnnotationLocked, isAnnotationUnlocked } from './annotation/annotation-lock.js';
13
+ import { syncAnnotationRuntimeStates } from './annotation/annotation-style.js';
14
+ import { normalizeLayerOrder, getEditableOverlayRange } from './core/layer-order.js';
15
+ import { applyCrop as applyCropImpl, cancelCrop as cancelCropImpl, enterCropMode as enterCropModeImpl, setCropAspectRatio as setCropAspectRatioImpl, } from './crop/crop-controller.js';
10
16
  import { enterMosaicMode as enterMosaicModeImpl, exitMosaicMode as exitMosaicModeImpl, updateMosaicPreview, } from './mosaic/mosaic-controller.js';
11
- import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
17
+ import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeAnnotations as mergeAnnotationsImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
12
18
  import { loadImage as loadImageImpl } from './image/image-loader.js';
13
19
  import { ViewportCache, applyCanvasDimensions, computeScrollableCanvasSize, measureScrollbarSize, } from './image/layout-manager.js';
14
20
  import { TransformController } from './image/transform-controller.js';
@@ -18,7 +24,7 @@ import { renderMaskList, updateMaskListSelection } from './mask/mask-list.js';
18
24
  import { applyMaskSelectedStyle, applyMaskUnselectedStyle, reattachMaskHoverHandlers, } from './mask/mask-style.js';
19
25
  import { DomBindings } from './ui/dom-bindings.js';
20
26
  import { setPlaceholderVisible as setPlaceholderVisibleImpl } from './ui/visibility-state.js';
21
- import { inferImageMimeType, readFileAsDataUrl, resetFileInput } from './utils/file.js';
27
+ import { inferImageMimeType, isSupportedImageDataUrl, readFileAsDataUrl, resetFileInput, } from './utils/file.js';
22
28
  import { detectSourceMimeType } from './image/image-resampler.js';
23
29
  const LAYOUT_EPSILON = 0.5;
24
30
  const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
@@ -29,10 +35,28 @@ const CROP_MODE_CONTROL_KEYS = [
29
35
  'rotateRightDegreesInput',
30
36
  'rotateLeftButton',
31
37
  'rotateRightButton',
38
+ 'flipHorizontalButton',
39
+ 'flipVerticalButton',
32
40
  'createMaskButton',
33
41
  'removeSelectedMaskButton',
34
42
  'removeAllMasksButton',
35
43
  'mergeMasksButton',
44
+ 'mergeAnnotationsButton',
45
+ 'enterTextModeButton',
46
+ 'exitTextModeButton',
47
+ 'textColorInput',
48
+ 'textFontSizeInput',
49
+ 'enterDrawModeButton',
50
+ 'exitDrawModeButton',
51
+ 'drawColorInput',
52
+ 'drawBrushSizeInput',
53
+ 'removeSelectedAnnotationButton',
54
+ 'removeAllAnnotationsButton',
55
+ 'deleteSelectedObjectButton',
56
+ 'bringSelectedObjectForwardButton',
57
+ 'sendSelectedObjectBackwardButton',
58
+ 'bringSelectedObjectToFrontButton',
59
+ 'sendSelectedObjectToBackButton',
36
60
  'downloadImageButton',
37
61
  'zoomInButton',
38
62
  'zoomOutButton',
@@ -41,6 +65,7 @@ const CROP_MODE_CONTROL_KEYS = [
41
65
  'redoButton',
42
66
  'imageInput',
43
67
  'enterCropModeButton',
68
+ 'cropAspectRatioSelect',
44
69
  'applyCropButton',
45
70
  'cancelCropButton',
46
71
  'enterMosaicModeButton',
@@ -48,18 +73,50 @@ const CROP_MODE_CONTROL_KEYS = [
48
73
  'mosaicBrushSizeInput',
49
74
  'mosaicBlockSizeInput',
50
75
  ];
51
- const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
52
- const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
76
+ const CROP_MODE_ENABLED_KEYS = [
77
+ 'cropAspectRatioSelect',
78
+ 'applyCropButton',
79
+ 'cancelCropButton',
80
+ ];
81
+ const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['setCropAspectRatio', 'applyCrop', 'cancelCrop']);
82
+ const TEXT_MODE_ENABLED_KEYS = [
83
+ 'exitTextModeButton',
84
+ 'textColorInput',
85
+ 'textFontSizeInput',
86
+ ];
87
+ const DRAW_MODE_ENABLED_KEYS = [
88
+ 'exitDrawModeButton',
89
+ 'drawColorInput',
90
+ 'drawBrushSizeInput',
91
+ ];
53
92
  const MOSAIC_MODE_CONTROL_KEYS = [
54
93
  'scalePercentageInput',
55
94
  'rotateLeftDegreesInput',
56
95
  'rotateRightDegreesInput',
57
96
  'rotateLeftButton',
58
97
  'rotateRightButton',
98
+ 'flipHorizontalButton',
99
+ 'flipVerticalButton',
59
100
  'createMaskButton',
60
101
  'removeSelectedMaskButton',
61
102
  'removeAllMasksButton',
62
103
  'mergeMasksButton',
104
+ 'mergeAnnotationsButton',
105
+ 'enterTextModeButton',
106
+ 'exitTextModeButton',
107
+ 'textColorInput',
108
+ 'textFontSizeInput',
109
+ 'enterDrawModeButton',
110
+ 'exitDrawModeButton',
111
+ 'drawColorInput',
112
+ 'drawBrushSizeInput',
113
+ 'removeSelectedAnnotationButton',
114
+ 'removeAllAnnotationsButton',
115
+ 'deleteSelectedObjectButton',
116
+ 'bringSelectedObjectForwardButton',
117
+ 'sendSelectedObjectBackwardButton',
118
+ 'bringSelectedObjectToFrontButton',
119
+ 'sendSelectedObjectToBackButton',
63
120
  'downloadImageButton',
64
121
  'zoomInButton',
65
122
  'zoomOutButton',
@@ -68,6 +125,7 @@ const MOSAIC_MODE_CONTROL_KEYS = [
68
125
  'redoButton',
69
126
  'imageInput',
70
127
  'enterCropModeButton',
128
+ 'cropAspectRatioSelect',
71
129
  'applyCropButton',
72
130
  'cancelCropButton',
73
131
  'enterMosaicModeButton',
@@ -97,12 +155,38 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
97
155
  'saveState',
98
156
  'scaleImage',
99
157
  'rotateImage',
158
+ 'flipHorizontal',
159
+ 'flipVertical',
100
160
  'resetImageTransform',
101
161
  'createMask',
102
162
  'removeSelectedMask',
103
163
  'removeAllMasks',
104
164
  'mergeMasks',
165
+ 'createTextAnnotation',
166
+ 'enterTextMode',
167
+ 'exitTextMode',
168
+ 'setTextConfig',
169
+ 'resetTextConfig',
170
+ 'setTextColor',
171
+ 'setTextFontSize',
172
+ 'enterDrawMode',
173
+ 'exitDrawMode',
174
+ 'setDrawConfig',
175
+ 'resetDrawConfig',
176
+ 'setDrawColor',
177
+ 'setDrawBrushSize',
178
+ 'updateSelectedAnnotation',
179
+ 'updateAnnotation',
180
+ 'removeSelectedAnnotation',
181
+ 'removeAllAnnotations',
182
+ 'deleteSelectedObject',
183
+ 'mergeAnnotations',
184
+ 'bringSelectedObjectForward',
185
+ 'sendSelectedObjectBackward',
186
+ 'bringSelectedObjectToFront',
187
+ 'sendSelectedObjectToBack',
105
188
  'enterCropMode',
189
+ 'setCropAspectRatio',
106
190
  'applyCrop',
107
191
  'cancelCrop',
108
192
  'enterMosaicMode',
@@ -119,6 +203,27 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
119
203
  'downloadImage',
120
204
  'dispose',
121
205
  ]);
206
+ const TOOL_MODE_ALLOWED_OPERATIONS = {
207
+ crop: CROP_SESSION_ALLOWED_OPERATIONS,
208
+ mosaic: MOSAIC_SESSION_ALLOWED_OPERATIONS,
209
+ text: new Set([
210
+ 'exitTextMode',
211
+ 'createTextAnnotation',
212
+ 'setTextConfig',
213
+ 'resetTextConfig',
214
+ 'setTextColor',
215
+ 'setTextFontSize',
216
+ 'saveState',
217
+ ]),
218
+ draw: new Set([
219
+ 'exitDrawMode',
220
+ 'setDrawConfig',
221
+ 'resetDrawConfig',
222
+ 'setDrawColor',
223
+ 'setDrawBrushSize',
224
+ 'saveState',
225
+ ]),
226
+ };
122
227
  function isImageEditorOperation(value) {
123
228
  return value !== null && IMAGE_EDITOR_OPERATIONS.has(value);
124
229
  }
@@ -161,6 +266,30 @@ export class ImageEditor {
161
266
  writable: true,
162
267
  value: void 0
163
268
  });
269
+ Object.defineProperty(this, "defaultTextConfig", {
270
+ enumerable: true,
271
+ configurable: true,
272
+ writable: true,
273
+ value: void 0
274
+ });
275
+ Object.defineProperty(this, "currentTextConfig", {
276
+ enumerable: true,
277
+ configurable: true,
278
+ writable: true,
279
+ value: void 0
280
+ });
281
+ Object.defineProperty(this, "defaultDrawConfig", {
282
+ enumerable: true,
283
+ configurable: true,
284
+ writable: true,
285
+ value: void 0
286
+ });
287
+ Object.defineProperty(this, "currentDrawConfig", {
288
+ enumerable: true,
289
+ configurable: true,
290
+ writable: true,
291
+ value: void 0
292
+ });
164
293
  Object.defineProperty(this, "canvas", {
165
294
  enumerable: true,
166
295
  configurable: true,
@@ -257,6 +386,12 @@ export class ImageEditor {
257
386
  writable: true,
258
387
  value: null
259
388
  });
389
+ Object.defineProperty(this, "annotationCounter", {
390
+ enumerable: true,
391
+ configurable: true,
392
+ writable: true,
393
+ value: 0
394
+ });
260
395
  Object.defineProperty(this, "lastSnapshot", {
261
396
  enumerable: true,
262
397
  configurable: true,
@@ -305,12 +440,36 @@ export class ImageEditor {
305
440
  writable: true,
306
441
  value: null
307
442
  });
443
+ Object.defineProperty(this, "textSession", {
444
+ enumerable: true,
445
+ configurable: true,
446
+ writable: true,
447
+ value: null
448
+ });
449
+ Object.defineProperty(this, "drawSession", {
450
+ enumerable: true,
451
+ configurable: true,
452
+ writable: true,
453
+ value: null
454
+ });
308
455
  Object.defineProperty(this, "domBindings", {
309
456
  enumerable: true,
310
457
  configurable: true,
311
458
  writable: true,
312
459
  value: null
313
460
  });
461
+ Object.defineProperty(this, "keyboardDocument", {
462
+ enumerable: true,
463
+ configurable: true,
464
+ writable: true,
465
+ value: null
466
+ });
467
+ Object.defineProperty(this, "keyboardHandler", {
468
+ enumerable: true,
469
+ configurable: true,
470
+ writable: true,
471
+ value: null
472
+ });
314
473
  Object.defineProperty(this, "isDisposed", {
315
474
  enumerable: true,
316
475
  configurable: true,
@@ -348,6 +507,10 @@ export class ImageEditor {
348
507
  this.currentLayoutMode = this.options.layoutMode;
349
508
  this.defaultMosaicConfig = this.options.defaultMosaicConfig;
350
509
  this.currentMosaicConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
510
+ this.defaultTextConfig = this.options.defaultTextConfig;
511
+ this.currentTextConfig = cloneResolvedTextAnnotationConfig(this.defaultTextConfig);
512
+ this.defaultDrawConfig = this.options.defaultDrawConfig;
513
+ this.currentDrawConfig = cloneResolvedDrawConfig(this.defaultDrawConfig);
351
514
  const rawDefaultLayoutMode = detected.options
352
515
  .defaultLayoutMode;
353
516
  if (rawDefaultLayoutMode !== undefined && !isLayoutMode(rawDefaultLayoutMode)) {
@@ -380,10 +543,29 @@ export class ImageEditor {
380
543
  rotateRightDegreesInput: 'rotateRightDegreesInput',
381
544
  rotateLeftButton: 'rotateLeftButton',
382
545
  rotateRightButton: 'rotateRightButton',
546
+ flipHorizontalButton: 'flipHorizontalButton',
547
+ flipVerticalButton: 'flipVerticalButton',
383
548
  createMaskButton: 'createMaskButton',
384
549
  removeSelectedMaskButton: 'removeSelectedMaskButton',
385
550
  removeAllMasksButton: 'removeAllMasksButton',
386
551
  mergeMasksButton: 'mergeMasksButton',
552
+ annotationList: 'annotationList',
553
+ enterTextModeButton: 'enterTextModeButton',
554
+ exitTextModeButton: 'exitTextModeButton',
555
+ textColorInput: 'textColorInput',
556
+ textFontSizeInput: 'textFontSizeInput',
557
+ enterDrawModeButton: 'enterDrawModeButton',
558
+ exitDrawModeButton: 'exitDrawModeButton',
559
+ drawColorInput: 'drawColorInput',
560
+ drawBrushSizeInput: 'drawBrushSizeInput',
561
+ removeSelectedAnnotationButton: 'removeSelectedAnnotationButton',
562
+ removeAllAnnotationsButton: 'removeAllAnnotationsButton',
563
+ deleteSelectedObjectButton: 'deleteSelectedObjectButton',
564
+ mergeAnnotationsButton: 'mergeAnnotationsButton',
565
+ bringSelectedObjectForwardButton: 'bringSelectedObjectForwardButton',
566
+ sendSelectedObjectBackwardButton: 'sendSelectedObjectBackwardButton',
567
+ bringSelectedObjectToFrontButton: 'bringSelectedObjectToFrontButton',
568
+ sendSelectedObjectToBackButton: 'sendSelectedObjectToBackButton',
387
569
  downloadImageButton: 'downloadImageButton',
388
570
  maskList: 'maskList',
389
571
  zoomInButton: 'zoomInButton',
@@ -393,6 +575,7 @@ export class ImageEditor {
393
575
  redoButton: 'redoButton',
394
576
  imageInput: 'imageInput',
395
577
  enterCropModeButton: 'enterCropModeButton',
578
+ cropAspectRatioSelect: 'cropAspectRatioSelect',
396
579
  applyCropButton: 'applyCropButton',
397
580
  cancelCropButton: 'cancelCropButton',
398
581
  enterMosaicModeButton: 'enterMosaicModeButton',
@@ -402,12 +585,13 @@ export class ImageEditor {
402
585
  uploadArea: 'uploadArea',
403
586
  };
404
587
  this.elements = { ...defaults, ...idMap };
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; });
406
588
  this.initCanvas();
589
+ 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; });
407
590
  this.transformController = new TransformController(this.buildTransformContext());
408
591
  this.bindDomEvents();
409
592
  this.updateInputs();
410
593
  this.updateMaskList();
594
+ this.updateAnnotationList();
411
595
  this.updateUi();
412
596
  if (this.options.initialImageBase64) {
413
597
  void this.loadImage(this.options.initialImageBase64).catch(() => {
@@ -459,20 +643,24 @@ export class ImageEditor {
459
643
  });
460
644
  this.canvas.on('selection:cleared', () => this.handleSelectionChanged([]));
461
645
  const onObjectEvent = (e) => {
462
- if (e.target && isMaskObject(e.target))
463
- this.syncMaskLabel(e.target);
646
+ if (e.target)
647
+ this.handleObjectMovingScalingRotating(e.target);
464
648
  };
465
649
  const onObjectModified = (e) => {
466
- if (!e.target || !isMaskObject(e.target))
467
- return;
468
- this.syncMaskLabel(e.target);
469
- this.saveState();
650
+ if (e.target)
651
+ this.handleObjectModified(e.target);
470
652
  };
471
653
  this.canvas.on('object:moving', onObjectEvent);
472
654
  this.canvas.on('object:scaling', onObjectEvent);
473
655
  this.canvas.on('object:rotating', onObjectEvent);
474
656
  this.canvas.on('object:modified', onObjectModified);
475
657
  }
658
+ getLiveCanvasOrThrow(operationName) {
659
+ if (this.isDisposed || !this.canvas) {
660
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" after dispose.`);
661
+ }
662
+ return this.canvas;
663
+ }
476
664
  bindDomEvents() {
477
665
  this.bindElementIfExists('uploadArea', 'click', () => {
478
666
  var _a;
@@ -495,6 +683,12 @@ export class ImageEditor {
495
683
  this.bindElementIfExists('resetImageTransformButton', 'click', () => {
496
684
  void this.resetImageTransform();
497
685
  });
686
+ this.bindElementIfExists('flipHorizontalButton', 'click', () => {
687
+ void this.flipHorizontal();
688
+ });
689
+ this.bindElementIfExists('flipVerticalButton', 'click', () => {
690
+ void this.flipVertical();
691
+ });
498
692
  this.bindElementIfExists('createMaskButton', 'click', () => {
499
693
  this.createMask();
500
694
  });
@@ -507,6 +701,42 @@ export class ImageEditor {
507
701
  this.bindElementIfExists('mergeMasksButton', 'click', () => {
508
702
  void this.mergeMasks();
509
703
  });
704
+ this.bindElementIfExists('mergeAnnotationsButton', 'click', () => {
705
+ void this.mergeAnnotations();
706
+ });
707
+ this.bindElementIfExists('enterTextModeButton', 'click', () => {
708
+ this.enterTextMode();
709
+ });
710
+ this.bindElementIfExists('exitTextModeButton', 'click', () => {
711
+ this.exitTextMode();
712
+ });
713
+ this.bindElementIfExists('enterDrawModeButton', 'click', () => {
714
+ this.enterDrawMode();
715
+ });
716
+ this.bindElementIfExists('exitDrawModeButton', 'click', () => {
717
+ this.exitDrawMode();
718
+ });
719
+ this.bindElementIfExists('removeSelectedAnnotationButton', 'click', () => {
720
+ this.removeSelectedAnnotation();
721
+ });
722
+ this.bindElementIfExists('removeAllAnnotationsButton', 'click', () => {
723
+ this.removeAllAnnotations();
724
+ });
725
+ this.bindElementIfExists('deleteSelectedObjectButton', 'click', () => {
726
+ this.deleteSelectedObject();
727
+ });
728
+ this.bindElementIfExists('bringSelectedObjectForwardButton', 'click', () => {
729
+ this.bringSelectedObjectForward();
730
+ });
731
+ this.bindElementIfExists('sendSelectedObjectBackwardButton', 'click', () => {
732
+ this.sendSelectedObjectBackward();
733
+ });
734
+ this.bindElementIfExists('bringSelectedObjectToFrontButton', 'click', () => {
735
+ this.bringSelectedObjectToFront();
736
+ });
737
+ this.bindElementIfExists('sendSelectedObjectToBackButton', 'click', () => {
738
+ this.sendSelectedObjectToBack();
739
+ });
510
740
  this.bindElementIfExists('downloadImageButton', 'click', () => {
511
741
  this.downloadImage();
512
742
  });
@@ -543,7 +773,11 @@ export class ImageEditor {
543
773
  void this.rotateImage(this.currentRotation + step);
544
774
  });
545
775
  this.bindElementIfExists('enterCropModeButton', 'click', () => {
546
- this.enterCropMode();
776
+ this.enterCropMode({ aspectRatio: this.getSelectedCropAspectRatio() });
777
+ });
778
+ this.bindElementIfExists('cropAspectRatioSelect', 'change', () => {
779
+ if (this.cropSession)
780
+ this.setCropAspectRatio(this.getSelectedCropAspectRatio());
547
781
  });
548
782
  this.bindElementIfExists('applyCropButton', 'click', () => {
549
783
  void this.applyCrop().catch((error) => {
@@ -573,11 +807,92 @@ export class ImageEditor {
573
807
  bindMosaicSizeInput('mosaicBlockSizeInput', (value) => {
574
808
  this.setMosaicBlockSize(value);
575
809
  });
810
+ const bindStringInput = (key, applyValue) => {
811
+ const handler = (event) => {
812
+ applyValue(event.target.value);
813
+ };
814
+ this.bindElementIfExists(key, 'input', handler);
815
+ this.bindElementIfExists(key, 'change', handler);
816
+ };
817
+ const bindNumberInput = (key, applyValue) => {
818
+ const handler = (event) => {
819
+ applyValue(parseFloat(event.target.value));
820
+ };
821
+ this.bindElementIfExists(key, 'input', handler);
822
+ this.bindElementIfExists(key, 'change', handler);
823
+ };
824
+ bindStringInput('textColorInput', (value) => this.applyTextColorInput(value));
825
+ bindNumberInput('textFontSizeInput', (value) => this.applyTextFontSizeInput(value));
826
+ bindStringInput('drawColorInput', (value) => this.applyDrawColorInput(value));
827
+ bindNumberInput('drawBrushSizeInput', (value) => this.applyDrawBrushSizeInput(value));
828
+ this.bindKeyboardEvents();
576
829
  }
577
830
  bindElementIfExists(key, event, handler) {
578
831
  var _a;
579
832
  (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.bindIfExists(key, event, handler);
580
833
  }
834
+ bindKeyboardEvents() {
835
+ var _a, _b;
836
+ const ownerDocument = (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document;
837
+ if (this.keyboardHandler && this.keyboardDocument) {
838
+ this.keyboardDocument.removeEventListener('keydown', this.keyboardHandler);
839
+ }
840
+ this.keyboardDocument = ownerDocument;
841
+ this.keyboardHandler = (event) => this.handleKeyboardEvent(event);
842
+ ownerDocument.addEventListener('keydown', this.keyboardHandler);
843
+ }
844
+ isNativeTextInputActive() {
845
+ var _a;
846
+ const activeElement = (_a = this.keyboardDocument) === null || _a === void 0 ? void 0 : _a.activeElement;
847
+ if (!activeElement)
848
+ return false;
849
+ const tagName = activeElement.tagName.toLowerCase();
850
+ return (tagName === 'input' ||
851
+ tagName === 'textarea' ||
852
+ tagName === 'select' ||
853
+ activeElement.isContentEditable === true);
854
+ }
855
+ isFabricTextEditingActive() {
856
+ var _a;
857
+ const activeObject = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
858
+ return !!(activeObject &&
859
+ isTextAnnotationObject(activeObject) &&
860
+ activeObject.isEditing === true);
861
+ }
862
+ handleKeyboardEvent(event) {
863
+ if (this.isDisposed)
864
+ return;
865
+ if (event.key === 'Delete' || event.key === 'Backspace') {
866
+ if (this.isNativeTextInputActive() || this.isFabricTextEditingActive())
867
+ return;
868
+ this.deleteSelectedObject();
869
+ return;
870
+ }
871
+ if (event.key !== 'Escape')
872
+ return;
873
+ if (this.isFabricTextEditingActive() && this.canvas) {
874
+ finalizeActiveTextEditing(this.buildTextControllerContext(), { commit: false });
875
+ event.preventDefault();
876
+ return;
877
+ }
878
+ if (this.textSession) {
879
+ this.exitTextMode();
880
+ }
881
+ else if (this.drawSession) {
882
+ this.exitDrawMode();
883
+ }
884
+ else if (this.mosaicSession) {
885
+ this.exitMosaicMode();
886
+ }
887
+ else if (this.cropSession) {
888
+ this.cancelCrop();
889
+ }
890
+ }
891
+ finalizeActiveTextEditingIfNeeded() {
892
+ if (!this.canvas || !this.isFabricTextEditingActive())
893
+ return;
894
+ finalizeActiveTextEditing(this.buildTextControllerContext(), { commit: true });
895
+ }
581
896
  async loadImageFile(file) {
582
897
  const inputId = this.elements.imageInput;
583
898
  const inputEl = inputId
@@ -615,13 +930,15 @@ export class ImageEditor {
615
930
  return;
616
931
  if (this.isDisposed)
617
932
  return;
618
- if (typeof base64 !== 'string' || !base64.startsWith('data:image/'))
933
+ if (!isSupportedImageDataUrl(base64))
619
934
  return;
620
935
  if (!this.canRunIdleOperation('loadImage', options))
621
936
  return;
937
+ this.finalizeActiveTextEditingIfNeeded();
622
938
  const callbackContext = this.getOperationContext('loadImage', options);
623
939
  const previousImage = this.originalImage;
624
940
  const hadMasks = this.getMasks().length > 0;
941
+ const hadAnnotations = this.getAnnotations().length > 0;
625
942
  this.emitOptionCallback('onImageLoadStart', [callbackContext]);
626
943
  this.operationGuard.beginLoading();
627
944
  this.emitBusyChangeIfChanged(callbackContext);
@@ -650,6 +967,10 @@ export class ImageEditor {
650
967
  setMaskCounter: (v) => {
651
968
  this.maskCounter = v;
652
969
  },
970
+ getAnnotationCounter: () => this.annotationCounter,
971
+ setAnnotationCounter: (v) => {
972
+ this.annotationCounter = v;
973
+ },
653
974
  getCurrentScale: () => this.currentScale,
654
975
  setCurrentScale: (v) => {
655
976
  this.currentScale = v;
@@ -682,6 +1003,7 @@ export class ImageEditor {
682
1003
  this.lastMask = null;
683
1004
  this.updateInputs();
684
1005
  this.updateMaskList();
1006
+ this.updateAnnotationList();
685
1007
  this.updateUi();
686
1008
  if (previousImage && previousImage !== this.originalImage) {
687
1009
  this.emitOptionCallback('onImageCleared', [previousImage, callbackContext]);
@@ -693,6 +1015,9 @@ export class ImageEditor {
693
1015
  if (hadMasks) {
694
1016
  this.emitMasksChanged(callbackContext);
695
1017
  }
1018
+ if (hadAnnotations) {
1019
+ this.emitAnnotationsChanged(callbackContext);
1020
+ }
696
1021
  this.emitImageChanged(callbackContext);
697
1022
  }
698
1023
  getInternalOperationToken(options) {
@@ -717,15 +1042,11 @@ export class ImageEditor {
717
1042
  assertIdleForOperation(operationName, options) {
718
1043
  const token = this.getInternalOperationToken(options);
719
1044
  this.operationGuard.assertIdleForOperation(operationName, token);
720
- if (this.cropSession &&
721
- !this.operationGuard.isOwnOperation(token) &&
722
- !CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
723
- throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
724
- }
725
- if (this.mosaicSession &&
1045
+ const activeToolMode = this.getActiveToolMode();
1046
+ if (activeToolMode &&
726
1047
  !this.operationGuard.isOwnOperation(token) &&
727
- !MOSAIC_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
728
- throw new Error(`[ImageEditor] Cannot run "${operationName}" while mosaic mode is active.`);
1048
+ !TOOL_MODE_ALLOWED_OPERATIONS[activeToolMode].has(operationName)) {
1049
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while ${activeToolMode} mode is active.`);
729
1050
  }
730
1051
  if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
731
1052
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
@@ -743,12 +1064,27 @@ export class ImageEditor {
743
1064
  return false;
744
1065
  }
745
1066
  }
1067
+ getSelectedCropAspectRatio() {
1068
+ const inputId = this.elements.cropAspectRatioSelect;
1069
+ const inputEl = inputId
1070
+ ? document.getElementById(inputId)
1071
+ : null;
1072
+ const value = inputEl && 'value' in inputEl ? String(inputEl.value).trim() : '';
1073
+ return (value || 'free');
1074
+ }
746
1075
  isExpectedIdleGuardError(error, operationName) {
747
1076
  return (error instanceof Error &&
748
1077
  error.message.startsWith(`[ImageEditor] Cannot run "${operationName}" `));
749
1078
  }
750
1079
  assertCanQueueAnimation(operationName, options) {
751
- this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
1080
+ const token = this.getInternalOperationToken(options);
1081
+ this.operationGuard.assertCanQueueAnimation(operationName, token);
1082
+ const activeToolMode = this.getActiveToolMode();
1083
+ if (activeToolMode &&
1084
+ !this.operationGuard.isOwnOperation(token) &&
1085
+ !TOOL_MODE_ALLOWED_OPERATIONS[activeToolMode].has(operationName)) {
1086
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while ${activeToolMode} mode is active.`);
1087
+ }
752
1088
  }
753
1089
  isImageLoaded() {
754
1090
  var _a, _b;
@@ -758,10 +1094,7 @@ export class ImageEditor {
758
1094
  ((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
759
1095
  }
760
1096
  isBusy() {
761
- return (this.operationGuard.isBusy() ||
762
- this.animQueue.isBusy() ||
763
- this.cropSession !== null ||
764
- this.mosaicSession !== null);
1097
+ return this.operationGuard.isBusy() || this.animQueue.isBusy() || this.isToolModeActive();
765
1098
  }
766
1099
  setLayoutMode(mode) {
767
1100
  if (!isLayoutMode(mode)) {
@@ -836,12 +1169,37 @@ export class ImageEditor {
836
1169
  return [];
837
1170
  return this.canvas.getObjects().filter(isMaskObject).slice();
838
1171
  }
1172
+ getAnnotations() {
1173
+ if (!this.canvas)
1174
+ return [];
1175
+ return getAnnotationsImpl(this.canvas);
1176
+ }
839
1177
  getMaskCollectionSignature() {
840
1178
  return this.getMasks()
841
1179
  .map((mask) => `${mask.maskId}:${mask.maskName}`)
842
1180
  .join('|');
843
1181
  }
1182
+ getAnnotationCollectionSignature() {
1183
+ return this.getAnnotations()
1184
+ .map((annotation) => `${annotation.annotationId}:${annotation.annotationName}`)
1185
+ .join('|');
1186
+ }
1187
+ getActiveToolMode() {
1188
+ if (this.cropSession)
1189
+ return 'crop';
1190
+ if (this.mosaicSession)
1191
+ return 'mosaic';
1192
+ if (this.textSession)
1193
+ return 'text';
1194
+ if (this.drawSession)
1195
+ return 'draw';
1196
+ return null;
1197
+ }
1198
+ isToolModeActive() {
1199
+ return this.getActiveToolMode() !== null;
1200
+ }
844
1201
  getEditorState() {
1202
+ var _a, _b;
845
1203
  const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
846
1204
  const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
847
1205
  const image = this.getImageInfo();
@@ -849,11 +1207,17 @@ export class ImageEditor {
849
1207
  hasImage: image !== null,
850
1208
  image,
851
1209
  maskCount: this.getMasks().length,
1210
+ annotationCount: this.getAnnotations().length,
852
1211
  currentScale: this.currentScale,
853
1212
  currentRotation: this.currentRotation,
1213
+ isFlippedHorizontally: !!((_a = this.originalImage) === null || _a === void 0 ? void 0 : _a.flipX),
1214
+ isFlippedVertically: !!((_b = this.originalImage) === null || _b === void 0 ? void 0 : _b.flipY),
854
1215
  isBusy: this.isBusy(),
1216
+ activeToolMode: this.getActiveToolMode(),
855
1217
  isCropMode: this.cropSession !== null,
856
1218
  isMosaicMode: this.mosaicSession !== null,
1219
+ isTextMode: this.textSession !== null,
1220
+ isDrawMode: this.drawSession !== null,
857
1221
  canUndo: this.historyManager.canUndo(),
858
1222
  canRedo: this.historyManager.canRedo(),
859
1223
  canvasWidth,
@@ -866,6 +1230,9 @@ export class ImageEditor {
866
1230
  emitMasksChanged(context) {
867
1231
  this.emitOptionCallback('onMasksChanged', [this.getMasks(), context]);
868
1232
  }
1233
+ emitAnnotationsChanged(context) {
1234
+ this.emitOptionCallback('onAnnotationsChanged', [this.getAnnotations(), context]);
1235
+ }
869
1236
  emitBusyChangeIfChanged(context) {
870
1237
  const isBusy = this.isBusy();
871
1238
  if (this.lastEmittedIsBusy === isBusy)
@@ -874,11 +1241,20 @@ export class ImageEditor {
874
1241
  this.emitOptionCallback('onBusyChange', [isBusy, context]);
875
1242
  }
876
1243
  buildSelection(selected) {
877
- var _a;
1244
+ var _a, _b;
878
1245
  const selectedMasks = selected.filter(isMaskObject);
1246
+ const selectedAnnotations = selected.filter(isAnnotationObject);
1247
+ const selectedObjectKind = selectedMasks.length === 1 && selectedAnnotations.length === 0
1248
+ ? 'mask'
1249
+ : selectedAnnotations.length === 1 && selectedMasks.length === 0
1250
+ ? 'annotation'
1251
+ : null;
879
1252
  return {
880
1253
  selectedMask: (_a = selectedMasks[0]) !== null && _a !== void 0 ? _a : null,
881
1254
  selectedMasks,
1255
+ selectedAnnotation: (_b = selectedAnnotations[0]) !== null && _b !== void 0 ? _b : null,
1256
+ selectedAnnotations,
1257
+ selectedObjectKind,
882
1258
  };
883
1259
  }
884
1260
  withSelectionChangeContext(context, callback) {
@@ -917,7 +1293,7 @@ export class ImageEditor {
917
1293
  applyCanvasDimensions(this.canvas, widthPx, heightPx, this.containerElement);
918
1294
  }
919
1295
  alignObjectBoundingBoxToCanvasTopLeft(object) {
920
- var _a, _b;
1296
+ var _a, _b, _c;
921
1297
  object.setCoords();
922
1298
  const boundingRect = object.getBoundingRect();
923
1299
  object.set({
@@ -925,7 +1301,7 @@ export class ImageEditor {
925
1301
  top: ((_b = object.top) !== null && _b !== void 0 ? _b : 0) - boundingRect.top,
926
1302
  });
927
1303
  object.setCoords();
928
- this.canvas.renderAll();
1304
+ (_c = this.canvas) === null || _c === void 0 ? void 0 : _c.renderAll();
929
1305
  }
930
1306
  measureLayoutViewport(scrollbarSize) {
931
1307
  return this.viewportCache.measure(this.containerElement, {
@@ -933,7 +1309,13 @@ export class ImageEditor {
933
1309
  height: this.options.canvasHeight,
934
1310
  }, scrollbarSize);
935
1311
  }
936
- updateCanvasSizeToImageBounds() {
1312
+ getScrollbarStableViewportCanvasSize(viewport) {
1313
+ return {
1314
+ width: Math.max(1, viewport.width - 1),
1315
+ height: Math.max(1, viewport.height - 1),
1316
+ };
1317
+ }
1318
+ updateCanvasSizeToImageBounds(options = {}) {
937
1319
  var _a, _b;
938
1320
  if (!this.originalImage)
939
1321
  return;
@@ -941,13 +1323,26 @@ export class ImageEditor {
941
1323
  const boundingRect = this.originalImage.getBoundingRect();
942
1324
  const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
943
1325
  const viewport = this.measureLayoutViewport(scrollbarSize);
1326
+ const shouldStabilizeContainedViewport = options.stabilizeContainedViewport !== false;
1327
+ const imageFitsViewport = boundingRect.width <= viewport.width + LAYOUT_EPSILON &&
1328
+ boundingRect.height <= viewport.height + LAYOUT_EPSILON;
944
1329
  if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
1330
+ if (imageFitsViewport) {
1331
+ const canvasSize = shouldStabilizeContainedViewport
1332
+ ? this.getScrollbarStableViewportCanvasSize(viewport)
1333
+ : viewport;
1334
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
1335
+ return;
1336
+ }
945
1337
  const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
946
1338
  this.setCanvasSizePx(canvasSize.width, canvasSize.height);
947
1339
  return;
948
1340
  }
949
- if (boundingRect.width <= viewport.width && boundingRect.height <= viewport.height) {
950
- this.setCanvasSizePx(viewport.width, viewport.height);
1341
+ if (imageFitsViewport) {
1342
+ const canvasSize = shouldStabilizeContainedViewport
1343
+ ? this.getScrollbarStableViewportCanvasSize(viewport)
1344
+ : viewport;
1345
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
951
1346
  return;
952
1347
  }
953
1348
  this.setCanvasSizePx(Math.max(viewport.width, Math.ceil(boundingRect.width)), Math.max(viewport.height, Math.ceil(boundingRect.height)));
@@ -1047,7 +1442,7 @@ export class ImageEditor {
1047
1442
  }
1048
1443
  buildTransformContext() {
1049
1444
  return {
1050
- canvas: this.canvas,
1445
+ canvas: this.getLiveCanvasOrThrow('buildTransformContext'),
1051
1446
  options: this.options,
1052
1447
  guard: this.operationGuard,
1053
1448
  getOriginalImage: () => this.originalImage,
@@ -1146,6 +1541,70 @@ export class ImageEditor {
1146
1541
  this.emitBusyChangeIfChanged(context);
1147
1542
  });
1148
1543
  }
1544
+ flipHorizontal() {
1545
+ if (this.isDisposed || !this.transformController)
1546
+ return Promise.resolve();
1547
+ try {
1548
+ this.assertCanQueueAnimation('flipHorizontal');
1549
+ }
1550
+ catch (error) {
1551
+ return Promise.reject(error);
1552
+ }
1553
+ const controller = this.transformController;
1554
+ const context = this.buildCallbackContext('flipHorizontal', false);
1555
+ const job = this.animQueue.add(async () => {
1556
+ if (this.isDisposed)
1557
+ return;
1558
+ this.updateUi();
1559
+ try {
1560
+ await controller.flipHorizontal();
1561
+ if (!this.isDisposed)
1562
+ this.emitImageChanged(context);
1563
+ }
1564
+ finally {
1565
+ if (!this.isDisposed) {
1566
+ this.updateInputs();
1567
+ }
1568
+ }
1569
+ });
1570
+ this.emitBusyChangeIfChanged(context);
1571
+ return job.finally(() => {
1572
+ this.refreshUiAfterQueuedAnimation();
1573
+ this.emitBusyChangeIfChanged(context);
1574
+ });
1575
+ }
1576
+ flipVertical() {
1577
+ if (this.isDisposed || !this.transformController)
1578
+ return Promise.resolve();
1579
+ try {
1580
+ this.assertCanQueueAnimation('flipVertical');
1581
+ }
1582
+ catch (error) {
1583
+ return Promise.reject(error);
1584
+ }
1585
+ const controller = this.transformController;
1586
+ const context = this.buildCallbackContext('flipVertical', false);
1587
+ const job = this.animQueue.add(async () => {
1588
+ if (this.isDisposed)
1589
+ return;
1590
+ this.updateUi();
1591
+ try {
1592
+ await controller.flipVertical();
1593
+ if (!this.isDisposed)
1594
+ this.emitImageChanged(context);
1595
+ }
1596
+ finally {
1597
+ if (!this.isDisposed) {
1598
+ this.updateInputs();
1599
+ }
1600
+ }
1601
+ });
1602
+ this.emitBusyChangeIfChanged(context);
1603
+ return job.finally(() => {
1604
+ this.refreshUiAfterQueuedAnimation();
1605
+ this.emitBusyChangeIfChanged(context);
1606
+ });
1607
+ }
1149
1608
  resetImageTransform() {
1150
1609
  if (this.isDisposed || !this.transformController)
1151
1610
  return Promise.resolve();
@@ -1199,6 +1658,7 @@ export class ImageEditor {
1199
1658
  const context = this.buildCallbackContext(activeRestoreOperation !== null && activeRestoreOperation !== void 0 ? activeRestoreOperation : 'loadFromState', activeRestoreOperation === 'undo' || activeRestoreOperation === 'redo');
1200
1659
  const previousImage = this.originalImage;
1201
1660
  const previousMaskSignature = this.getMaskCollectionSignature();
1661
+ const previousAnnotationSignature = this.getAnnotationCollectionSignature();
1202
1662
  try {
1203
1663
  const restoredState = await loadFromStateImpl({
1204
1664
  canvas: this.canvas,
@@ -1221,6 +1681,7 @@ export class ImageEditor {
1221
1681
  this.canvas.sendObjectToBack(this.originalImage);
1222
1682
  }
1223
1683
  this.maskCounter = restoredState.maxMaskId;
1684
+ this.annotationCounter = restoredState.maxAnnotationId;
1224
1685
  const editorState = restoredState.editorState;
1225
1686
  if (editorState) {
1226
1687
  this.currentScale = editorState.currentScale;
@@ -1238,22 +1699,25 @@ export class ImageEditor {
1238
1699
  }
1239
1700
  this.isImageLoadedToCanvas = !!this.originalImage;
1240
1701
  if (this.originalImage && this.shouldNormalizeCanvasSizeAfterStateRestore()) {
1241
- this.updateCanvasSizeToImageBounds();
1702
+ this.updateCanvasSizeToImageBounds({ stabilizeContainedViewport: false });
1242
1703
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
1243
1704
  }
1244
1705
  if (this.originalImage) {
1245
1706
  this.settleFitCoverScrollbarsAfterStateRestore();
1246
1707
  }
1247
- const restoredMasks = restoredState.objects.filter(isMaskObject);
1708
+ const restoredMasks = restoredState.masks;
1248
1709
  this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
1249
1710
  restoredMasks.forEach((maskObject) => {
1250
1711
  applyMaskUnselectedStyle(maskObject);
1251
1712
  reattachMaskHoverHandlers(maskObject);
1252
1713
  });
1714
+ syncAnnotationRuntimeStates(restoredState.annotations);
1715
+ attachTextEditingHandlersToAnnotations(this.buildTextControllerContext(), restoredState.annotations);
1253
1716
  this.lastSnapshot = this.captureSnapshotInternal();
1254
1717
  this.canvas.renderAll();
1255
1718
  this.updateInputs();
1256
1719
  this.updateMaskList();
1720
+ this.updateAnnotationList();
1257
1721
  this.updateUi();
1258
1722
  if (previousImage && previousImage !== this.originalImage) {
1259
1723
  this.emitOptionCallback('onImageCleared', [previousImage, context]);
@@ -1261,17 +1725,32 @@ export class ImageEditor {
1261
1725
  if (previousMaskSignature !== this.getMaskCollectionSignature()) {
1262
1726
  this.emitMasksChanged(context);
1263
1727
  }
1728
+ if (previousAnnotationSignature !== this.getAnnotationCollectionSignature()) {
1729
+ this.emitAnnotationsChanged(context);
1730
+ }
1264
1731
  this.emitImageChanged(context);
1732
+ const canvas = this.getLiveCanvasOrThrow('loadFromState');
1265
1733
  const activeMaskId = editorState === null || editorState === void 0 ? void 0 : editorState.activeMaskId;
1266
- if (typeof activeMaskId === 'number') {
1734
+ const activeAnnotationId = editorState === null || editorState === void 0 ? void 0 : editorState.activeAnnotationId;
1735
+ if ((editorState === null || editorState === void 0 ? void 0 : editorState.activeObjectKind) === 'mask' && typeof activeMaskId === 'number') {
1267
1736
  const activeMask = restoredMasks.find((maskObject) => maskObject.maskId === activeMaskId);
1268
1737
  if (activeMask) {
1269
1738
  this.withSelectionChangeContext(context, () => {
1270
- this.canvas.setActiveObject(activeMask);
1739
+ canvas.setActiveObject(activeMask);
1271
1740
  this.handleSelectionChanged([activeMask]);
1272
1741
  });
1273
1742
  }
1274
1743
  }
1744
+ else if ((editorState === null || editorState === void 0 ? void 0 : editorState.activeObjectKind) === 'annotation' &&
1745
+ typeof activeAnnotationId === 'number') {
1746
+ const activeAnnotation = restoredState.annotations.find((annotation) => annotation.annotationId === activeAnnotationId);
1747
+ if (activeAnnotation) {
1748
+ this.withSelectionChangeContext(context, () => {
1749
+ canvas.setActiveObject(activeAnnotation);
1750
+ this.handleSelectionChanged([activeAnnotation]);
1751
+ });
1752
+ }
1753
+ }
1275
1754
  }
1276
1755
  catch (error) {
1277
1756
  reportError(this.options, error, 'Failed to restore canvas state.');
@@ -1282,24 +1761,26 @@ export class ImageEditor {
1282
1761
  this.saveStateInternal();
1283
1762
  }
1284
1763
  saveStateInternal(options) {
1285
- var _a, _b;
1764
+ var _a, _b, _c;
1286
1765
  if (!this.canvas || this.shouldSuppressSaveState)
1287
1766
  return;
1288
1767
  if (!this.canRunIdleOperation('saveState', options))
1289
1768
  return;
1290
1769
  const activeObj = this.canvas.getActiveObject();
1291
1770
  const activeMask = this.getActiveMaskForSnapshot();
1771
+ const activeAnnotation = this.getActiveAnnotationForSnapshot();
1292
1772
  this.hideAllMaskLabels();
1293
1773
  try {
1294
1774
  const after = saveStateImpl({
1295
1775
  canvas: this.canvas,
1296
1776
  activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
1777
+ activeAnnotationId: (_b = activeAnnotation === null || activeAnnotation === void 0 ? void 0 : activeAnnotation.annotationId) !== null && _b !== void 0 ? _b : null,
1297
1778
  currentScale: this.currentScale,
1298
1779
  currentRotation: this.currentRotation,
1299
1780
  baseImageScale: this.baseImageScale,
1300
1781
  currentImageMimeType: this.currentImageMimeType,
1301
1782
  });
1302
- const before = (_b = this.lastSnapshot) !== null && _b !== void 0 ? _b : after;
1783
+ const before = (_c = this.lastSnapshot) !== null && _c !== void 0 ? _c : after;
1303
1784
  if (after === before) {
1304
1785
  return;
1305
1786
  }
@@ -1315,25 +1796,32 @@ export class ImageEditor {
1315
1796
  reportWarning(this.options, error, 'Failed to capture canvas snapshot.');
1316
1797
  }
1317
1798
  finally {
1318
- this.restoreActiveMaskAfterSnapshot(activeObj, activeMask);
1799
+ this.restoreActiveObjectAfterSnapshot(activeObj, activeMask, activeAnnotation);
1319
1800
  this.updateUi();
1320
1801
  }
1321
1802
  }
1322
- restoreActiveMaskAfterSnapshot(activeObj, activeMask) {
1803
+ restoreActiveObjectAfterSnapshot(activeObj, activeMask, activeAnnotation) {
1323
1804
  if (!this.canvas)
1324
1805
  return;
1325
1806
  const maskToRestore = activeObj && isMaskObject(activeObj) ? activeObj : activeMask;
1326
- if (!maskToRestore || !this.canvas.getObjects().includes(maskToRestore))
1807
+ const annotationToRestore = activeObj && isAnnotationObject(activeObj) ? activeObj : activeAnnotation;
1808
+ if (maskToRestore && this.canvas.getObjects().includes(maskToRestore)) {
1809
+ this.canvas.setActiveObject(maskToRestore);
1810
+ this.showLabelForMask(maskToRestore);
1811
+ this.updateMaskListSelection(maskToRestore);
1327
1812
  return;
1328
- this.canvas.setActiveObject(maskToRestore);
1329
- this.showLabelForMask(maskToRestore);
1330
- this.updateMaskListSelection(maskToRestore);
1813
+ }
1814
+ if (annotationToRestore && this.canvas.getObjects().includes(annotationToRestore)) {
1815
+ this.canvas.setActiveObject(annotationToRestore);
1816
+ this.updateAnnotationListSelection(annotationToRestore);
1817
+ }
1331
1818
  }
1332
1819
  undo() {
1333
1820
  if (this.isDisposed)
1334
1821
  return Promise.resolve();
1335
1822
  if (!this.canRunIdleOperation('undo'))
1336
1823
  return Promise.resolve();
1824
+ this.finalizeActiveTextEditingIfNeeded();
1337
1825
  const context = this.buildCallbackContext('undo', true);
1338
1826
  const job = this.animQueue.add(async () => {
1339
1827
  if (this.isDisposed)
@@ -1357,6 +1845,7 @@ export class ImageEditor {
1357
1845
  return Promise.resolve();
1358
1846
  if (!this.canRunIdleOperation('redo'))
1359
1847
  return Promise.resolve();
1848
+ this.finalizeActiveTextEditingIfNeeded();
1360
1849
  const context = this.buildCallbackContext('redo', true);
1361
1850
  const job = this.animQueue.add(async () => {
1362
1851
  if (this.isDisposed)
@@ -1422,7 +1911,7 @@ export class ImageEditor {
1422
1911
  buildCreateMaskContext() {
1423
1912
  return {
1424
1913
  fabric: this.fabricModule,
1425
- canvas: this.canvas,
1914
+ canvas: this.getLiveCanvasOrThrow('createMask'),
1426
1915
  options: this.getRuntimeOptions(),
1427
1916
  getLastMask: () => this.lastMask,
1428
1917
  setLastMask: (maskObject) => {
@@ -1445,7 +1934,7 @@ export class ImageEditor {
1445
1934
  }
1446
1935
  buildRemoveMaskContext() {
1447
1936
  return {
1448
- canvas: this.canvas,
1937
+ canvas: this.getLiveCanvasOrThrow('removeMask'),
1449
1938
  removeLabelForMask: (mask) => {
1450
1939
  this.removeLabelForMask(mask);
1451
1940
  },
@@ -1495,11 +1984,35 @@ export class ImageEditor {
1495
1984
  return;
1496
1985
  showLabelForMask(context, mask);
1497
1986
  }
1987
+ handleObjectMovingScalingRotating(target) {
1988
+ if (isMaskObject(target)) {
1989
+ this.syncMaskLabel(target);
1990
+ }
1991
+ }
1992
+ handleObjectModified(target) {
1993
+ if (isMaskObject(target)) {
1994
+ this.syncMaskLabel(target);
1995
+ const context = this.buildCallbackContext('saveState', false);
1996
+ this.saveState();
1997
+ this.emitMasksChanged(context);
1998
+ this.emitImageChanged(context);
1999
+ return;
2000
+ }
2001
+ if (isAnnotationObject(target)) {
2002
+ if (isAnnotationLocked(target))
2003
+ return;
2004
+ const context = this.buildCallbackContext('updateAnnotation', false);
2005
+ this.saveState();
2006
+ this.emitAnnotationsChanged(context);
2007
+ this.emitImageChanged(context);
2008
+ }
2009
+ }
1498
2010
  handleSelectionChanged(selected) {
1499
- var _a, _b, _c;
2011
+ var _a, _b, _c, _d;
1500
2012
  if (!this.canvas)
1501
2013
  return;
1502
2014
  const selectedMask = (_a = selected.find(isMaskObject)) !== null && _a !== void 0 ? _a : null;
2015
+ const selectedAnnotation = (_b = selected.find(isAnnotationObject)) !== null && _b !== void 0 ? _b : null;
1503
2016
  const masks = this.canvas.getObjects().filter(isMaskObject);
1504
2017
  masks.forEach((maskObject) => {
1505
2018
  if (maskObject !== selectedMask) {
@@ -1515,9 +2028,10 @@ export class ImageEditor {
1515
2028
  if (selectedMask)
1516
2029
  this.showLabelForMask(selectedMask);
1517
2030
  this.updateMaskListSelection(selectedMask);
2031
+ this.updateAnnotationListSelection(selectedAnnotation);
1518
2032
  this.canvas.requestRenderAll();
1519
2033
  this.updateUi();
1520
- const context = (_b = this.nextSelectionChangeContext) !== null && _b !== void 0 ? _b : this.buildCallbackContext((_c = this.activeStateRestoreOperation) !== null && _c !== void 0 ? _c : 'createMask', this.activeStateRestoreOperation === 'undo' ||
2034
+ const context = (_c = this.nextSelectionChangeContext) !== null && _c !== void 0 ? _c : this.buildCallbackContext((_d = this.activeStateRestoreOperation) !== null && _d !== void 0 ? _d : 'createMask', this.activeStateRestoreOperation === 'undo' ||
1521
2035
  this.activeStateRestoreOperation === 'redo');
1522
2036
  this.emitOptionCallback('onSelectionChange', [this.buildSelection(selected), context]);
1523
2037
  }
@@ -1534,11 +2048,426 @@ export class ImageEditor {
1534
2048
  updateMaskListSelection(selectedMask) {
1535
2049
  updateMaskListSelection(this.buildMaskListContext(), selectedMask);
1536
2050
  }
2051
+ enterTextMode() {
2052
+ if (!this.canvas)
2053
+ return;
2054
+ if (!this.canRunIdleOperation('enterTextMode'))
2055
+ return;
2056
+ if (this.isToolModeActive())
2057
+ return;
2058
+ enterTextModeImpl(this.buildTextControllerContext());
2059
+ const callbackContext = this.buildCallbackContext('enterTextMode', false);
2060
+ this.emitBusyChangeIfChanged(callbackContext);
2061
+ this.emitImageChanged(callbackContext);
2062
+ }
2063
+ exitTextMode() {
2064
+ if (!this.canvas || !this.textSession)
2065
+ return;
2066
+ if (!this.canRunIdleOperation('exitTextMode'))
2067
+ return;
2068
+ exitTextModeImpl(this.buildTextControllerContext());
2069
+ const callbackContext = this.buildCallbackContext('exitTextMode', false);
2070
+ this.emitBusyChangeIfChanged(callbackContext);
2071
+ this.emitImageChanged(callbackContext);
2072
+ }
2073
+ isTextMode() {
2074
+ return this.textSession !== null;
2075
+ }
2076
+ createTextAnnotation(config = {}) {
2077
+ if (!this.canvas)
2078
+ return null;
2079
+ if (!this.canRunIdleOperation('createTextAnnotation'))
2080
+ return null;
2081
+ return createTextAnnotationImpl(this.buildTextControllerContext(), config);
2082
+ }
2083
+ enterDrawMode() {
2084
+ if (!this.canvas)
2085
+ return;
2086
+ if (!this.canRunIdleOperation('enterDrawMode'))
2087
+ return;
2088
+ if (this.isToolModeActive())
2089
+ return;
2090
+ enterDrawModeImpl(this.buildDrawControllerContext());
2091
+ const callbackContext = this.buildCallbackContext('enterDrawMode', false);
2092
+ this.emitBusyChangeIfChanged(callbackContext);
2093
+ this.emitImageChanged(callbackContext);
2094
+ }
2095
+ exitDrawMode() {
2096
+ if (!this.canvas || !this.drawSession)
2097
+ return;
2098
+ if (!this.canRunIdleOperation('exitDrawMode'))
2099
+ return;
2100
+ exitDrawModeImpl(this.buildDrawControllerContext());
2101
+ const callbackContext = this.buildCallbackContext('exitDrawMode', false);
2102
+ this.emitBusyChangeIfChanged(callbackContext);
2103
+ this.emitImageChanged(callbackContext);
2104
+ }
2105
+ isDrawMode() {
2106
+ return this.drawSession !== null;
2107
+ }
2108
+ getTextConfig() {
2109
+ return cloneResolvedTextAnnotationConfig(this.currentTextConfig);
2110
+ }
2111
+ setTextConfig(config) {
2112
+ this.applyTextConfigPatch(config, 'setTextConfig');
2113
+ }
2114
+ resetTextConfig() {
2115
+ this.applyTextConfigPatch(this.defaultTextConfig, 'resetTextConfig');
2116
+ }
2117
+ setTextColor(color) {
2118
+ this.applyTextConfigPatch({ fill: color }, 'setTextColor');
2119
+ }
2120
+ setTextFontSize(size) {
2121
+ this.applyTextConfigPatch({ fontSize: size }, 'setTextFontSize');
2122
+ }
2123
+ getDrawConfig() {
2124
+ return cloneResolvedDrawConfig(this.currentDrawConfig);
2125
+ }
2126
+ setDrawConfig(config) {
2127
+ this.applyDrawConfigPatch(config, 'setDrawConfig');
2128
+ }
2129
+ resetDrawConfig() {
2130
+ this.applyDrawConfigPatch(this.defaultDrawConfig, 'resetDrawConfig');
2131
+ }
2132
+ setDrawColor(color) {
2133
+ this.applyDrawConfigPatch({ color }, 'setDrawColor');
2134
+ }
2135
+ setDrawBrushSize(size) {
2136
+ this.applyDrawConfigPatch({ brushSize: size }, 'setDrawBrushSize');
2137
+ }
2138
+ removeSelectedAnnotation() {
2139
+ if (!this.canvas)
2140
+ return;
2141
+ if (!this.canRunIdleOperation('removeSelectedAnnotation'))
2142
+ return;
2143
+ const before = this.getAnnotations().length;
2144
+ const callbackContext = this.buildCallbackContext('removeSelectedAnnotation', false);
2145
+ this.withSelectionChangeContext(callbackContext, () => {
2146
+ removeSelectedAnnotationImpl(this.buildAnnotationManagerContext());
2147
+ });
2148
+ this.updateAnnotationList();
2149
+ this.updateUi();
2150
+ if (this.getAnnotations().length !== before) {
2151
+ this.emitAnnotationsChanged(callbackContext);
2152
+ this.emitImageChanged(callbackContext);
2153
+ }
2154
+ }
2155
+ removeAllAnnotations(options = {}) {
2156
+ if (!this.canvas)
2157
+ return;
2158
+ if (!this.canRunIdleOperation('removeAllAnnotations', options))
2159
+ return;
2160
+ const before = this.getAnnotations().length;
2161
+ const callbackContext = this.buildCallbackContext('removeAllAnnotations', false);
2162
+ this.withSelectionChangeContext(callbackContext, () => {
2163
+ removeAllAnnotationsImpl(this.buildAnnotationManagerContext(), options);
2164
+ });
2165
+ this.updateAnnotationList();
2166
+ this.updateUi();
2167
+ if (this.getAnnotations().length !== before) {
2168
+ this.emitAnnotationsChanged(callbackContext);
2169
+ this.emitImageChanged(callbackContext);
2170
+ }
2171
+ }
2172
+ updateAnnotation(annotationId, config) {
2173
+ if (!this.canvas)
2174
+ return;
2175
+ if (!this.canRunIdleOperation('updateAnnotation'))
2176
+ return;
2177
+ const callbackContext = this.buildCallbackContext('updateAnnotation', false);
2178
+ const changed = updateAnnotationImpl(this.buildAnnotationManagerContext(), annotationId, config);
2179
+ if (changed) {
2180
+ this.updateAnnotationList();
2181
+ this.emitAnnotationsChanged(callbackContext);
2182
+ this.emitImageChanged(callbackContext);
2183
+ }
2184
+ }
2185
+ updateSelectedAnnotation(config) {
2186
+ if (!this.canvas)
2187
+ return;
2188
+ if (!this.canRunIdleOperation('updateSelectedAnnotation'))
2189
+ return;
2190
+ const callbackContext = this.buildCallbackContext('updateSelectedAnnotation', false);
2191
+ const changed = updateSelectedAnnotationImpl(this.buildAnnotationManagerContext(), config);
2192
+ if (changed) {
2193
+ this.updateAnnotationList();
2194
+ this.emitAnnotationsChanged(callbackContext);
2195
+ this.emitImageChanged(callbackContext);
2196
+ }
2197
+ }
2198
+ deleteSelectedObject() {
2199
+ if (!this.canvas)
2200
+ return;
2201
+ if (!this.canRunIdleOperation('deleteSelectedObject'))
2202
+ return;
2203
+ this.finalizeActiveTextEditingIfNeeded();
2204
+ const selectedObjects = this.getSelectedCanvasObjects();
2205
+ const selectedMasks = selectedObjects.filter(isMaskObject);
2206
+ const selectedAnnotations = selectedObjects.filter((object) => isAnnotationObject(object) && isAnnotationUnlocked(object));
2207
+ if (selectedMasks.length === 0 && selectedAnnotations.length === 0)
2208
+ return;
2209
+ const canvas = this.getLiveCanvasOrThrow('deleteSelectedObject');
2210
+ const callbackContext = this.buildCallbackContext('deleteSelectedObject', false);
2211
+ this.withSelectionChangeContext(callbackContext, () => {
2212
+ for (const mask of selectedMasks) {
2213
+ this.removeLabelForMask(mask);
2214
+ canvas.remove(mask);
2215
+ }
2216
+ removeAnnotationObjects(this.buildAnnotationManagerContext(), selectedAnnotations, {
2217
+ saveHistory: false,
2218
+ force: true,
2219
+ });
2220
+ canvas.discardActiveObject();
2221
+ canvas.renderAll();
2222
+ this.saveState();
2223
+ });
2224
+ this.updateMaskList();
2225
+ this.updateAnnotationList();
2226
+ this.updateUi();
2227
+ if (selectedMasks.length > 0)
2228
+ this.emitMasksChanged(callbackContext);
2229
+ if (selectedAnnotations.length > 0)
2230
+ this.emitAnnotationsChanged(callbackContext);
2231
+ this.emitImageChanged(callbackContext);
2232
+ }
2233
+ bringSelectedObjectForward() {
2234
+ this.moveSelectedEditableObject('bringSelectedObjectForward');
2235
+ }
2236
+ sendSelectedObjectBackward() {
2237
+ this.moveSelectedEditableObject('sendSelectedObjectBackward');
2238
+ }
2239
+ bringSelectedObjectToFront() {
2240
+ this.moveSelectedEditableObject('bringSelectedObjectToFront');
2241
+ }
2242
+ sendSelectedObjectToBack() {
2243
+ this.moveSelectedEditableObject('sendSelectedObjectToBack');
2244
+ }
2245
+ buildAnnotationManagerContext() {
2246
+ return {
2247
+ canvas: this.getLiveCanvasOrThrow('annotationManager'),
2248
+ saveCanvasState: () => this.saveState(),
2249
+ updateUi: () => this.updateUi(),
2250
+ };
2251
+ }
2252
+ buildAnnotationListContext() {
2253
+ return {
2254
+ canvas: this.canvas,
2255
+ getListElementId: () => this.elements.annotationList,
2256
+ onAnnotationSelected: (annotation) => this.handleSelectionChanged([annotation]),
2257
+ };
2258
+ }
2259
+ updateAnnotationList() {
2260
+ renderAnnotationList(this.buildAnnotationListContext());
2261
+ }
2262
+ updateAnnotationListSelection(selectedAnnotation) {
2263
+ updateAnnotationListSelection(this.buildAnnotationListContext(), selectedAnnotation);
2264
+ }
2265
+ buildTextControllerContext() {
2266
+ return {
2267
+ fabric: this.fabricModule,
2268
+ canvas: this.getLiveCanvasOrThrow('textController'),
2269
+ options: this.options,
2270
+ getOriginalImage: () => this.originalImage,
2271
+ getTextConfig: () => this.currentTextConfig,
2272
+ isImageLoaded: () => this.isImageLoaded(),
2273
+ getAnnotationCounter: () => this.annotationCounter,
2274
+ setAnnotationCounter: (value) => {
2275
+ this.annotationCounter = value;
2276
+ },
2277
+ getTextSession: () => this.textSession,
2278
+ setTextSession: (session) => {
2279
+ this.textSession = session;
2280
+ },
2281
+ saveCanvasState: () => this.saveState(),
2282
+ updateAnnotationList: () => this.updateAnnotationList(),
2283
+ updateUi: () => this.updateUi(),
2284
+ emitAnnotationsChanged: (context) => this.emitAnnotationsChanged(context),
2285
+ emitImageChanged: (context) => this.emitImageChanged(context),
2286
+ buildCallbackContext: (operation) => this.buildCallbackContext(operation, false),
2287
+ };
2288
+ }
2289
+ buildDrawControllerContext() {
2290
+ return {
2291
+ fabric: this.fabricModule,
2292
+ canvas: this.getLiveCanvasOrThrow('drawController'),
2293
+ options: this.options,
2294
+ getDrawConfig: () => this.currentDrawConfig,
2295
+ isImageLoaded: () => this.isImageLoaded(),
2296
+ getAnnotationCounter: () => this.annotationCounter,
2297
+ setAnnotationCounter: (value) => {
2298
+ this.annotationCounter = value;
2299
+ },
2300
+ getDrawSession: () => this.drawSession,
2301
+ setDrawSession: (session) => {
2302
+ this.drawSession = session;
2303
+ },
2304
+ saveCanvasState: () => this.saveState(),
2305
+ updateAnnotationList: () => this.updateAnnotationList(),
2306
+ updateUi: () => this.updateUi(),
2307
+ emitAnnotationsChanged: (context) => this.emitAnnotationsChanged(context),
2308
+ emitImageChanged: (context) => this.emitImageChanged(context),
2309
+ buildCallbackContext: (operation) => this.buildCallbackContext(operation, false),
2310
+ };
2311
+ }
2312
+ applyTextConfigPatch(config, operation) {
2313
+ if (!this.canRunIdleOperation(operation))
2314
+ return;
2315
+ const invalidFields = getInvalidTextAnnotationConfigFields(config);
2316
+ if (invalidFields.length > 0) {
2317
+ reportWarning(this.options, null, `${operation} ignored invalid Text config fields: ${invalidFields.join(', ')}.`);
2318
+ }
2319
+ const next = mergeTextAnnotationConfigPatch(this.currentTextConfig, config, this.defaultTextConfig);
2320
+ if (areResolvedTextAnnotationConfigsEqual(this.currentTextConfig, next))
2321
+ return;
2322
+ this.currentTextConfig = next;
2323
+ this.updateInputs();
2324
+ this.updateUi();
2325
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
2326
+ }
2327
+ applyDrawConfigPatch(config, operation) {
2328
+ if (!this.canRunIdleOperation(operation))
2329
+ return;
2330
+ const invalidFields = getInvalidDrawConfigFields(config);
2331
+ if (invalidFields.length > 0) {
2332
+ reportWarning(this.options, null, `${operation} ignored invalid Draw config fields: ${invalidFields.join(', ')}.`);
2333
+ }
2334
+ const next = mergeDrawConfigPatch(this.currentDrawConfig, config, this.defaultDrawConfig);
2335
+ if (areResolvedDrawConfigsEqual(this.currentDrawConfig, next))
2336
+ return;
2337
+ this.currentDrawConfig = next;
2338
+ updateDrawBrush(this.buildDrawControllerContext());
2339
+ this.updateInputs();
2340
+ this.updateUi();
2341
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
2342
+ }
2343
+ applyTextColorInput(color) {
2344
+ var _a;
2345
+ if (this.isTextMode()) {
2346
+ this.setTextColor(color);
2347
+ return;
2348
+ }
2349
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2350
+ if (selected && isTextAnnotationObject(selected)) {
2351
+ this.updateSelectedAnnotation({ fill: color });
2352
+ return;
2353
+ }
2354
+ this.setTextColor(color);
2355
+ }
2356
+ applyTextFontSizeInput(size) {
2357
+ var _a;
2358
+ if (this.isTextMode()) {
2359
+ this.setTextFontSize(size);
2360
+ return;
2361
+ }
2362
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2363
+ if (selected && isTextAnnotationObject(selected)) {
2364
+ this.updateSelectedAnnotation({ fontSize: size });
2365
+ return;
2366
+ }
2367
+ this.setTextFontSize(size);
2368
+ }
2369
+ applyDrawColorInput(color) {
2370
+ var _a;
2371
+ if (this.isDrawMode()) {
2372
+ this.setDrawColor(color);
2373
+ return;
2374
+ }
2375
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2376
+ if (selected && isDrawAnnotationObject(selected)) {
2377
+ this.updateSelectedAnnotation({ stroke: color });
2378
+ return;
2379
+ }
2380
+ this.setDrawColor(color);
2381
+ }
2382
+ applyDrawBrushSizeInput(size) {
2383
+ var _a;
2384
+ if (this.isDrawMode()) {
2385
+ this.setDrawBrushSize(size);
2386
+ return;
2387
+ }
2388
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
2389
+ if (selected && isDrawAnnotationObject(selected)) {
2390
+ this.updateSelectedAnnotation({ strokeWidth: size });
2391
+ return;
2392
+ }
2393
+ this.setDrawBrushSize(size);
2394
+ }
2395
+ getSelectedCanvasObjects() {
2396
+ var _a, _b, _c;
2397
+ if (!this.canvas)
2398
+ return [];
2399
+ const activeObject = this.canvas.getActiveObject();
2400
+ if (!activeObject)
2401
+ return [];
2402
+ const type = typeof activeObject.type === 'string' ? activeObject.type.toLowerCase() : '';
2403
+ const isActiveSelection = type === 'activeselection' ||
2404
+ ((_c = (_b = (_a = activeObject).isType) === null || _b === void 0 ? void 0 : _b.call(_a, 'ActiveSelection')) !== null && _c !== void 0 ? _c : false);
2405
+ if (!isActiveSelection)
2406
+ return [activeObject];
2407
+ const getObjects = activeObject
2408
+ .getObjects;
2409
+ return typeof getObjects === 'function' ? getObjects.call(activeObject) : [];
2410
+ }
2411
+ moveSelectedEditableObject(operation) {
2412
+ if (!this.canvas)
2413
+ return;
2414
+ if (!this.canRunIdleOperation(operation))
2415
+ return;
2416
+ const selected = this.getSelectedCanvasObjects().filter(isEditableOverlayObject);
2417
+ if (selected.length !== 1) {
2418
+ if (selected.length > 1) {
2419
+ reportWarning(this.options, null, `${operation} skipped: ActiveSelection layer moves are not supported.`);
2420
+ }
2421
+ return;
2422
+ }
2423
+ const object = selected[0];
2424
+ const range = getEditableOverlayRange(this.canvas);
2425
+ const overlays = range.overlays;
2426
+ const currentOverlayIndex = overlays.indexOf(object);
2427
+ if (currentOverlayIndex < 0)
2428
+ return;
2429
+ let nextOverlayIndex = currentOverlayIndex;
2430
+ if (operation === 'bringSelectedObjectForward') {
2431
+ nextOverlayIndex = Math.min(overlays.length - 1, currentOverlayIndex + 1);
2432
+ }
2433
+ else if (operation === 'sendSelectedObjectBackward') {
2434
+ nextOverlayIndex = Math.max(0, currentOverlayIndex - 1);
2435
+ }
2436
+ else if (operation === 'bringSelectedObjectToFront') {
2437
+ nextOverlayIndex = overlays.length - 1;
2438
+ }
2439
+ else if (operation === 'sendSelectedObjectToBack') {
2440
+ nextOverlayIndex = 0;
2441
+ }
2442
+ if (nextOverlayIndex === currentOverlayIndex)
2443
+ return;
2444
+ const reordered = overlays.slice();
2445
+ reordered.splice(currentOverlayIndex, 1);
2446
+ reordered.splice(nextOverlayIndex, 0, object);
2447
+ reordered.forEach((overlay, index) => {
2448
+ var _a, _b;
2449
+ (_b = (_a = this.canvas).moveObjectTo) === null || _b === void 0 ? void 0 : _b.call(_a, overlay, range.start + index);
2450
+ });
2451
+ normalizeLayerOrder(this.canvas);
2452
+ this.canvas.setActiveObject(object);
2453
+ this.canvas.renderAll();
2454
+ this.saveState();
2455
+ this.updateMaskList();
2456
+ this.updateAnnotationList();
2457
+ this.updateUi();
2458
+ const context = this.buildCallbackContext(operation, false);
2459
+ if (isMaskObject(object))
2460
+ this.emitMasksChanged(context);
2461
+ if (isAnnotationObject(object))
2462
+ this.emitAnnotationsChanged(context);
2463
+ this.emitImageChanged(context);
2464
+ }
1537
2465
  async mergeMasks() {
1538
2466
  if (!this.canvas)
1539
2467
  return;
1540
2468
  if (!this.canRunIdleOperation('mergeMasks'))
1541
2469
  return;
2470
+ this.finalizeActiveTextEditingIfNeeded();
1542
2471
  const hasMasks = this.canvas.getObjects().some(isMaskObject);
1543
2472
  if (!hasMasks)
1544
2473
  return;
@@ -1551,7 +2480,11 @@ export class ImageEditor {
1551
2480
  await mergeMasksImpl(mergeMasksContext);
1552
2481
  this.updateInputs();
1553
2482
  this.updateMaskList();
2483
+ this.updateAnnotationList();
1554
2484
  this.emitMasksChanged(callbackContext);
2485
+ if (this.getAnnotations().length > 0) {
2486
+ this.emitAnnotationsChanged(callbackContext);
2487
+ }
1555
2488
  this.emitImageChanged(callbackContext);
1556
2489
  }
1557
2490
  finally {
@@ -1560,17 +2493,18 @@ export class ImageEditor {
1560
2493
  this.updateUi();
1561
2494
  }
1562
2495
  }
1563
- downloadImage(fileName) {
2496
+ async downloadImage(options) {
1564
2497
  if (!this.canvas)
1565
2498
  return;
1566
2499
  if (!this.canRunIdleOperation('downloadImage'))
1567
2500
  return;
2501
+ this.finalizeActiveTextEditingIfNeeded();
1568
2502
  const callbackContext = this.buildCallbackContext('downloadImage', false);
1569
2503
  const operationToken = this.operationGuard.beginBusyOperation('downloadImage');
1570
2504
  this.emitBusyChangeIfChanged(callbackContext);
1571
2505
  const exportContext = this.buildExportServiceContext();
1572
2506
  try {
1573
- downloadImageImpl(exportContext, fileName);
2507
+ await downloadImageImpl(exportContext, options);
1574
2508
  }
1575
2509
  finally {
1576
2510
  this.operationGuard.endBusyOperation(operationToken);
@@ -1582,6 +2516,7 @@ export class ImageEditor {
1582
2516
  return '';
1583
2517
  if (!this.canRunIdleOperation('exportImageBase64', options))
1584
2518
  return '';
2519
+ this.finalizeActiveTextEditingIfNeeded();
1585
2520
  const callbackContext = this.buildCallbackContext('exportImageBase64', false);
1586
2521
  const operationToken = this.operationGuard.beginBusyOperation('exportImageBase64');
1587
2522
  this.emitBusyChangeIfChanged(callbackContext);
@@ -1596,6 +2531,7 @@ export class ImageEditor {
1596
2531
  }
1597
2532
  async exportImageFile(options) {
1598
2533
  this.assertIdleForOperation('exportImageFile', options);
2534
+ this.finalizeActiveTextEditingIfNeeded();
1599
2535
  const callbackContext = this.buildCallbackContext('exportImageFile', false);
1600
2536
  const operationToken = this.operationGuard.beginBusyOperation('exportImageFile');
1601
2537
  this.emitBusyChangeIfChanged(callbackContext);
@@ -1611,7 +2547,7 @@ export class ImageEditor {
1611
2547
  buildExportServiceContext() {
1612
2548
  return {
1613
2549
  fabric: this.fabricModule,
1614
- canvas: this.canvas,
2550
+ canvas: this.getLiveCanvasOrThrow('export'),
1615
2551
  options: this.options,
1616
2552
  isImageLoaded: () => this.isImageLoaded(),
1617
2553
  getOriginalImage: () => this.originalImage,
@@ -1627,24 +2563,74 @@ export class ImageEditor {
1627
2563
  await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
1628
2564
  this.restoreMergedImageDisplayGeometry(geometry);
1629
2565
  },
1630
- saveState: () => this.captureSnapshotInternal(),
2566
+ captureSnapshot: () => this.captureSnapshotInternal(),
1631
2567
  loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
2568
+ exportImageBase64: (options) => exportImageBase64Impl(this.buildExportServiceContext(), options),
2569
+ updateUi: () => this.updateUi(),
2570
+ updateInputs: () => this.updateInputs(),
1632
2571
  removeAllMasksNoHistory: () => {
1633
2572
  const context = this.buildRemoveMaskContext();
1634
2573
  removeAllMasksImpl(context, { saveHistory: false });
1635
2574
  },
2575
+ getAnnotations: () => this.getAnnotations(),
2576
+ restoreAnnotations: (objects) => {
2577
+ const canvas = this.getLiveCanvasOrThrow('restoreAnnotations');
2578
+ objects.forEach((annotation) => {
2579
+ canvas.add(annotation);
2580
+ });
2581
+ syncAnnotationRuntimeStates(objects);
2582
+ attachTextEditingHandlersToAnnotations(this.buildTextControllerContext(), objects);
2583
+ this.annotationCounter = Math.max(this.annotationCounter, ...objects.map((annotation) => annotation.annotationId), 0);
2584
+ this.updateAnnotationList();
2585
+ },
2586
+ };
2587
+ }
2588
+ buildMergeAnnotationsContext(operationToken) {
2589
+ return {
2590
+ ...this.buildExportServiceContext(),
2591
+ historyManager: this.historyManager,
2592
+ containerElement: this.containerElement,
2593
+ loadImage: async (base64, providedOptions) => {
2594
+ const geometry = this.captureImageDisplayGeometry();
2595
+ await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
2596
+ this.restoreMergedImageDisplayGeometry(geometry);
2597
+ },
2598
+ captureSnapshot: () => this.captureSnapshotInternal(),
2599
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
2600
+ exportImageBase64: (options) => exportImageBase64Impl(this.buildExportServiceContext(), options),
2601
+ updateUi: () => this.updateUi(),
2602
+ updateInputs: () => this.updateInputs(),
2603
+ removeAllAnnotationsNoHistory: () => {
2604
+ removeAllAnnotationsImpl(this.buildAnnotationManagerContext(), {
2605
+ saveHistory: false,
2606
+ force: true,
2607
+ });
2608
+ },
2609
+ getMasks: () => this.getMasks(),
2610
+ restoreMasks: (objects) => {
2611
+ const canvas = this.getLiveCanvasOrThrow('restoreMasks');
2612
+ objects.forEach((mask) => {
2613
+ canvas.add(mask);
2614
+ reattachMaskHoverHandlers(mask);
2615
+ });
2616
+ this.lastMask = objects.reduce((lastMask, mask) => !lastMask || mask.maskId > lastMask.maskId ? mask : lastMask, null);
2617
+ this.maskCounter = Math.max(this.maskCounter, ...objects.map((mask) => mask.maskId), 0);
2618
+ this.updateMaskList();
2619
+ },
1636
2620
  };
1637
2621
  }
1638
2622
  captureSnapshotInternal() {
1639
- var _a;
2623
+ var _a, _b;
1640
2624
  if (!this.canvas) {
1641
2625
  throw new Error('[ImageEditor] Cannot capture canvas snapshot before init or after dispose.');
1642
2626
  }
1643
2627
  const activeMask = this.getActiveMaskForSnapshot();
2628
+ const activeAnnotation = this.getActiveAnnotationForSnapshot();
1644
2629
  this.hideAllMaskLabels();
1645
2630
  return saveStateImpl({
1646
2631
  canvas: this.canvas,
1647
2632
  activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
2633
+ activeAnnotationId: (_b = activeAnnotation === null || activeAnnotation === void 0 ? void 0 : activeAnnotation.annotationId) !== null && _b !== void 0 ? _b : null,
1648
2634
  currentScale: this.currentScale,
1649
2635
  currentRotation: this.currentRotation,
1650
2636
  baseImageScale: this.baseImageScale,
@@ -1663,6 +2649,12 @@ export class ImageEditor {
1663
2649
  .filter((object) => isMaskObject(object) && !!object.labelObject);
1664
2650
  return labeledMasks.length === 1 ? ((_a = labeledMasks[0]) !== null && _a !== void 0 ? _a : null) : null;
1665
2651
  }
2652
+ getActiveAnnotationForSnapshot() {
2653
+ if (!this.canvas)
2654
+ return null;
2655
+ const activeObject = this.canvas.getActiveObject();
2656
+ return activeObject && isAnnotationObject(activeObject) ? activeObject : null;
2657
+ }
1666
2658
  enterMosaicMode() {
1667
2659
  if (!this.canvas || !this.originalImage)
1668
2660
  return;
@@ -1746,7 +2738,7 @@ export class ImageEditor {
1746
2738
  buildMosaicControllerContext() {
1747
2739
  return {
1748
2740
  fabric: this.fabricModule,
1749
- canvas: this.canvas,
2741
+ canvas: this.getLiveCanvasOrThrow('mosaicController'),
1750
2742
  options: this.options,
1751
2743
  historyManager: this.historyManager,
1752
2744
  getMosaicConfig: () => cloneResolvedMosaicConfig(this.currentMosaicConfig),
@@ -1787,7 +2779,7 @@ export class ImageEditor {
1787
2779
  },
1788
2780
  };
1789
2781
  }
1790
- enterCropMode() {
2782
+ enterCropMode(options = {}) {
1791
2783
  if (!this.canvas || !this.originalImage)
1792
2784
  return;
1793
2785
  if (this.cropSession)
@@ -1797,12 +2789,23 @@ export class ImageEditor {
1797
2789
  if (!this.canRunIdleOperation('enterCropMode'))
1798
2790
  return;
1799
2791
  const cropControllerContext = this.buildCropControllerContext();
1800
- enterCropModeImpl(cropControllerContext);
2792
+ enterCropModeImpl(cropControllerContext, options);
1801
2793
  this.updateUi();
1802
2794
  const callbackContext = this.buildCallbackContext('enterCropMode', false);
1803
2795
  this.emitBusyChangeIfChanged(callbackContext);
1804
2796
  this.emitImageChanged(callbackContext);
1805
2797
  }
2798
+ setCropAspectRatio(aspectRatio) {
2799
+ if (!this.canvas || !this.cropSession)
2800
+ return;
2801
+ if (!this.canRunIdleOperation('setCropAspectRatio'))
2802
+ return;
2803
+ const cropControllerContext = this.buildCropControllerContext();
2804
+ setCropAspectRatioImpl(cropControllerContext, aspectRatio);
2805
+ this.updateUi();
2806
+ const callbackContext = this.buildCallbackContext('setCropAspectRatio', false);
2807
+ this.emitImageChanged(callbackContext);
2808
+ }
1806
2809
  cancelCrop() {
1807
2810
  if (!this.canvas || !this.cropSession)
1808
2811
  return;
@@ -1846,7 +2849,7 @@ export class ImageEditor {
1846
2849
  buildCropControllerContext(operationToken) {
1847
2850
  return {
1848
2851
  fabric: this.fabricModule,
1849
- canvas: this.canvas,
2852
+ canvas: this.getLiveCanvasOrThrow('cropController'),
1850
2853
  options: this.options,
1851
2854
  historyManager: this.historyManager,
1852
2855
  isImageLoaded: () => this.isImageLoaded(),
@@ -1868,50 +2871,127 @@ export class ImageEditor {
1868
2871
  },
1869
2872
  };
1870
2873
  }
2874
+ syncInputValue(inputElement, value) {
2875
+ if (!inputElement)
2876
+ return;
2877
+ const ownerDocument = inputElement.ownerDocument;
2878
+ if (ownerDocument.activeElement === inputElement && !inputElement.readOnly)
2879
+ return;
2880
+ if (inputElement.value !== value)
2881
+ inputElement.value = value;
2882
+ }
1871
2883
  updateInputs() {
1872
2884
  const scaleId = this.elements.scalePercentageInput;
1873
2885
  if (scaleId) {
1874
2886
  const scaleInputElement = document.getElementById(scaleId);
1875
- if (scaleInputElement) {
1876
- scaleInputElement.value = String(Math.round(this.currentScale * 100));
1877
- }
2887
+ this.syncInputValue(scaleInputElement, String(Math.round(this.currentScale * 100)));
1878
2888
  }
1879
2889
  const mosaicConfig = this.getMosaicConfig();
1880
2890
  const mosaicBrushSizeInputId = this.elements.mosaicBrushSizeInput;
1881
2891
  if (mosaicBrushSizeInputId) {
1882
2892
  const brushInput = document.getElementById(mosaicBrushSizeInputId);
1883
- if (brushInput)
1884
- brushInput.value = String(mosaicConfig.brushSize);
2893
+ this.syncInputValue(brushInput, String(mosaicConfig.brushSize));
1885
2894
  }
1886
2895
  const mosaicBlockSizeInputId = this.elements.mosaicBlockSizeInput;
1887
2896
  if (mosaicBlockSizeInputId) {
1888
2897
  const blockInput = document.getElementById(mosaicBlockSizeInputId);
1889
- if (blockInput)
1890
- blockInput.value = String(mosaicConfig.blockSize);
2898
+ this.syncInputValue(blockInput, String(mosaicConfig.blockSize));
2899
+ }
2900
+ const textConfig = this.getTextConfig();
2901
+ const textColorInputId = this.elements.textColorInput;
2902
+ if (textColorInputId) {
2903
+ const textColorInput = document.getElementById(textColorInputId);
2904
+ this.syncInputValue(textColorInput, textConfig.fill);
2905
+ }
2906
+ const textFontSizeInputId = this.elements.textFontSizeInput;
2907
+ if (textFontSizeInputId) {
2908
+ const fontInput = document.getElementById(textFontSizeInputId);
2909
+ this.syncInputValue(fontInput, String(textConfig.fontSize));
2910
+ }
2911
+ const drawConfig = this.getDrawConfig();
2912
+ const drawColorInputId = this.elements.drawColorInput;
2913
+ if (drawColorInputId) {
2914
+ const drawColorInput = document.getElementById(drawColorInputId);
2915
+ this.syncInputValue(drawColorInput, drawConfig.color);
2916
+ }
2917
+ const drawBrushSizeInputId = this.elements.drawBrushSizeInput;
2918
+ if (drawBrushSizeInputId) {
2919
+ const brushInput = document.getElementById(drawBrushSizeInputId);
2920
+ this.syncInputValue(brushInput, String(drawConfig.brushSize));
2921
+ }
2922
+ }
2923
+ async mergeAnnotations() {
2924
+ if (!this.canvas)
2925
+ return;
2926
+ if (!this.canRunIdleOperation('mergeAnnotations'))
2927
+ return;
2928
+ this.finalizeActiveTextEditingIfNeeded();
2929
+ const hasAnnotations = this.canvas.getObjects().some(isAnnotationObject);
2930
+ if (!hasAnnotations)
2931
+ return;
2932
+ const callbackContext = this.buildCallbackContext('mergeAnnotations', false);
2933
+ const operationToken = this.operationGuard.beginBusyOperation('mergeAnnotations');
2934
+ this.emitBusyChangeIfChanged(callbackContext);
2935
+ this.updateUi();
2936
+ try {
2937
+ await mergeAnnotationsImpl(this.buildMergeAnnotationsContext(operationToken));
2938
+ this.updateInputs();
2939
+ this.updateMaskList();
2940
+ this.updateAnnotationList();
2941
+ this.emitAnnotationsChanged(callbackContext);
2942
+ if (this.getMasks().length > 0)
2943
+ this.emitMasksChanged(callbackContext);
2944
+ this.emitImageChanged(callbackContext);
2945
+ }
2946
+ finally {
2947
+ this.operationGuard.endBusyOperation(operationToken);
2948
+ this.emitBusyChangeIfChanged(callbackContext);
2949
+ this.updateUi();
1891
2950
  }
1892
2951
  }
1893
2952
  updateUi() {
1894
- var _a;
2953
+ var _a, _b, _c;
1895
2954
  if (!this.canvas)
1896
2955
  return;
1897
2956
  const hasImage = !!this.originalImage;
1898
2957
  const masks = hasImage ? this.canvas.getObjects().filter(isMaskObject) : [];
2958
+ const annotations = hasImage ? this.canvas.getObjects().filter(isAnnotationObject) : [];
1899
2959
  const hasMasks = masks.length > 0;
2960
+ const hasAnnotations = annotations.length > 0;
1900
2961
  const activeObject = this.canvas.getActiveObject();
1901
2962
  const hasSelectedMask = !!(activeObject && isMaskObject(activeObject));
1902
- const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
2963
+ const hasSelectedAnnotation = !!(activeObject && isAnnotationObject(activeObject));
2964
+ const hasSelectedEditableObject = !!activeObject && isEditableOverlayObject(activeObject);
2965
+ const isDefaultTransform = this.currentScale === 1 &&
2966
+ this.currentRotation === 0 &&
2967
+ !((_a = this.originalImage) === null || _a === void 0 ? void 0 : _a.flipX) &&
2968
+ !((_b = this.originalImage) === null || _b === void 0 ? void 0 : _b.flipY);
1903
2969
  const canUndo = this.historyManager.canUndo();
1904
2970
  const canRedo = this.historyManager.canRedo();
1905
2971
  const isInCropMode = this.cropSession !== null;
1906
2972
  const isInMosaicMode = this.mosaicSession !== null;
2973
+ const isInTextMode = this.textSession !== null;
2974
+ const isInDrawMode = this.drawSession !== null;
1907
2975
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
1908
- const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
2976
+ const isMosaicApplying = ((_c = this.mosaicSession) === null || _c === void 0 ? void 0 : _c.isApplying) === true;
1909
2977
  if (isInCropMode) {
1910
2978
  CROP_MODE_CONTROL_KEYS.forEach((key) => {
1911
2979
  this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
1912
2980
  });
1913
2981
  return;
1914
2982
  }
2983
+ if (isInTextMode) {
2984
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
2985
+ this.setControlEnabled(key, !isBusy && TEXT_MODE_ENABLED_KEYS.includes(key));
2986
+ });
2987
+ return;
2988
+ }
2989
+ if (isInDrawMode) {
2990
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
2991
+ this.setControlEnabled(key, !isBusy && DRAW_MODE_ENABLED_KEYS.includes(key));
2992
+ });
2993
+ return;
2994
+ }
1915
2995
  if (isInMosaicMode) {
1916
2996
  MOSAIC_MODE_CONTROL_KEYS.forEach((key) => {
1917
2997
  this.setControlEnabled(key, !isBusy && !isMosaicApplying && MOSAIC_MODE_ENABLED_KEYS.includes(key));
@@ -1926,19 +3006,38 @@ export class ImageEditor {
1926
3006
  this.setControlEnabled('zoomOutButton', hasImage && !isBusy && this.currentScale > this.options.minScale);
1927
3007
  this.setControlEnabled('rotateLeftButton', hasImage && !isBusy);
1928
3008
  this.setControlEnabled('rotateRightButton', hasImage && !isBusy);
3009
+ this.setControlEnabled('flipHorizontalButton', hasImage && !isBusy);
3010
+ this.setControlEnabled('flipVerticalButton', hasImage && !isBusy);
1929
3011
  this.setControlEnabled('createMaskButton', hasImage && !isBusy);
1930
3012
  this.setControlEnabled('removeSelectedMaskButton', hasSelectedMask && !isBusy);
1931
3013
  this.setControlEnabled('removeAllMasksButton', hasMasks && !isBusy);
1932
3014
  this.setControlEnabled('mergeMasksButton', hasImage && hasMasks && !isBusy);
3015
+ this.setControlEnabled('removeSelectedAnnotationButton', hasSelectedAnnotation && !isBusy);
3016
+ this.setControlEnabled('removeAllAnnotationsButton', hasAnnotations && !isBusy);
3017
+ this.setControlEnabled('deleteSelectedObjectButton', hasSelectedEditableObject && !isBusy);
3018
+ this.setControlEnabled('mergeAnnotationsButton', hasImage && hasAnnotations && !isBusy);
3019
+ this.setControlEnabled('bringSelectedObjectForwardButton', hasSelectedEditableObject && !isBusy);
3020
+ this.setControlEnabled('sendSelectedObjectBackwardButton', hasSelectedEditableObject && !isBusy);
3021
+ this.setControlEnabled('bringSelectedObjectToFrontButton', hasSelectedEditableObject && !isBusy);
3022
+ this.setControlEnabled('sendSelectedObjectToBackButton', hasSelectedEditableObject && !isBusy);
1933
3023
  this.setControlEnabled('downloadImageButton', hasImage && !isBusy);
1934
3024
  this.setControlEnabled('resetImageTransformButton', hasImage && !isDefaultTransform && !isBusy);
1935
3025
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
1936
3026
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
1937
3027
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
3028
+ this.setControlEnabled('cropAspectRatioSelect', hasImage && !isBusy);
1938
3029
  this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
3030
+ this.setControlEnabled('enterTextModeButton', hasImage && !isBusy);
3031
+ this.setControlEnabled('enterDrawModeButton', hasImage && !isBusy);
1939
3032
  this.setControlEnabled('exitMosaicModeButton', false);
3033
+ this.setControlEnabled('exitTextModeButton', false);
3034
+ this.setControlEnabled('exitDrawModeButton', false);
1940
3035
  this.setControlEnabled('mosaicBrushSizeInput', !this.isDisposed);
1941
3036
  this.setControlEnabled('mosaicBlockSizeInput', !this.isDisposed);
3037
+ this.setControlEnabled('textColorInput', !this.isDisposed);
3038
+ this.setControlEnabled('textFontSizeInput', !this.isDisposed);
3039
+ this.setControlEnabled('drawColorInput', !this.isDisposed);
3040
+ this.setControlEnabled('drawBrushSizeInput', !this.isDisposed);
1942
3041
  this.setControlEnabled('imageInput', !isBusy);
1943
3042
  this.setControlEnabled('applyCropButton', false);
1944
3043
  this.setControlEnabled('cancelCropButton', false);
@@ -1953,7 +3052,10 @@ export class ImageEditor {
1953
3052
  return;
1954
3053
  this.recordElementOriginalState(key, controlElement);
1955
3054
  if ('disabled' in controlElement) {
1956
- controlElement.disabled = !isEnabled;
3055
+ const formControl = controlElement;
3056
+ const nextDisabled = !isEnabled;
3057
+ if (formControl.disabled !== nextDisabled)
3058
+ formControl.disabled = nextDisabled;
1957
3059
  return;
1958
3060
  }
1959
3061
  if (!isEnabled) {
@@ -2026,6 +3128,15 @@ export class ImageEditor {
2026
3128
  this.operationGuard.markDisposed();
2027
3129
  this.animQueue.clear();
2028
3130
  (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.removeAll();
3131
+ if (this.keyboardHandler && this.keyboardDocument) {
3132
+ try {
3133
+ this.keyboardDocument.removeEventListener('keydown', this.keyboardHandler);
3134
+ }
3135
+ catch {
3136
+ }
3137
+ }
3138
+ this.keyboardHandler = null;
3139
+ this.keyboardDocument = null;
2029
3140
  this.restoreElementOriginalStates();
2030
3141
  if (this.cropSession && this.canvas) {
2031
3142
  try {
@@ -2044,6 +3155,22 @@ export class ImageEditor {
2044
3155
  }
2045
3156
  this.mosaicSession = null;
2046
3157
  }
3158
+ if (this.textSession && this.canvas) {
3159
+ try {
3160
+ exitTextModeImpl(this.buildTextControllerContext());
3161
+ }
3162
+ catch {
3163
+ }
3164
+ this.textSession = null;
3165
+ }
3166
+ if (this.drawSession && this.canvas) {
3167
+ try {
3168
+ exitDrawModeImpl(this.buildDrawControllerContext());
3169
+ }
3170
+ catch {
3171
+ }
3172
+ this.drawSession = null;
3173
+ }
2047
3174
  if (this.canvas) {
2048
3175
  try {
2049
3176
  void Promise.resolve(this.canvas.dispose()).catch(() => {
@@ -2059,6 +3186,7 @@ export class ImageEditor {
2059
3186
  this.currentImageMimeType = null;
2060
3187
  this.lastMask = null;
2061
3188
  this.maskCounter = 0;
3189
+ this.annotationCounter = 0;
2062
3190
  this.currentScale = 1;
2063
3191
  this.currentRotation = 0;
2064
3192
  this.baseImageScale = 1;