@bensitu/image-editor 1.5.2 → 2.0.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 (154) hide show
  1. package/README.md +367 -518
  2. package/dist/cjs/index.cjs +5422 -0
  3. package/dist/cjs/index.cjs.map +1 -0
  4. package/dist/esm/animation/animation-queue.js +67 -0
  5. package/dist/esm/animation/animation-queue.js.map +1 -0
  6. package/dist/esm/core/callback-reporter.js +23 -0
  7. package/dist/esm/core/callback-reporter.js.map +1 -0
  8. package/dist/esm/core/default-options.js +322 -0
  9. package/dist/esm/core/default-options.js.map +1 -0
  10. package/dist/esm/core/errors.js +156 -0
  11. package/dist/esm/core/errors.js.map +1 -0
  12. package/dist/esm/core/operation-guard.js +129 -0
  13. package/dist/esm/core/operation-guard.js.map +1 -0
  14. package/dist/esm/core/public-types.js +4 -0
  15. package/dist/esm/core/public-types.js.map +1 -0
  16. package/dist/esm/core/state-serializer.js +251 -0
  17. package/dist/esm/core/state-serializer.js.map +1 -0
  18. package/dist/esm/crop/crop-controller.js +403 -0
  19. package/dist/esm/crop/crop-controller.js.map +1 -0
  20. package/dist/esm/export/export-format.js +53 -0
  21. package/dist/esm/export/export-format.js.map +1 -0
  22. package/dist/esm/export/export-service.js +596 -0
  23. package/dist/esm/export/export-service.js.map +1 -0
  24. package/dist/esm/fabric/fabric-adapter.js +37 -0
  25. package/dist/esm/fabric/fabric-adapter.js.map +1 -0
  26. package/dist/esm/fabric/fabric-animation.js +37 -0
  27. package/dist/esm/fabric/fabric-animation.js.map +1 -0
  28. package/dist/esm/history/command.js +2 -0
  29. package/dist/esm/history/command.js.map +1 -0
  30. package/dist/esm/history/history-manager.js +103 -0
  31. package/dist/esm/history/history-manager.js.map +1 -0
  32. package/dist/esm/image/image-loader.js +245 -0
  33. package/dist/esm/image/image-loader.js.map +1 -0
  34. package/dist/esm/image/image-resampler.js +55 -0
  35. package/dist/esm/image/image-resampler.js.map +1 -0
  36. package/dist/esm/image/layout-manager.js +224 -0
  37. package/dist/esm/image/layout-manager.js.map +1 -0
  38. package/dist/esm/image/transform-controller.js +132 -0
  39. package/dist/esm/image/transform-controller.js.map +1 -0
  40. package/dist/esm/image-editor.js +1740 -0
  41. package/dist/esm/image-editor.js.map +1 -0
  42. package/dist/esm/index.js +5 -0
  43. package/dist/esm/index.js.map +1 -0
  44. package/dist/esm/mask/mask-factory.js +332 -0
  45. package/dist/esm/mask/mask-factory.js.map +1 -0
  46. package/dist/esm/mask/mask-label-manager.js +120 -0
  47. package/dist/esm/mask/mask-label-manager.js.map +1 -0
  48. package/dist/esm/mask/mask-list.js +47 -0
  49. package/dist/esm/mask/mask-list.js.map +1 -0
  50. package/dist/esm/mask/mask-style.js +182 -0
  51. package/dist/esm/mask/mask-style.js.map +1 -0
  52. package/dist/esm/ui/dom-bindings.js +60 -0
  53. package/dist/esm/ui/dom-bindings.js.map +1 -0
  54. package/dist/esm/ui/ui-state.js +25 -0
  55. package/dist/esm/ui/ui-state.js.map +1 -0
  56. package/dist/esm/ui/visibility-state.js +11 -0
  57. package/dist/esm/ui/visibility-state.js.map +1 -0
  58. package/dist/esm/utils/canvas-region.js +100 -0
  59. package/dist/esm/utils/canvas-region.js.map +1 -0
  60. package/dist/esm/utils/dom.js +6 -0
  61. package/dist/esm/utils/dom.js.map +1 -0
  62. package/dist/esm/utils/file.js +53 -0
  63. package/dist/esm/utils/file.js.map +1 -0
  64. package/dist/esm/utils/number.js +24 -0
  65. package/dist/esm/utils/number.js.map +1 -0
  66. package/dist/esm/utils/timeout.js +17 -0
  67. package/dist/esm/utils/timeout.js.map +1 -0
  68. package/dist/types/animation/animation-queue.d.ts +111 -0
  69. package/dist/types/animation/animation-queue.d.ts.map +1 -0
  70. package/dist/types/core/callback-reporter.d.ts +125 -0
  71. package/dist/types/core/callback-reporter.d.ts.map +1 -0
  72. package/dist/types/core/default-options.d.ts +56 -0
  73. package/dist/types/core/default-options.d.ts.map +1 -0
  74. package/dist/types/core/errors.d.ts +142 -0
  75. package/dist/types/core/errors.d.ts.map +1 -0
  76. package/dist/types/core/operation-guard.d.ts +192 -0
  77. package/dist/types/core/operation-guard.d.ts.map +1 -0
  78. package/dist/types/core/public-types.d.ts +678 -0
  79. package/dist/types/core/public-types.d.ts.map +1 -0
  80. package/dist/types/core/state-serializer.d.ts +301 -0
  81. package/dist/types/core/state-serializer.d.ts.map +1 -0
  82. package/dist/types/crop/crop-controller.d.ts +407 -0
  83. package/dist/types/crop/crop-controller.d.ts.map +1 -0
  84. package/dist/types/export/export-format.d.ts +136 -0
  85. package/dist/types/export/export-format.d.ts.map +1 -0
  86. package/dist/types/export/export-service.d.ts +333 -0
  87. package/dist/types/export/export-service.d.ts.map +1 -0
  88. package/dist/types/fabric/fabric-adapter.d.ts +74 -0
  89. package/dist/types/fabric/fabric-adapter.d.ts.map +1 -0
  90. package/dist/types/fabric/fabric-animation.d.ts +141 -0
  91. package/dist/types/fabric/fabric-animation.d.ts.map +1 -0
  92. package/dist/types/history/command.d.ts +16 -0
  93. package/dist/types/history/command.d.ts.map +1 -0
  94. package/dist/types/history/history-manager.d.ts +129 -0
  95. package/dist/types/history/history-manager.d.ts.map +1 -0
  96. package/dist/types/image/image-loader.d.ts +265 -0
  97. package/dist/types/image/image-loader.d.ts.map +1 -0
  98. package/dist/types/image/image-resampler.d.ts +139 -0
  99. package/dist/types/image/image-resampler.d.ts.map +1 -0
  100. package/dist/types/image/layout-manager.d.ts +255 -0
  101. package/dist/types/image/layout-manager.d.ts.map +1 -0
  102. package/dist/types/image/transform-controller.d.ts +287 -0
  103. package/dist/types/image/transform-controller.d.ts.map +1 -0
  104. package/dist/types/image-editor.d.ts +650 -0
  105. package/dist/types/image-editor.d.ts.map +1 -0
  106. package/dist/types/index.d.cts +31 -0
  107. package/dist/types/index.d.cts.map +1 -0
  108. package/dist/types/index.d.ts +31 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/mask/mask-factory.d.ts +209 -0
  111. package/dist/types/mask/mask-factory.d.ts.map +1 -0
  112. package/dist/types/mask/mask-label-manager.d.ts +171 -0
  113. package/dist/types/mask/mask-label-manager.d.ts.map +1 -0
  114. package/dist/types/mask/mask-list.d.ts +144 -0
  115. package/dist/types/mask/mask-list.d.ts.map +1 -0
  116. package/dist/types/mask/mask-style.d.ts +338 -0
  117. package/dist/types/mask/mask-style.d.ts.map +1 -0
  118. package/dist/types/ui/dom-bindings.d.ts +103 -0
  119. package/dist/types/ui/dom-bindings.d.ts.map +1 -0
  120. package/dist/types/ui/ui-state.d.ts +112 -0
  121. package/dist/types/ui/ui-state.d.ts.map +1 -0
  122. package/dist/types/ui/visibility-state.d.ts +77 -0
  123. package/dist/types/ui/visibility-state.d.ts.map +1 -0
  124. package/dist/types/utils/canvas-region.d.ts +177 -0
  125. package/dist/types/utils/canvas-region.d.ts.map +1 -0
  126. package/dist/types/utils/dom.d.ts +26 -0
  127. package/dist/types/utils/dom.d.ts.map +1 -0
  128. package/dist/types/utils/file.d.ts +80 -0
  129. package/dist/types/utils/file.d.ts.map +1 -0
  130. package/dist/types/utils/number.d.ts +132 -0
  131. package/dist/types/utils/number.d.ts.map +1 -0
  132. package/dist/types/utils/timeout.d.ts +84 -0
  133. package/dist/types/utils/timeout.d.ts.map +1 -0
  134. package/dist/umd/image-editor.umd.js +2 -0
  135. package/dist/umd/image-editor.umd.js.map +1 -0
  136. package/package.json +72 -66
  137. package/dist/image-editor.cjs +0 -4407
  138. package/dist/image-editor.cjs.map +0 -7
  139. package/dist/image-editor.esm.js +0 -4376
  140. package/dist/image-editor.esm.js.map +0 -7
  141. package/dist/image-editor.esm.min.js +0 -9
  142. package/dist/image-editor.esm.min.js.map +0 -7
  143. package/dist/image-editor.esm.min.mjs +0 -9
  144. package/dist/image-editor.esm.min.mjs.map +0 -7
  145. package/dist/image-editor.esm.mjs +0 -4376
  146. package/dist/image-editor.esm.mjs.map +0 -7
  147. package/dist/image-editor.js +0 -4373
  148. package/dist/image-editor.js.map +0 -7
  149. package/dist/image-editor.min.js +0 -9
  150. package/dist/image-editor.min.js.map +0 -7
  151. package/image-editor.d.ts +0 -271
  152. package/src/browser.js +0 -11
  153. package/src/esm.js +0 -9
  154. package/src/image-editor.js +0 -5013
@@ -0,0 +1,1740 @@
1
+ import { AnimationQueue } from './animation/animation-queue.js';
2
+ import { reportError, reportWarning } from './core/callback-reporter.js';
3
+ import { resolveOptions } from './core/default-options.js';
4
+ import { OperationGuard } from './core/operation-guard.js';
5
+ import { loadFromState as loadFromStateImpl, saveState as saveStateImpl, } from './core/state-serializer.js';
6
+ import { Command, HistoryManager } from './history/history-manager.js';
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';
10
+ import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
11
+ import { loadImage as loadImageImpl } from './image/image-loader.js';
12
+ import { ViewportCache, applyCanvasDimensions, computeScrollableCanvasSize, detectLayoutConflict, measureScrollbarSize, } from './image/layout-manager.js';
13
+ import { TransformController } from './image/transform-controller.js';
14
+ import { createMask as createMaskImpl, removeAllMasks as removeAllMasksImpl, removeSelectedMask as removeSelectedMaskImpl, } from './mask/mask-factory.js';
15
+ import { createLabelForMask, hideAllMaskLabels, removeLabelForMask, showLabelForMask, syncMaskLabel, } from './mask/mask-label-manager.js';
16
+ import { renderMaskList, updateMaskListSelection } from './mask/mask-list.js';
17
+ import { applyMaskSelectedStyle, applyMaskUnselectedStyle, reattachMaskHoverHandlers, } from './mask/mask-style.js';
18
+ import { DomBindings } from './ui/dom-bindings.js';
19
+ import { setPlaceholderVisible as setPlaceholderVisibleImpl } from './ui/visibility-state.js';
20
+ import { inferImageMimeType, readFileAsDataUrl, resetFileInput } from './utils/file.js';
21
+ import { detectSourceMimeType } from './image/image-resampler.js';
22
+ const LAYOUT_EPSILON = 0.5;
23
+ const INTERNAL_OPERATION_TOKEN = Symbol.for('ImageEditorInternalOperation');
24
+ const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol.for('ImageEditorAllowDuringAnimationQueue');
25
+ const CROP_MODE_CONTROL_KEYS = [
26
+ 'scalePercentageInput',
27
+ 'rotateLeftDegreesInput',
28
+ 'rotateRightDegreesInput',
29
+ 'rotateLeftButton',
30
+ 'rotateRightButton',
31
+ 'createMaskButton',
32
+ 'removeSelectedMaskButton',
33
+ 'removeAllMasksButton',
34
+ 'mergeMasksButton',
35
+ 'downloadImageButton',
36
+ 'zoomInButton',
37
+ 'zoomOutButton',
38
+ 'resetImageTransformButton',
39
+ 'undoButton',
40
+ 'redoButton',
41
+ 'imageInput',
42
+ 'enterCropModeButton',
43
+ 'applyCropButton',
44
+ 'cancelCropButton',
45
+ ];
46
+ const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
47
+ const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
48
+ export class ImageEditor {
49
+ constructor(fabricModuleOrOptions = {}, options = {}) {
50
+ var _a;
51
+ Object.defineProperty(this, "fabricModule", {
52
+ enumerable: true,
53
+ configurable: true,
54
+ writable: true,
55
+ value: void 0
56
+ });
57
+ Object.defineProperty(this, "isFabricLoaded", {
58
+ enumerable: true,
59
+ configurable: true,
60
+ writable: true,
61
+ value: void 0
62
+ });
63
+ Object.defineProperty(this, "options", {
64
+ enumerable: true,
65
+ configurable: true,
66
+ writable: true,
67
+ value: void 0
68
+ });
69
+ Object.defineProperty(this, "canvas", {
70
+ enumerable: true,
71
+ configurable: true,
72
+ writable: true,
73
+ value: null
74
+ });
75
+ Object.defineProperty(this, "canvasElement", {
76
+ enumerable: true,
77
+ configurable: true,
78
+ writable: true,
79
+ value: null
80
+ });
81
+ Object.defineProperty(this, "containerElement", {
82
+ enumerable: true,
83
+ configurable: true,
84
+ writable: true,
85
+ value: null
86
+ });
87
+ Object.defineProperty(this, "placeholderElement", {
88
+ enumerable: true,
89
+ configurable: true,
90
+ writable: true,
91
+ value: null
92
+ });
93
+ Object.defineProperty(this, "elements", {
94
+ enumerable: true,
95
+ configurable: true,
96
+ writable: true,
97
+ value: {}
98
+ });
99
+ Object.defineProperty(this, "elementOriginalDisabledMap", {
100
+ enumerable: true,
101
+ configurable: true,
102
+ writable: true,
103
+ value: new Map()
104
+ });
105
+ Object.defineProperty(this, "elementOriginalAriaDisabledMap", {
106
+ enumerable: true,
107
+ configurable: true,
108
+ writable: true,
109
+ value: new Map()
110
+ });
111
+ Object.defineProperty(this, "elementOriginalPointerEventsMap", {
112
+ enumerable: true,
113
+ configurable: true,
114
+ writable: true,
115
+ value: new Map()
116
+ });
117
+ Object.defineProperty(this, "originalImage", {
118
+ enumerable: true,
119
+ configurable: true,
120
+ writable: true,
121
+ value: null
122
+ });
123
+ Object.defineProperty(this, "baseImageScale", {
124
+ enumerable: true,
125
+ configurable: true,
126
+ writable: true,
127
+ value: 1
128
+ });
129
+ Object.defineProperty(this, "currentScale", {
130
+ enumerable: true,
131
+ configurable: true,
132
+ writable: true,
133
+ value: 1
134
+ });
135
+ Object.defineProperty(this, "currentRotation", {
136
+ enumerable: true,
137
+ configurable: true,
138
+ writable: true,
139
+ value: 0
140
+ });
141
+ Object.defineProperty(this, "isImageLoadedToCanvas", {
142
+ enumerable: true,
143
+ configurable: true,
144
+ writable: true,
145
+ value: false
146
+ });
147
+ Object.defineProperty(this, "currentImageMimeType", {
148
+ enumerable: true,
149
+ configurable: true,
150
+ writable: true,
151
+ value: null
152
+ });
153
+ Object.defineProperty(this, "maskCounter", {
154
+ enumerable: true,
155
+ configurable: true,
156
+ writable: true,
157
+ value: 0
158
+ });
159
+ Object.defineProperty(this, "lastMask", {
160
+ enumerable: true,
161
+ configurable: true,
162
+ writable: true,
163
+ value: null
164
+ });
165
+ Object.defineProperty(this, "lastSnapshot", {
166
+ enumerable: true,
167
+ configurable: true,
168
+ writable: true,
169
+ value: null
170
+ });
171
+ Object.defineProperty(this, "historyManager", {
172
+ enumerable: true,
173
+ configurable: true,
174
+ writable: true,
175
+ value: void 0
176
+ });
177
+ Object.defineProperty(this, "operationGuard", {
178
+ enumerable: true,
179
+ configurable: true,
180
+ writable: true,
181
+ value: void 0
182
+ });
183
+ Object.defineProperty(this, "animQueue", {
184
+ enumerable: true,
185
+ configurable: true,
186
+ writable: true,
187
+ value: void 0
188
+ });
189
+ Object.defineProperty(this, "transformController", {
190
+ enumerable: true,
191
+ configurable: true,
192
+ writable: true,
193
+ value: null
194
+ });
195
+ Object.defineProperty(this, "viewportCache", {
196
+ enumerable: true,
197
+ configurable: true,
198
+ writable: true,
199
+ value: new ViewportCache()
200
+ });
201
+ Object.defineProperty(this, "cropSession", {
202
+ enumerable: true,
203
+ configurable: true,
204
+ writable: true,
205
+ value: null
206
+ });
207
+ Object.defineProperty(this, "domBindings", {
208
+ enumerable: true,
209
+ configurable: true,
210
+ writable: true,
211
+ value: null
212
+ });
213
+ Object.defineProperty(this, "isDisposed", {
214
+ enumerable: true,
215
+ configurable: true,
216
+ writable: true,
217
+ value: false
218
+ });
219
+ Object.defineProperty(this, "shouldSuppressSaveState", {
220
+ enumerable: true,
221
+ configurable: true,
222
+ writable: true,
223
+ value: false
224
+ });
225
+ Object.defineProperty(this, "lastEmittedIsBusy", {
226
+ enumerable: true,
227
+ configurable: true,
228
+ writable: true,
229
+ value: null
230
+ });
231
+ Object.defineProperty(this, "activeStateRestoreOperation", {
232
+ enumerable: true,
233
+ configurable: true,
234
+ writable: true,
235
+ value: null
236
+ });
237
+ Object.defineProperty(this, "nextSelectionChangeContext", {
238
+ enumerable: true,
239
+ configurable: true,
240
+ writable: true,
241
+ value: null
242
+ });
243
+ const detected = detectFabric(fabricModuleOrOptions, options);
244
+ this.fabricModule = (_a = detected.fabric) !== null && _a !== void 0 ? _a : {};
245
+ this.isFabricLoaded = detected.isFabricLoaded;
246
+ this.options = resolveOptions(detected.options);
247
+ const layoutConflict = detectLayoutConflict(this.options);
248
+ if (layoutConflict) {
249
+ reportWarning(this.options, null, layoutConflict.message);
250
+ }
251
+ this.operationGuard = new OperationGuard();
252
+ this.animQueue = new AnimationQueue();
253
+ this.historyManager = new HistoryManager(this.options.maxHistorySize);
254
+ }
255
+ init(idMap = {}) {
256
+ if (!this.isFabricLoaded) {
257
+ const globalFabric = globalThis.fabric;
258
+ if (!globalFabric ||
259
+ typeof globalFabric.Canvas !== 'function') {
260
+ return;
261
+ }
262
+ this.fabricModule = globalFabric;
263
+ this.isFabricLoaded = true;
264
+ }
265
+ if (this.isDisposed)
266
+ return;
267
+ const defaults = {
268
+ canvas: 'canvas',
269
+ canvasContainer: null,
270
+ imagePlaceholder: 'imagePlaceholder',
271
+ scalePercentageInput: 'scalePercentageInput',
272
+ rotateLeftDegreesInput: 'rotateLeftDegreesInput',
273
+ rotateRightDegreesInput: 'rotateRightDegreesInput',
274
+ rotateLeftButton: 'rotateLeftButton',
275
+ rotateRightButton: 'rotateRightButton',
276
+ createMaskButton: 'createMaskButton',
277
+ removeSelectedMaskButton: 'removeSelectedMaskButton',
278
+ removeAllMasksButton: 'removeAllMasksButton',
279
+ mergeMasksButton: 'mergeMasksButton',
280
+ downloadImageButton: 'downloadImageButton',
281
+ maskList: 'maskList',
282
+ zoomInButton: 'zoomInButton',
283
+ zoomOutButton: 'zoomOutButton',
284
+ resetImageTransformButton: 'resetImageTransformButton',
285
+ undoButton: 'undoButton',
286
+ redoButton: 'redoButton',
287
+ imageInput: 'imageInput',
288
+ enterCropModeButton: 'enterCropModeButton',
289
+ applyCropButton: 'applyCropButton',
290
+ cancelCropButton: 'cancelCropButton',
291
+ uploadArea: 'uploadArea',
292
+ };
293
+ this.elements = { ...defaults, ...idMap };
294
+ this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed);
295
+ this.initCanvas();
296
+ this.transformController = new TransformController(this.buildTransformContext());
297
+ this.bindDomEvents();
298
+ this.updateInputs();
299
+ this.updateMaskList();
300
+ this.updateUi();
301
+ if (this.options.initialImageBase64) {
302
+ void this.loadImage(this.options.initialImageBase64).catch(() => {
303
+ });
304
+ }
305
+ else {
306
+ this.updatePlaceholderStatus();
307
+ }
308
+ }
309
+ initCanvas() {
310
+ var _a;
311
+ const id = this.elements.canvas;
312
+ const canvasElement = id ? document.getElementById(id) : null;
313
+ if (!canvasElement)
314
+ throw new Error(`[ImageEditor] Canvas element not found: "${id}"`);
315
+ this.canvasElement = canvasElement;
316
+ const containerId = this.elements.canvasContainer;
317
+ if (containerId) {
318
+ this.containerElement =
319
+ (_a = document.getElementById(containerId)) !== null && _a !== void 0 ? _a : canvasElement.parentElement;
320
+ }
321
+ else {
322
+ this.containerElement = canvasElement.parentElement;
323
+ }
324
+ const placeholderId = this.elements.imagePlaceholder;
325
+ this.placeholderElement = placeholderId ? document.getElementById(placeholderId) : null;
326
+ let initialWidth = this.options.canvasWidth;
327
+ let initialHeight = this.options.canvasHeight;
328
+ if (this.containerElement) {
329
+ const containerWidth = Math.floor(this.containerElement.clientWidth);
330
+ const containerHeight = Math.floor(this.containerElement.clientHeight);
331
+ if (containerWidth > 0 && containerHeight > 0) {
332
+ initialWidth = containerWidth;
333
+ initialHeight = containerHeight;
334
+ }
335
+ }
336
+ this.canvas = new this.fabricModule.Canvas(canvasElement, {
337
+ width: initialWidth,
338
+ height: initialHeight,
339
+ backgroundColor: this.options.backgroundColor,
340
+ selection: this.options.groupSelection,
341
+ preserveObjectStacking: true,
342
+ });
343
+ this.canvas.on('selection:created', (e) => {
344
+ this.handleSelectionChanged(e.selected);
345
+ });
346
+ this.canvas.on('selection:updated', (e) => {
347
+ this.handleSelectionChanged(e.selected);
348
+ });
349
+ this.canvas.on('selection:cleared', () => this.handleSelectionChanged([]));
350
+ const onObjectEvent = (e) => {
351
+ if (e.target && isMaskObject(e.target))
352
+ this.syncMaskLabel(e.target);
353
+ };
354
+ const onObjectModified = (e) => {
355
+ if (!e.target || !isMaskObject(e.target))
356
+ return;
357
+ this.syncMaskLabel(e.target);
358
+ this.saveState();
359
+ };
360
+ this.canvas.on('object:moving', onObjectEvent);
361
+ this.canvas.on('object:scaling', onObjectEvent);
362
+ this.canvas.on('object:rotating', onObjectEvent);
363
+ this.canvas.on('object:modified', onObjectModified);
364
+ }
365
+ bindDomEvents() {
366
+ this.bindElementIfExists('uploadArea', 'click', () => {
367
+ var _a;
368
+ const inputId = this.elements.imageInput;
369
+ if (inputId)
370
+ (_a = document.getElementById(inputId)) === null || _a === void 0 ? void 0 : _a.click();
371
+ });
372
+ this.bindElementIfExists('imageInput', 'change', (e) => {
373
+ var _a;
374
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
375
+ if (file)
376
+ void this.loadImageFile(file);
377
+ });
378
+ this.bindElementIfExists('zoomInButton', 'click', () => {
379
+ void this.scaleImage(this.currentScale + this.options.scaleStep);
380
+ });
381
+ this.bindElementIfExists('zoomOutButton', 'click', () => {
382
+ void this.scaleImage(this.currentScale - this.options.scaleStep);
383
+ });
384
+ this.bindElementIfExists('resetImageTransformButton', 'click', () => {
385
+ void this.resetImageTransform();
386
+ });
387
+ this.bindElementIfExists('createMaskButton', 'click', () => {
388
+ this.createMask();
389
+ });
390
+ this.bindElementIfExists('removeSelectedMaskButton', 'click', () => {
391
+ this.removeSelectedMask();
392
+ });
393
+ this.bindElementIfExists('removeAllMasksButton', 'click', () => {
394
+ this.removeAllMasks();
395
+ });
396
+ this.bindElementIfExists('mergeMasksButton', 'click', () => {
397
+ void this.mergeMasks();
398
+ });
399
+ this.bindElementIfExists('downloadImageButton', 'click', () => {
400
+ this.downloadImage();
401
+ });
402
+ this.bindElementIfExists('undoButton', 'click', () => {
403
+ this.undo();
404
+ });
405
+ this.bindElementIfExists('redoButton', 'click', () => {
406
+ this.redo();
407
+ });
408
+ this.bindElementIfExists('rotateLeftButton', 'click', () => {
409
+ const inputId = this.elements.rotateLeftDegreesInput;
410
+ const inputEl = inputId
411
+ ? document.getElementById(inputId)
412
+ : null;
413
+ let step = this.options.rotationStep;
414
+ if (inputEl) {
415
+ const parsedStep = parseFloat(inputEl.value);
416
+ if (!isNaN(parsedStep))
417
+ step = parsedStep;
418
+ }
419
+ void this.rotateImage(this.currentRotation - step);
420
+ });
421
+ this.bindElementIfExists('rotateRightButton', 'click', () => {
422
+ const inputId = this.elements.rotateRightDegreesInput;
423
+ const inputEl = inputId
424
+ ? document.getElementById(inputId)
425
+ : null;
426
+ let step = this.options.rotationStep;
427
+ if (inputEl) {
428
+ const parsedStep = parseFloat(inputEl.value);
429
+ if (!isNaN(parsedStep))
430
+ step = parsedStep;
431
+ }
432
+ void this.rotateImage(this.currentRotation + step);
433
+ });
434
+ this.bindElementIfExists('enterCropModeButton', 'click', () => {
435
+ this.enterCropMode();
436
+ });
437
+ this.bindElementIfExists('applyCropButton', 'click', () => {
438
+ void this.applyCrop().catch((error) => {
439
+ reportError(this.options, error, 'Crop apply failed.');
440
+ });
441
+ });
442
+ this.bindElementIfExists('cancelCropButton', 'click', () => {
443
+ this.cancelCrop();
444
+ });
445
+ }
446
+ bindElementIfExists(key, event, handler) {
447
+ var _a;
448
+ (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.bindIfExists(key, event, handler);
449
+ }
450
+ async loadImageFile(file) {
451
+ const inputId = this.elements.imageInput;
452
+ const inputEl = inputId
453
+ ? document.getElementById(inputId)
454
+ : null;
455
+ const mime = inferImageMimeType(file);
456
+ if (!mime) {
457
+ reportWarning(this.options, null, `Unsupported image file type: ${file.type || file.name || 'unknown'}.`);
458
+ resetFileInput(inputEl);
459
+ return;
460
+ }
461
+ let dataUrl;
462
+ try {
463
+ dataUrl = await readFileAsDataUrl(file);
464
+ }
465
+ catch (error) {
466
+ reportError(this.options, error, 'Failed to read selected image file.');
467
+ resetFileInput(inputEl);
468
+ return;
469
+ }
470
+ try {
471
+ await this.loadImage(dataUrl);
472
+ }
473
+ catch {
474
+ }
475
+ finally {
476
+ resetFileInput(inputEl);
477
+ }
478
+ }
479
+ async loadImage(base64, options = {}) {
480
+ if (!this.isFabricLoaded || !this.canvas)
481
+ return;
482
+ if (this.isDisposed)
483
+ return;
484
+ if (typeof base64 !== 'string' || !base64.startsWith('data:image/'))
485
+ return;
486
+ if (!this.canRunIdleOperation('loadImage', options))
487
+ return;
488
+ const callbackContext = this.getOperationContext('loadImage', options);
489
+ const previousImage = this.originalImage;
490
+ const hadMasks = this.getMasks().length > 0;
491
+ this.emitOptionCallback('onImageLoadStart', [callbackContext]);
492
+ this.operationGuard.beginLoading();
493
+ this.emitBusyChangeIfChanged(callbackContext);
494
+ this.updateUi();
495
+ this.hideAllMaskLabels();
496
+ const loadImageContext = {
497
+ fabric: this.fabricModule,
498
+ canvas: this.canvas,
499
+ options: this.options,
500
+ containerElement: this.containerElement,
501
+ placeholderElement: this.placeholderElement,
502
+ viewportCache: this.viewportCache,
503
+ getOriginalImage: () => this.originalImage,
504
+ setOriginalImage: (v) => {
505
+ this.originalImage = v;
506
+ },
507
+ getIsImageLoadedToCanvas: () => this.isImageLoadedToCanvas,
508
+ setIsImageLoadedToCanvas: (v) => {
509
+ this.isImageLoadedToCanvas = v;
510
+ },
511
+ getLastSnapshot: () => this.lastSnapshot,
512
+ setLastSnapshot: (v) => {
513
+ this.lastSnapshot = v;
514
+ },
515
+ getMaskCounter: () => this.maskCounter,
516
+ setMaskCounter: (v) => {
517
+ this.maskCounter = v;
518
+ },
519
+ getCurrentScale: () => this.currentScale,
520
+ setCurrentScale: (v) => {
521
+ this.currentScale = v;
522
+ },
523
+ getCurrentRotation: () => this.currentRotation,
524
+ setCurrentRotation: (v) => {
525
+ this.currentRotation = v;
526
+ },
527
+ getBaseImageScale: () => this.baseImageScale,
528
+ setBaseImageScale: (v) => {
529
+ this.baseImageScale = v;
530
+ },
531
+ getCurrentImageMimeType: () => this.currentImageMimeType,
532
+ setCurrentImageMimeType: (v) => {
533
+ this.currentImageMimeType = v;
534
+ },
535
+ setPlaceholderVisible: (show) => {
536
+ setPlaceholderVisibleImpl(this.placeholderElement, this.containerElement, this.options.showPlaceholder ? show : false);
537
+ },
538
+ };
539
+ try {
540
+ await loadImageImpl(loadImageContext, base64, options);
541
+ }
542
+ finally {
543
+ this.operationGuard.endLoading();
544
+ this.emitBusyChangeIfChanged(callbackContext);
545
+ if (!this.isDisposed && this.canvas)
546
+ this.updateUi();
547
+ }
548
+ this.lastMask = null;
549
+ this.updateInputs();
550
+ this.updateMaskList();
551
+ this.updateUi();
552
+ if (previousImage && previousImage !== this.originalImage) {
553
+ this.emitOptionCallback('onImageCleared', [previousImage, callbackContext]);
554
+ }
555
+ const imageInfo = this.getImageInfo();
556
+ if (imageInfo) {
557
+ this.emitOptionCallback('onImageLoaded', [imageInfo, callbackContext]);
558
+ }
559
+ if (hadMasks) {
560
+ this.emitMasksChanged(callbackContext);
561
+ }
562
+ this.emitImageChanged(callbackContext);
563
+ }
564
+ getInternalOperationToken(options) {
565
+ var _a;
566
+ return ((_a = options === null || options === void 0 ? void 0 : options[INTERNAL_OPERATION_TOKEN]) !== null && _a !== void 0 ? _a : null);
567
+ }
568
+ canRunDuringAnimationQueue(options) {
569
+ return !!(options === null || options === void 0 ? void 0 : options[INTERNAL_ALLOW_DURING_ANIMATION_QUEUE]);
570
+ }
571
+ withInternalOperationOptions(token, options = {}) {
572
+ return {
573
+ ...options,
574
+ ...(token ? { [INTERNAL_OPERATION_TOKEN]: token } : {}),
575
+ };
576
+ }
577
+ withAnimationQueueBypass(options = {}) {
578
+ return {
579
+ ...options,
580
+ [INTERNAL_ALLOW_DURING_ANIMATION_QUEUE]: true,
581
+ };
582
+ }
583
+ assertIdleForOperation(operationName, options) {
584
+ const token = this.getInternalOperationToken(options);
585
+ this.operationGuard.assertIdleForOperation(operationName, token);
586
+ if (this.cropSession &&
587
+ !this.operationGuard.isOwnOperation(token) &&
588
+ !CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
589
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
590
+ }
591
+ if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
592
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
593
+ }
594
+ }
595
+ canRunIdleOperation(operationName, options) {
596
+ try {
597
+ this.assertIdleForOperation(operationName, options);
598
+ return true;
599
+ }
600
+ catch {
601
+ return false;
602
+ }
603
+ }
604
+ assertCanQueueAnimation(operationName, options) {
605
+ this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
606
+ }
607
+ isImageLoaded() {
608
+ var _a, _b;
609
+ return !!(this.originalImage &&
610
+ this.originalImage instanceof this.fabricModule.FabricImage &&
611
+ ((_a = this.originalImage.width) !== null && _a !== void 0 ? _a : 0) > 0 &&
612
+ ((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
613
+ }
614
+ isBusy() {
615
+ return this.operationGuard.isBusy() || this.animQueue.isBusy() || this.cropSession !== null;
616
+ }
617
+ setLayoutMode(mode) {
618
+ if (mode !== 'fit' && mode !== 'cover' && mode !== 'expand') {
619
+ reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported layout mode ${JSON.stringify(mode)}. ` +
620
+ 'Expected "fit", "cover", or "expand".'), 'Ignored invalid layout mode.');
621
+ return;
622
+ }
623
+ this.options.fitImageToCanvas = mode === 'fit';
624
+ this.options.coverImageToCanvas = mode === 'cover';
625
+ this.options.expandCanvasToImage = mode === 'expand';
626
+ }
627
+ buildCallbackContext(operation, isInternalOperation = false) {
628
+ return { operation, isInternalOperation };
629
+ }
630
+ getOperationContext(fallback, options) {
631
+ const internal = this.getInternalOperationToken(options);
632
+ const activeOperation = this.operationGuard.activeOperationName();
633
+ if (internal && activeOperation) {
634
+ return this.buildCallbackContext(activeOperation, true);
635
+ }
636
+ return this.buildCallbackContext(fallback, false);
637
+ }
638
+ emitOptionCallback(callbackName, args) {
639
+ const callback = this.options[callbackName];
640
+ if (typeof callback !== 'function')
641
+ return;
642
+ try {
643
+ callback(...args);
644
+ }
645
+ catch (error) {
646
+ console.error(`[ImageEditor] ${callbackName} callback threw`, error);
647
+ }
648
+ }
649
+ getImageInfo() {
650
+ if (!this.canvas || !this.originalImage)
651
+ return null;
652
+ const canvasWidth = this.canvas.getWidth();
653
+ const canvasHeight = this.canvas.getHeight();
654
+ let displayWidth;
655
+ let displayHeight;
656
+ try {
657
+ this.originalImage.setCoords();
658
+ const bounds = this.originalImage.getBoundingRect();
659
+ displayWidth = Math.max(0, Number(bounds.width) || 0);
660
+ displayHeight = Math.max(0, Number(bounds.height) || 0);
661
+ }
662
+ catch {
663
+ displayWidth = Math.max(0, (Number(this.originalImage.width) || 0) *
664
+ Math.abs(Number(this.originalImage.scaleX) || 1));
665
+ displayHeight = Math.max(0, (Number(this.originalImage.height) || 0) *
666
+ Math.abs(Number(this.originalImage.scaleY) || 1));
667
+ }
668
+ return {
669
+ width: Math.max(0, Number(this.originalImage.width) || 0),
670
+ height: Math.max(0, Number(this.originalImage.height) || 0),
671
+ displayWidth,
672
+ displayHeight,
673
+ scale: this.currentScale,
674
+ rotation: this.currentRotation,
675
+ canvasWidth,
676
+ canvasHeight,
677
+ };
678
+ }
679
+ getMasks() {
680
+ if (!this.canvas)
681
+ return [];
682
+ return this.canvas.getObjects().filter(isMaskObject).slice();
683
+ }
684
+ getMaskCollectionSignature() {
685
+ return this.getMasks()
686
+ .map((mask) => `${mask.maskId}:${mask.maskName}`)
687
+ .join('|');
688
+ }
689
+ getEditorState() {
690
+ const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
691
+ const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
692
+ const image = this.getImageInfo();
693
+ return {
694
+ hasImage: image !== null,
695
+ image,
696
+ maskCount: this.getMasks().length,
697
+ currentScale: this.currentScale,
698
+ currentRotation: this.currentRotation,
699
+ isBusy: this.isBusy(),
700
+ isCropMode: this.cropSession !== null,
701
+ canUndo: this.historyManager.canUndo(),
702
+ canRedo: this.historyManager.canRedo(),
703
+ canvasWidth,
704
+ canvasHeight,
705
+ };
706
+ }
707
+ emitImageChanged(context) {
708
+ this.emitOptionCallback('onImageChanged', [this.getEditorState(), context]);
709
+ }
710
+ emitMasksChanged(context) {
711
+ this.emitOptionCallback('onMasksChanged', [this.getMasks(), context]);
712
+ }
713
+ emitBusyChangeIfChanged(context) {
714
+ const isBusy = this.isBusy();
715
+ if (this.lastEmittedIsBusy === isBusy)
716
+ return;
717
+ this.lastEmittedIsBusy = isBusy;
718
+ this.emitOptionCallback('onBusyChange', [isBusy, context]);
719
+ }
720
+ buildSelection(selected) {
721
+ var _a;
722
+ const selectedMasks = selected.filter(isMaskObject);
723
+ return {
724
+ selectedMask: (_a = selectedMasks[0]) !== null && _a !== void 0 ? _a : null,
725
+ selectedMasks,
726
+ };
727
+ }
728
+ withSelectionChangeContext(context, callback) {
729
+ const previous = this.nextSelectionChangeContext;
730
+ this.nextSelectionChangeContext = context;
731
+ try {
732
+ return callback();
733
+ }
734
+ finally {
735
+ this.nextSelectionChangeContext = previous;
736
+ }
737
+ }
738
+ isSupportedImageMimeType(mimeType) {
739
+ return mimeType === 'image/jpeg' || mimeType === 'image/png' || mimeType === 'image/webp';
740
+ }
741
+ inferCurrentImageMimeType() {
742
+ const image = this.originalImage;
743
+ if (!image)
744
+ return null;
745
+ let source = null;
746
+ try {
747
+ if (typeof image.getSrc === 'function')
748
+ source = image.getSrc();
749
+ else if (typeof image.src === 'string')
750
+ source = image.src;
751
+ }
752
+ catch {
753
+ source = null;
754
+ }
755
+ const mimeType = source ? detectSourceMimeType(source) : null;
756
+ return this.isSupportedImageMimeType(mimeType) ? mimeType : null;
757
+ }
758
+ setCanvasSizePx(widthPx, heightPx) {
759
+ if (!this.canvas)
760
+ return;
761
+ applyCanvasDimensions(this.canvas, widthPx, heightPx, this.containerElement);
762
+ }
763
+ alignObjectBoundingBoxToCanvasTopLeft(object) {
764
+ var _a, _b;
765
+ object.setCoords();
766
+ const boundingRect = object.getBoundingRect();
767
+ object.set({
768
+ left: ((_a = object.left) !== null && _a !== void 0 ? _a : 0) - boundingRect.left,
769
+ top: ((_b = object.top) !== null && _b !== void 0 ? _b : 0) - boundingRect.top,
770
+ });
771
+ object.setCoords();
772
+ this.canvas.renderAll();
773
+ }
774
+ measureLayoutViewport(scrollbarSize) {
775
+ return this.viewportCache.measure(this.containerElement, {
776
+ width: this.options.canvasWidth,
777
+ height: this.options.canvasHeight,
778
+ }, scrollbarSize);
779
+ }
780
+ updateCanvasSizeToImageBounds() {
781
+ var _a, _b;
782
+ if (!this.originalImage)
783
+ return;
784
+ this.originalImage.setCoords();
785
+ const boundingRect = this.originalImage.getBoundingRect();
786
+ const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
787
+ const viewport = this.measureLayoutViewport(scrollbarSize);
788
+ if (this.options.fitImageToCanvas || this.options.coverImageToCanvas) {
789
+ const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
790
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
791
+ return;
792
+ }
793
+ if (boundingRect.width <= viewport.width && boundingRect.height <= viewport.height) {
794
+ this.setCanvasSizePx(viewport.width, viewport.height);
795
+ return;
796
+ }
797
+ this.setCanvasSizePx(Math.max(viewport.width, Math.ceil(boundingRect.width)), Math.max(viewport.height, Math.ceil(boundingRect.height)));
798
+ }
799
+ shouldNormalizeCanvasSizeAfterStateRestore() {
800
+ var _a, _b;
801
+ if (!this.canvas || !this.originalImage)
802
+ return false;
803
+ this.originalImage.setCoords();
804
+ const boundingRect = this.originalImage.getBoundingRect();
805
+ const viewport = this.measureLayoutViewport(measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null));
806
+ const canvasW = Math.ceil(this.canvas.getWidth());
807
+ const canvasH = Math.ceil(this.canvas.getHeight());
808
+ const clipsImage = boundingRect.width > canvasW + LAYOUT_EPSILON ||
809
+ boundingRect.height > canvasH + LAYOUT_EPSILON;
810
+ if (this.options.fitImageToCanvas || this.options.coverImageToCanvas) {
811
+ const staleOverflowWidth = canvasW > viewport.width + LAYOUT_EPSILON &&
812
+ boundingRect.width <= viewport.width + LAYOUT_EPSILON;
813
+ const staleOverflowHeight = canvasH > viewport.height + LAYOUT_EPSILON &&
814
+ boundingRect.height <= viewport.height + LAYOUT_EPSILON;
815
+ return clipsImage || staleOverflowWidth || staleOverflowHeight;
816
+ }
817
+ if (this.options.expandCanvasToImage) {
818
+ const expectedW = Math.max(viewport.width, Math.ceil(boundingRect.width));
819
+ const expectedH = Math.max(viewport.height, Math.ceil(boundingRect.height));
820
+ return (Math.abs(canvasW - expectedW) > LAYOUT_EPSILON ||
821
+ Math.abs(canvasH - expectedH) > LAYOUT_EPSILON);
822
+ }
823
+ return clipsImage;
824
+ }
825
+ captureImageDisplayGeometry() {
826
+ if (!this.canvas || !this.originalImage)
827
+ return null;
828
+ this.originalImage.setCoords();
829
+ const boundingRect = this.originalImage.getBoundingRect();
830
+ return {
831
+ canvasWidth: this.canvas.getWidth(),
832
+ canvasHeight: this.canvas.getHeight(),
833
+ imageDisplayWidth: Math.max(1, boundingRect.width),
834
+ imageDisplayHeight: Math.max(1, boundingRect.height),
835
+ };
836
+ }
837
+ restoreMergedImageDisplayGeometry(geometry) {
838
+ if (!geometry || !this.canvas || !this.originalImage)
839
+ return;
840
+ this.setCanvasSizePx(geometry.canvasWidth, geometry.canvasHeight);
841
+ const sourceW = Math.max(1, this.originalImage.width || geometry.imageDisplayWidth);
842
+ const sourceH = Math.max(1, this.originalImage.height || geometry.imageDisplayHeight);
843
+ const scale = Math.min(geometry.imageDisplayWidth / sourceW, geometry.imageDisplayHeight / sourceH);
844
+ this.originalImage.set({
845
+ left: 0,
846
+ top: 0,
847
+ angle: 0,
848
+ scaleX: scale,
849
+ scaleY: scale,
850
+ originX: 'left',
851
+ originY: 'top',
852
+ selectable: false,
853
+ evented: false,
854
+ hasControls: false,
855
+ hoverCursor: 'default',
856
+ });
857
+ this.originalImage.setCoords();
858
+ this.canvas.sendObjectToBack(this.originalImage);
859
+ this.currentScale = 1;
860
+ this.currentRotation = 0;
861
+ this.baseImageScale = scale;
862
+ this.lastSnapshot = this.captureSnapshotInternal();
863
+ this.canvas.renderAll();
864
+ }
865
+ buildTransformContext() {
866
+ return {
867
+ canvas: this.canvas,
868
+ options: this.options,
869
+ guard: this.operationGuard,
870
+ getOriginalImage: () => this.originalImage,
871
+ getCurrentScale: () => this.currentScale,
872
+ setCurrentScale: (n) => {
873
+ this.currentScale = n;
874
+ },
875
+ getCurrentRotation: () => this.currentRotation,
876
+ setCurrentRotation: (n) => {
877
+ this.currentRotation = n;
878
+ },
879
+ getBaseImageScale: () => this.baseImageScale,
880
+ saveCanvasState: () => {
881
+ this.saveStateInternal(this.withAnimationQueueBypass());
882
+ },
883
+ setSuppressSaveState: (suppress) => {
884
+ this.shouldSuppressSaveState = suppress;
885
+ },
886
+ afterTransformSnap: () => {
887
+ if (this.isDisposed || !this.canvas || !this.originalImage)
888
+ return;
889
+ if (this.options.expandCanvasToImage ||
890
+ this.options.coverImageToCanvas ||
891
+ this.options.fitImageToCanvas) {
892
+ this.updateCanvasSizeToImageBounds();
893
+ }
894
+ this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
895
+ this.canvas
896
+ .getObjects()
897
+ .filter(isMaskObject)
898
+ .forEach((maskObject) => this.syncMaskLabel(maskObject));
899
+ },
900
+ };
901
+ }
902
+ scaleImage(factor) {
903
+ if (this.isDisposed || !this.transformController)
904
+ return Promise.resolve();
905
+ if (!Number.isFinite(factor))
906
+ return Promise.resolve();
907
+ try {
908
+ this.assertCanQueueAnimation('scaleImage');
909
+ }
910
+ catch (error) {
911
+ return Promise.reject(error);
912
+ }
913
+ const controller = this.transformController;
914
+ const context = this.buildCallbackContext('scaleImage', false);
915
+ const job = this.animQueue.add(async () => {
916
+ if (this.isDisposed)
917
+ return;
918
+ this.updateUi();
919
+ try {
920
+ await controller.scaleImage(factor);
921
+ if (!this.isDisposed)
922
+ this.emitImageChanged(context);
923
+ }
924
+ finally {
925
+ if (!this.isDisposed) {
926
+ this.updateInputs();
927
+ }
928
+ }
929
+ });
930
+ this.emitBusyChangeIfChanged(context);
931
+ return job.finally(() => {
932
+ this.refreshUiAfterQueuedAnimation();
933
+ this.emitBusyChangeIfChanged(context);
934
+ });
935
+ }
936
+ rotateImage(degrees) {
937
+ if (this.isDisposed || !this.transformController)
938
+ return Promise.resolve();
939
+ if (!Number.isFinite(degrees))
940
+ return Promise.resolve();
941
+ try {
942
+ this.assertCanQueueAnimation('rotateImage');
943
+ }
944
+ catch (error) {
945
+ return Promise.reject(error);
946
+ }
947
+ const controller = this.transformController;
948
+ const context = this.buildCallbackContext('rotateImage', false);
949
+ const job = this.animQueue.add(async () => {
950
+ if (this.isDisposed)
951
+ return;
952
+ this.updateUi();
953
+ try {
954
+ await controller.rotateImage(degrees);
955
+ if (!this.isDisposed)
956
+ this.emitImageChanged(context);
957
+ }
958
+ finally {
959
+ if (!this.isDisposed) {
960
+ this.updateInputs();
961
+ }
962
+ }
963
+ });
964
+ this.emitBusyChangeIfChanged(context);
965
+ return job.finally(() => {
966
+ this.refreshUiAfterQueuedAnimation();
967
+ this.emitBusyChangeIfChanged(context);
968
+ });
969
+ }
970
+ resetImageTransform() {
971
+ if (this.isDisposed || !this.transformController)
972
+ return Promise.resolve();
973
+ try {
974
+ this.assertCanQueueAnimation('resetImageTransform');
975
+ }
976
+ catch (error) {
977
+ return Promise.reject(error);
978
+ }
979
+ const controller = this.transformController;
980
+ const context = this.buildCallbackContext('resetImageTransform', false);
981
+ const job = this.animQueue.add(async () => {
982
+ if (this.isDisposed)
983
+ return;
984
+ this.updateUi();
985
+ try {
986
+ await controller.resetImageTransform();
987
+ if (!this.isDisposed)
988
+ this.emitImageChanged(context);
989
+ }
990
+ finally {
991
+ if (!this.isDisposed) {
992
+ this.updateInputs();
993
+ }
994
+ }
995
+ });
996
+ this.emitBusyChangeIfChanged(context);
997
+ return job.finally(() => {
998
+ this.refreshUiAfterQueuedAnimation();
999
+ this.emitBusyChangeIfChanged(context);
1000
+ });
1001
+ }
1002
+ refreshUiAfterQueuedAnimation() {
1003
+ if (this.isDisposed || !this.canvas)
1004
+ return;
1005
+ this.updateInputs();
1006
+ this.updateUi();
1007
+ }
1008
+ async loadFromState(jsonString) {
1009
+ return this.loadFromStateInternal(jsonString);
1010
+ }
1011
+ async loadFromStateInternal(jsonString, options) {
1012
+ var _a;
1013
+ if (!jsonString || !this.canvas)
1014
+ return;
1015
+ if (this.isDisposed)
1016
+ return;
1017
+ if (!this.canRunIdleOperation('loadFromState', options))
1018
+ return;
1019
+ const activeRestoreOperation = this.activeStateRestoreOperation;
1020
+ const context = this.buildCallbackContext(activeRestoreOperation !== null && activeRestoreOperation !== void 0 ? activeRestoreOperation : 'loadFromState', activeRestoreOperation === 'undo' || activeRestoreOperation === 'redo');
1021
+ const previousImage = this.originalImage;
1022
+ const previousMaskSignature = this.getMaskCollectionSignature();
1023
+ try {
1024
+ const restoredState = await loadFromStateImpl({
1025
+ canvas: this.canvas,
1026
+ jsonString,
1027
+ setCanvasSize: (widthPx, heightPx) => this.setCanvasSizePx(widthPx, heightPx),
1028
+ });
1029
+ if (this.isDisposed || !this.canvas)
1030
+ return;
1031
+ this.hideAllMaskLabels();
1032
+ this.originalImage = restoredState.originalImage;
1033
+ if (this.originalImage) {
1034
+ this.originalImage.set({
1035
+ originX: 'left',
1036
+ originY: 'top',
1037
+ selectable: false,
1038
+ evented: false,
1039
+ hasControls: false,
1040
+ hoverCursor: 'default',
1041
+ });
1042
+ this.canvas.sendObjectToBack(this.originalImage);
1043
+ }
1044
+ this.maskCounter = restoredState.maxMaskId;
1045
+ const editorState = restoredState.editorState;
1046
+ if (editorState) {
1047
+ this.currentScale = editorState.currentScale;
1048
+ this.currentRotation = editorState.currentRotation;
1049
+ this.baseImageScale = editorState.baseImageScale;
1050
+ }
1051
+ if (this.originalImage) {
1052
+ this.currentImageMimeType =
1053
+ editorState && 'currentImageMimeType' in editorState
1054
+ ? ((_a = editorState.currentImageMimeType) !== null && _a !== void 0 ? _a : null)
1055
+ : this.inferCurrentImageMimeType();
1056
+ }
1057
+ else {
1058
+ this.currentImageMimeType = null;
1059
+ }
1060
+ this.isImageLoadedToCanvas = !!this.originalImage;
1061
+ if (this.originalImage &&
1062
+ (this.options.expandCanvasToImage ||
1063
+ this.options.coverImageToCanvas ||
1064
+ this.options.fitImageToCanvas) &&
1065
+ this.shouldNormalizeCanvasSizeAfterStateRestore()) {
1066
+ this.updateCanvasSizeToImageBounds();
1067
+ this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
1068
+ }
1069
+ const restoredMasks = restoredState.objects.filter(isMaskObject);
1070
+ this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
1071
+ restoredMasks.forEach((maskObject) => {
1072
+ applyMaskUnselectedStyle(maskObject);
1073
+ reattachMaskHoverHandlers(maskObject);
1074
+ });
1075
+ this.lastSnapshot = this.captureSnapshotInternal();
1076
+ this.canvas.renderAll();
1077
+ this.updateInputs();
1078
+ this.updateMaskList();
1079
+ this.updateUi();
1080
+ if (previousImage && previousImage !== this.originalImage) {
1081
+ this.emitOptionCallback('onImageCleared', [previousImage, context]);
1082
+ }
1083
+ if (previousMaskSignature !== this.getMaskCollectionSignature()) {
1084
+ this.emitMasksChanged(context);
1085
+ }
1086
+ this.emitImageChanged(context);
1087
+ const activeMaskId = editorState === null || editorState === void 0 ? void 0 : editorState.activeMaskId;
1088
+ if (typeof activeMaskId === 'number') {
1089
+ const activeMask = restoredMasks.find((maskObject) => maskObject.maskId === activeMaskId);
1090
+ if (activeMask) {
1091
+ this.withSelectionChangeContext(context, () => {
1092
+ this.canvas.setActiveObject(activeMask);
1093
+ this.handleSelectionChanged([activeMask]);
1094
+ });
1095
+ }
1096
+ }
1097
+ }
1098
+ catch (error) {
1099
+ reportError(this.options, error, 'Failed to restore canvas state.');
1100
+ throw error;
1101
+ }
1102
+ }
1103
+ saveState() {
1104
+ this.saveStateInternal();
1105
+ }
1106
+ saveStateInternal(options) {
1107
+ var _a, _b;
1108
+ if (!this.canvas || this.shouldSuppressSaveState)
1109
+ return;
1110
+ if (!this.canRunIdleOperation('saveState', options))
1111
+ return;
1112
+ const activeObj = this.canvas.getActiveObject();
1113
+ const activeMask = this.getActiveMaskForSnapshot();
1114
+ this.hideAllMaskLabels();
1115
+ try {
1116
+ const after = saveStateImpl({
1117
+ canvas: this.canvas,
1118
+ activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
1119
+ currentScale: this.currentScale,
1120
+ currentRotation: this.currentRotation,
1121
+ baseImageScale: this.baseImageScale,
1122
+ currentImageMimeType: this.currentImageMimeType,
1123
+ });
1124
+ const before = (_b = this.lastSnapshot) !== null && _b !== void 0 ? _b : after;
1125
+ if (after === before) {
1126
+ return;
1127
+ }
1128
+ let executedOnce = false;
1129
+ const cmd = new Command(async () => {
1130
+ if (executedOnce) {
1131
+ await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
1132
+ }
1133
+ executedOnce = true;
1134
+ }, async () => {
1135
+ await this.loadFromStateInternal(before, this.withAnimationQueueBypass());
1136
+ });
1137
+ this.historyManager.execute(cmd);
1138
+ this.lastSnapshot = after;
1139
+ }
1140
+ catch (error) {
1141
+ reportWarning(this.options, error, 'Failed to capture canvas snapshot.');
1142
+ }
1143
+ finally {
1144
+ this.restoreActiveMaskAfterSnapshot(activeObj, activeMask);
1145
+ this.updateUi();
1146
+ }
1147
+ }
1148
+ restoreActiveMaskAfterSnapshot(activeObj, activeMask) {
1149
+ if (!this.canvas)
1150
+ return;
1151
+ const maskToRestore = activeObj && isMaskObject(activeObj) ? activeObj : activeMask;
1152
+ if (!maskToRestore || !this.canvas.getObjects().includes(maskToRestore))
1153
+ return;
1154
+ this.canvas.setActiveObject(maskToRestore);
1155
+ this.showLabelForMask(maskToRestore);
1156
+ this.updateMaskListSelection(maskToRestore);
1157
+ }
1158
+ undo() {
1159
+ if (this.isDisposed)
1160
+ return Promise.resolve();
1161
+ if (!this.canRunIdleOperation('undo'))
1162
+ return Promise.resolve();
1163
+ const context = this.buildCallbackContext('undo', true);
1164
+ const job = this.animQueue.add(async () => {
1165
+ if (this.isDisposed)
1166
+ return;
1167
+ this.activeStateRestoreOperation = 'undo';
1168
+ try {
1169
+ await this.historyManager.undo();
1170
+ }
1171
+ finally {
1172
+ this.activeStateRestoreOperation = null;
1173
+ }
1174
+ });
1175
+ this.emitBusyChangeIfChanged(context);
1176
+ return job.finally(() => {
1177
+ this.refreshUiAfterQueuedAnimation();
1178
+ this.emitBusyChangeIfChanged(context);
1179
+ });
1180
+ }
1181
+ redo() {
1182
+ if (this.isDisposed)
1183
+ return Promise.resolve();
1184
+ if (!this.canRunIdleOperation('redo'))
1185
+ return Promise.resolve();
1186
+ const context = this.buildCallbackContext('redo', true);
1187
+ const job = this.animQueue.add(async () => {
1188
+ if (this.isDisposed)
1189
+ return;
1190
+ this.activeStateRestoreOperation = 'redo';
1191
+ try {
1192
+ await this.historyManager.redo();
1193
+ }
1194
+ finally {
1195
+ this.activeStateRestoreOperation = null;
1196
+ }
1197
+ });
1198
+ this.emitBusyChangeIfChanged(context);
1199
+ return job.finally(() => {
1200
+ this.refreshUiAfterQueuedAnimation();
1201
+ this.emitBusyChangeIfChanged(context);
1202
+ });
1203
+ }
1204
+ createMask(config = {}) {
1205
+ if (!this.canvas)
1206
+ return null;
1207
+ if (!this.canRunIdleOperation('createMask'))
1208
+ return null;
1209
+ const callbackContext = this.buildCallbackContext('createMask', false);
1210
+ const createMaskContext = this.buildCreateMaskContext();
1211
+ const mask = this.withSelectionChangeContext(callbackContext, () => createMaskImpl(createMaskContext, config));
1212
+ if (mask) {
1213
+ this.emitMasksChanged(callbackContext);
1214
+ this.emitImageChanged(callbackContext);
1215
+ }
1216
+ return mask;
1217
+ }
1218
+ removeSelectedMask() {
1219
+ if (!this.canvas)
1220
+ return;
1221
+ if (!this.canRunIdleOperation('removeSelectedMask'))
1222
+ return;
1223
+ const before = this.getMasks().length;
1224
+ const callbackContext = this.buildCallbackContext('removeSelectedMask', false);
1225
+ const removeMaskContext = this.buildRemoveMaskContext();
1226
+ this.withSelectionChangeContext(callbackContext, () => removeSelectedMaskImpl(removeMaskContext));
1227
+ this.updateUi();
1228
+ if (this.getMasks().length !== before) {
1229
+ this.emitMasksChanged(callbackContext);
1230
+ this.emitImageChanged(callbackContext);
1231
+ }
1232
+ }
1233
+ removeAllMasks(options = {}) {
1234
+ if (!this.canvas)
1235
+ return;
1236
+ if (!this.canRunIdleOperation('removeAllMasks', options))
1237
+ return;
1238
+ const before = this.getMasks().length;
1239
+ const callbackContext = this.buildCallbackContext('removeAllMasks', false);
1240
+ const removeMaskContext = this.buildRemoveMaskContext();
1241
+ this.withSelectionChangeContext(callbackContext, () => removeAllMasksImpl(removeMaskContext, options));
1242
+ this.updateUi();
1243
+ if (this.getMasks().length !== before) {
1244
+ this.emitMasksChanged(callbackContext);
1245
+ this.emitImageChanged(callbackContext);
1246
+ }
1247
+ }
1248
+ buildCreateMaskContext() {
1249
+ return {
1250
+ fabric: this.fabricModule,
1251
+ canvas: this.canvas,
1252
+ options: this.options,
1253
+ getLastMask: () => this.lastMask,
1254
+ setLastMask: (maskObject) => {
1255
+ this.lastMask = maskObject;
1256
+ },
1257
+ getMaskCounter: () => this.maskCounter,
1258
+ setMaskCounter: (n) => {
1259
+ this.maskCounter = n;
1260
+ },
1261
+ updateMaskList: () => {
1262
+ this.updateMaskList();
1263
+ },
1264
+ saveCanvasState: () => {
1265
+ this.saveState();
1266
+ },
1267
+ expandCanvasIfNeeded: (widthPx, heightPx) => {
1268
+ this.setCanvasSizePx(widthPx, heightPx);
1269
+ },
1270
+ };
1271
+ }
1272
+ buildRemoveMaskContext() {
1273
+ return {
1274
+ canvas: this.canvas,
1275
+ removeLabelForMask: (mask) => {
1276
+ this.removeLabelForMask(mask);
1277
+ },
1278
+ updateMaskList: () => {
1279
+ this.updateMaskList();
1280
+ },
1281
+ saveCanvasState: () => {
1282
+ this.saveState();
1283
+ },
1284
+ setLastMask: (maskObject) => {
1285
+ this.lastMask = maskObject;
1286
+ },
1287
+ };
1288
+ }
1289
+ buildMaskLabelContext() {
1290
+ if (!this.canvas)
1291
+ return null;
1292
+ return { fabric: this.fabricModule, canvas: this.canvas, options: this.options };
1293
+ }
1294
+ removeLabelForMask(mask) {
1295
+ const context = this.buildMaskLabelContext();
1296
+ if (!context)
1297
+ return;
1298
+ removeLabelForMask(context, mask);
1299
+ }
1300
+ createLabelForMask(mask) {
1301
+ const context = this.buildMaskLabelContext();
1302
+ if (!context)
1303
+ return;
1304
+ createLabelForMask(context, mask);
1305
+ }
1306
+ hideAllMaskLabels() {
1307
+ const context = this.buildMaskLabelContext();
1308
+ if (!context)
1309
+ return;
1310
+ hideAllMaskLabels(context);
1311
+ }
1312
+ syncMaskLabel(mask) {
1313
+ const context = this.buildMaskLabelContext();
1314
+ if (!context)
1315
+ return;
1316
+ syncMaskLabel(context, mask);
1317
+ }
1318
+ showLabelForMask(mask) {
1319
+ const context = this.buildMaskLabelContext();
1320
+ if (!context)
1321
+ return;
1322
+ showLabelForMask(context, mask);
1323
+ }
1324
+ handleSelectionChanged(selected) {
1325
+ var _a, _b, _c;
1326
+ if (!this.canvas)
1327
+ return;
1328
+ const selectedMask = (_a = selected.find(isMaskObject)) !== null && _a !== void 0 ? _a : null;
1329
+ const masks = this.canvas.getObjects().filter(isMaskObject);
1330
+ masks.forEach((maskObject) => {
1331
+ if (maskObject !== selectedMask) {
1332
+ if (maskObject.labelObject) {
1333
+ this.removeLabelForMask(maskObject);
1334
+ }
1335
+ applyMaskUnselectedStyle(maskObject);
1336
+ }
1337
+ else {
1338
+ applyMaskSelectedStyle(maskObject);
1339
+ }
1340
+ });
1341
+ if (selectedMask)
1342
+ this.showLabelForMask(selectedMask);
1343
+ this.updateMaskListSelection(selectedMask);
1344
+ this.canvas.requestRenderAll();
1345
+ this.updateUi();
1346
+ const context = (_b = this.nextSelectionChangeContext) !== null && _b !== void 0 ? _b : this.buildCallbackContext((_c = this.activeStateRestoreOperation) !== null && _c !== void 0 ? _c : 'createMask', this.activeStateRestoreOperation === 'undo' ||
1347
+ this.activeStateRestoreOperation === 'redo');
1348
+ this.emitOptionCallback('onSelectionChange', [this.buildSelection(selected), context]);
1349
+ }
1350
+ buildMaskListContext() {
1351
+ return {
1352
+ canvas: this.canvas,
1353
+ getListElementId: () => this.elements.maskList,
1354
+ onMaskSelected: (mask) => this.handleSelectionChanged([mask]),
1355
+ };
1356
+ }
1357
+ updateMaskList() {
1358
+ renderMaskList(this.buildMaskListContext());
1359
+ }
1360
+ updateMaskListSelection(selectedMask) {
1361
+ updateMaskListSelection(this.buildMaskListContext(), selectedMask);
1362
+ }
1363
+ async mergeMasks() {
1364
+ if (!this.canvas)
1365
+ return;
1366
+ if (!this.canRunIdleOperation('mergeMasks'))
1367
+ return;
1368
+ const hasMasks = this.canvas.getObjects().some(isMaskObject);
1369
+ if (!hasMasks)
1370
+ return;
1371
+ const callbackContext = this.buildCallbackContext('mergeMasks', false);
1372
+ const operationToken = this.operationGuard.beginBusyOperation('mergeMasks');
1373
+ this.emitBusyChangeIfChanged(callbackContext);
1374
+ this.updateUi();
1375
+ try {
1376
+ const mergeMasksContext = this.buildMergeMasksContext(operationToken);
1377
+ await mergeMasksImpl(mergeMasksContext);
1378
+ this.updateInputs();
1379
+ this.updateMaskList();
1380
+ this.emitMasksChanged(callbackContext);
1381
+ this.emitImageChanged(callbackContext);
1382
+ }
1383
+ finally {
1384
+ this.operationGuard.endBusyOperation(operationToken);
1385
+ this.emitBusyChangeIfChanged(callbackContext);
1386
+ this.updateUi();
1387
+ }
1388
+ }
1389
+ downloadImage(fileName) {
1390
+ if (!this.canvas)
1391
+ return;
1392
+ if (!this.canRunIdleOperation('downloadImage'))
1393
+ return;
1394
+ const callbackContext = this.buildCallbackContext('downloadImage', false);
1395
+ const operationToken = this.operationGuard.beginBusyOperation('downloadImage');
1396
+ this.emitBusyChangeIfChanged(callbackContext);
1397
+ const exportContext = this.buildExportServiceContext();
1398
+ try {
1399
+ downloadImageImpl(exportContext, fileName);
1400
+ }
1401
+ finally {
1402
+ this.operationGuard.endBusyOperation(operationToken);
1403
+ this.emitBusyChangeIfChanged(callbackContext);
1404
+ }
1405
+ }
1406
+ async exportImageBase64(options) {
1407
+ if (!this.canvas)
1408
+ return '';
1409
+ if (!this.canRunIdleOperation('exportImageBase64', options))
1410
+ return '';
1411
+ const callbackContext = this.buildCallbackContext('exportImageBase64', false);
1412
+ const operationToken = this.operationGuard.beginBusyOperation('exportImageBase64');
1413
+ this.emitBusyChangeIfChanged(callbackContext);
1414
+ const exportContext = this.buildExportServiceContext();
1415
+ try {
1416
+ return await exportImageBase64Impl(exportContext, options);
1417
+ }
1418
+ finally {
1419
+ this.operationGuard.endBusyOperation(operationToken);
1420
+ this.emitBusyChangeIfChanged(callbackContext);
1421
+ }
1422
+ }
1423
+ async exportImageFile(options) {
1424
+ this.assertIdleForOperation('exportImageFile', options);
1425
+ const callbackContext = this.buildCallbackContext('exportImageFile', false);
1426
+ const operationToken = this.operationGuard.beginBusyOperation('exportImageFile');
1427
+ this.emitBusyChangeIfChanged(callbackContext);
1428
+ const exportContext = this.buildExportServiceContext();
1429
+ try {
1430
+ return await exportImageFileImpl(exportContext, options);
1431
+ }
1432
+ finally {
1433
+ this.operationGuard.endBusyOperation(operationToken);
1434
+ this.emitBusyChangeIfChanged(callbackContext);
1435
+ }
1436
+ }
1437
+ buildExportServiceContext() {
1438
+ return {
1439
+ fabric: this.fabricModule,
1440
+ canvas: this.canvas,
1441
+ options: this.options,
1442
+ isImageLoaded: () => this.isImageLoaded(),
1443
+ getOriginalImage: () => this.originalImage,
1444
+ };
1445
+ }
1446
+ buildMergeMasksContext(operationToken) {
1447
+ return {
1448
+ ...this.buildExportServiceContext(),
1449
+ historyManager: this.historyManager,
1450
+ containerElement: this.containerElement,
1451
+ loadImage: async (base64, providedOptions) => {
1452
+ const geometry = this.captureImageDisplayGeometry();
1453
+ await this.loadImage(base64, this.withInternalOperationOptions(operationToken, providedOptions));
1454
+ this.restoreMergedImageDisplayGeometry(geometry);
1455
+ },
1456
+ saveState: () => this.captureSnapshotInternal(),
1457
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
1458
+ removeAllMasksNoHistory: () => {
1459
+ const context = this.buildRemoveMaskContext();
1460
+ removeAllMasksImpl(context, { saveHistory: false });
1461
+ },
1462
+ };
1463
+ }
1464
+ captureSnapshotInternal() {
1465
+ var _a;
1466
+ if (!this.canvas)
1467
+ return '';
1468
+ const activeMask = this.getActiveMaskForSnapshot();
1469
+ this.hideAllMaskLabels();
1470
+ return saveStateImpl({
1471
+ canvas: this.canvas,
1472
+ activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
1473
+ currentScale: this.currentScale,
1474
+ currentRotation: this.currentRotation,
1475
+ baseImageScale: this.baseImageScale,
1476
+ currentImageMimeType: this.currentImageMimeType,
1477
+ });
1478
+ }
1479
+ getActiveMaskForSnapshot() {
1480
+ var _a;
1481
+ if (!this.canvas)
1482
+ return null;
1483
+ const activeObject = this.canvas.getActiveObject();
1484
+ if (activeObject && isMaskObject(activeObject))
1485
+ return activeObject;
1486
+ return ((_a = this.canvas
1487
+ .getObjects()
1488
+ .find((object) => isMaskObject(object) && !!object.labelObject)) !== null && _a !== void 0 ? _a : null);
1489
+ }
1490
+ enterCropMode() {
1491
+ if (!this.canvas || !this.originalImage)
1492
+ return;
1493
+ if (this.cropSession)
1494
+ return;
1495
+ if (!this.isImageLoaded())
1496
+ return;
1497
+ if (!this.canRunIdleOperation('enterCropMode'))
1498
+ return;
1499
+ const cropControllerContext = this.buildCropControllerContext();
1500
+ enterCropModeImpl(cropControllerContext);
1501
+ this.updateUi();
1502
+ const callbackContext = this.buildCallbackContext('enterCropMode', false);
1503
+ this.emitBusyChangeIfChanged(callbackContext);
1504
+ this.emitImageChanged(callbackContext);
1505
+ }
1506
+ cancelCrop() {
1507
+ if (!this.canvas || !this.cropSession)
1508
+ return;
1509
+ if (!this.canRunIdleOperation('cancelCrop'))
1510
+ return;
1511
+ const cropControllerContext = this.buildCropControllerContext();
1512
+ cancelCropImpl(cropControllerContext);
1513
+ this.cropSession = null;
1514
+ this.updateUi();
1515
+ this.canvas.requestRenderAll();
1516
+ const callbackContext = this.buildCallbackContext('cancelCrop', false);
1517
+ this.emitBusyChangeIfChanged(callbackContext);
1518
+ this.emitImageChanged(callbackContext);
1519
+ }
1520
+ async applyCrop() {
1521
+ if (!this.canvas || !this.cropSession)
1522
+ return;
1523
+ if (!this.canRunIdleOperation('applyCrop'))
1524
+ return;
1525
+ const callbackContext = this.buildCallbackContext('applyCrop', false);
1526
+ const hadMasks = this.getMasks().length > 0;
1527
+ const operationToken = this.operationGuard.beginBusyOperation('applyCrop');
1528
+ this.emitBusyChangeIfChanged(callbackContext);
1529
+ this.updateUi();
1530
+ try {
1531
+ const cropControllerContext = this.buildCropControllerContext(operationToken);
1532
+ await applyCropImpl(cropControllerContext);
1533
+ this.updateInputs();
1534
+ this.updateMaskList();
1535
+ if (hadMasks || this.getMasks().length > 0) {
1536
+ this.emitMasksChanged(callbackContext);
1537
+ }
1538
+ this.emitImageChanged(callbackContext);
1539
+ }
1540
+ finally {
1541
+ this.operationGuard.endBusyOperation(operationToken);
1542
+ this.emitBusyChangeIfChanged(callbackContext);
1543
+ this.updateUi();
1544
+ }
1545
+ }
1546
+ buildCropControllerContext(operationToken) {
1547
+ return {
1548
+ fabric: this.fabricModule,
1549
+ canvas: this.canvas,
1550
+ options: this.options,
1551
+ historyManager: this.historyManager,
1552
+ isImageLoaded: () => this.isImageLoaded(),
1553
+ getOriginalImage: () => this.originalImage,
1554
+ getCurrentImageMimeType: () => this.currentImageMimeType,
1555
+ getCropSession: () => this.cropSession,
1556
+ setCropSession: (s) => {
1557
+ this.cropSession = s;
1558
+ },
1559
+ saveState: () => this.captureSnapshotInternal(),
1560
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
1561
+ loadImage: (base64, providedOptions) => this.loadImage(base64, this.withInternalOperationOptions(operationToken, providedOptions)),
1562
+ getMaskCounter: () => this.maskCounter,
1563
+ setMaskCounter: (n) => {
1564
+ this.maskCounter = n;
1565
+ },
1566
+ updateMaskList: () => {
1567
+ this.updateMaskList();
1568
+ },
1569
+ };
1570
+ }
1571
+ updateInputs() {
1572
+ const scaleId = this.elements.scalePercentageInput;
1573
+ if (!scaleId)
1574
+ return;
1575
+ const scaleInputElement = document.getElementById(scaleId);
1576
+ if (scaleInputElement)
1577
+ scaleInputElement.value = String(Math.round(this.currentScale * 100));
1578
+ }
1579
+ updateUi() {
1580
+ if (!this.canvas)
1581
+ return;
1582
+ const hasImage = !!this.originalImage;
1583
+ const masks = hasImage ? this.canvas.getObjects().filter(isMaskObject) : [];
1584
+ const hasMasks = masks.length > 0;
1585
+ const activeObject = this.canvas.getActiveObject();
1586
+ const hasSelectedMask = !!(activeObject && isMaskObject(activeObject));
1587
+ const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
1588
+ const canUndo = this.historyManager.canUndo();
1589
+ const canRedo = this.historyManager.canRedo();
1590
+ const isInCropMode = this.cropSession !== null;
1591
+ const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
1592
+ if (isInCropMode) {
1593
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
1594
+ this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
1595
+ });
1596
+ return;
1597
+ }
1598
+ this.setControlEnabled('scalePercentageInput', hasImage && !isBusy);
1599
+ this.setControlEnabled('rotateLeftDegreesInput', hasImage && !isBusy);
1600
+ this.setControlEnabled('rotateRightDegreesInput', hasImage && !isBusy);
1601
+ this.setControlEnabled('zoomInButton', hasImage && !isBusy && this.currentScale < this.options.maxScale);
1602
+ this.setControlEnabled('zoomOutButton', hasImage && !isBusy && this.currentScale > this.options.minScale);
1603
+ this.setControlEnabled('rotateLeftButton', hasImage && !isBusy);
1604
+ this.setControlEnabled('rotateRightButton', hasImage && !isBusy);
1605
+ this.setControlEnabled('createMaskButton', hasImage && !isBusy);
1606
+ this.setControlEnabled('removeSelectedMaskButton', hasSelectedMask && !isBusy);
1607
+ this.setControlEnabled('removeAllMasksButton', hasMasks && !isBusy);
1608
+ this.setControlEnabled('mergeMasksButton', hasImage && hasMasks && !isBusy);
1609
+ this.setControlEnabled('downloadImageButton', hasImage && !isBusy);
1610
+ this.setControlEnabled('resetImageTransformButton', hasImage && !isDefaultTransform && !isBusy);
1611
+ this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
1612
+ this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
1613
+ this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
1614
+ this.setControlEnabled('imageInput', !isBusy);
1615
+ this.setControlEnabled('applyCropButton', false);
1616
+ this.setControlEnabled('cancelCropButton', false);
1617
+ }
1618
+ setControlEnabled(key, isEnabled) {
1619
+ var _a;
1620
+ const id = this.elements[key];
1621
+ if (!id)
1622
+ return;
1623
+ const controlElement = document.getElementById(id);
1624
+ if (!controlElement)
1625
+ return;
1626
+ this.recordElementOriginalState(key, controlElement);
1627
+ if ('disabled' in controlElement) {
1628
+ controlElement.disabled = !isEnabled;
1629
+ return;
1630
+ }
1631
+ if (!isEnabled) {
1632
+ controlElement.setAttribute('aria-disabled', 'true');
1633
+ controlElement.style.pointerEvents = 'none';
1634
+ }
1635
+ else {
1636
+ const originalAria = this.elementOriginalAriaDisabledMap.get(key);
1637
+ if (originalAria === null || originalAria === undefined) {
1638
+ controlElement.removeAttribute('aria-disabled');
1639
+ }
1640
+ else {
1641
+ controlElement.setAttribute('aria-disabled', originalAria);
1642
+ }
1643
+ controlElement.style.pointerEvents =
1644
+ (_a = this.elementOriginalPointerEventsMap.get(key)) !== null && _a !== void 0 ? _a : '';
1645
+ }
1646
+ }
1647
+ recordElementOriginalState(key, element) {
1648
+ if (!this.elementOriginalAriaDisabledMap.has(key)) {
1649
+ this.elementOriginalAriaDisabledMap.set(key, element.getAttribute('aria-disabled'));
1650
+ }
1651
+ if (!this.elementOriginalPointerEventsMap.has(key)) {
1652
+ this.elementOriginalPointerEventsMap.set(key, element.style.pointerEvents || '');
1653
+ }
1654
+ if ('disabled' in element && !this.elementOriginalDisabledMap.has(key)) {
1655
+ this.elementOriginalDisabledMap.set(key, !!element.disabled);
1656
+ }
1657
+ }
1658
+ restoreElementOriginalStates() {
1659
+ var _a, _b;
1660
+ for (const key of Object.keys(this.elements)) {
1661
+ const id = this.elements[key];
1662
+ if (!id)
1663
+ continue;
1664
+ const element = document.getElementById(id);
1665
+ if (!element)
1666
+ continue;
1667
+ if ('disabled' in element && this.elementOriginalDisabledMap.has(key)) {
1668
+ element.disabled =
1669
+ (_a = this.elementOriginalDisabledMap.get(key)) !== null && _a !== void 0 ? _a : false;
1670
+ }
1671
+ if (this.elementOriginalAriaDisabledMap.has(key)) {
1672
+ const originalAria = this.elementOriginalAriaDisabledMap.get(key);
1673
+ if (originalAria === null || originalAria === undefined) {
1674
+ element.removeAttribute('aria-disabled');
1675
+ }
1676
+ else {
1677
+ element.setAttribute('aria-disabled', originalAria);
1678
+ }
1679
+ }
1680
+ if (this.elementOriginalPointerEventsMap.has(key)) {
1681
+ element.style.pointerEvents = (_b = this.elementOriginalPointerEventsMap.get(key)) !== null && _b !== void 0 ? _b : '';
1682
+ }
1683
+ }
1684
+ this.elementOriginalDisabledMap.clear();
1685
+ this.elementOriginalAriaDisabledMap.clear();
1686
+ this.elementOriginalPointerEventsMap.clear();
1687
+ }
1688
+ updatePlaceholderStatus() {
1689
+ setPlaceholderVisibleImpl(this.placeholderElement, this.containerElement, this.options.showPlaceholder ? !this.originalImage : false);
1690
+ }
1691
+ dispose() {
1692
+ var _a;
1693
+ if (this.isDisposed)
1694
+ return;
1695
+ const context = this.buildCallbackContext('dispose', false);
1696
+ const previousImage = this.originalImage;
1697
+ this.isDisposed = true;
1698
+ this.operationGuard.markDisposed();
1699
+ this.animQueue.clear();
1700
+ (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.removeAll();
1701
+ this.restoreElementOriginalStates();
1702
+ if (this.cropSession && this.canvas) {
1703
+ try {
1704
+ const context = this.buildCropControllerContext();
1705
+ cancelCropImpl(context);
1706
+ }
1707
+ catch {
1708
+ }
1709
+ this.cropSession = null;
1710
+ }
1711
+ if (this.canvas) {
1712
+ try {
1713
+ void Promise.resolve(this.canvas.dispose()).catch(() => {
1714
+ });
1715
+ }
1716
+ catch {
1717
+ }
1718
+ this.canvas = null;
1719
+ this.canvasElement = null;
1720
+ this.isImageLoadedToCanvas = false;
1721
+ }
1722
+ this.originalImage = null;
1723
+ this.currentImageMimeType = null;
1724
+ this.lastMask = null;
1725
+ this.maskCounter = 0;
1726
+ this.currentScale = 1;
1727
+ this.currentRotation = 0;
1728
+ this.baseImageScale = 1;
1729
+ this.lastSnapshot = null;
1730
+ this.transformController = null;
1731
+ this.viewportCache.clear();
1732
+ if (previousImage) {
1733
+ this.emitOptionCallback('onImageCleared', [previousImage, context]);
1734
+ }
1735
+ this.emitImageChanged(context);
1736
+ this.emitBusyChangeIfChanged(context);
1737
+ this.emitOptionCallback('onEditorDisposed', [context]);
1738
+ }
1739
+ }
1740
+ //# sourceMappingURL=image-editor.js.map