@bensitu/image-editor 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +118 -16
  2. package/dist/cjs/index.cjs +1800 -330
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/animation/animation-queue.js +16 -9
  5. package/dist/esm/animation/animation-queue.js.map +1 -1
  6. package/dist/esm/core/default-options.js +216 -9
  7. package/dist/esm/core/default-options.js.map +1 -1
  8. package/dist/esm/core/operation-guard.js +28 -0
  9. package/dist/esm/core/operation-guard.js.map +1 -1
  10. package/dist/esm/core/public-types.js.map +1 -1
  11. package/dist/esm/core/state-serializer.js +5 -4
  12. package/dist/esm/core/state-serializer.js.map +1 -1
  13. package/dist/esm/crop/crop-controller.js +4 -2
  14. package/dist/esm/crop/crop-controller.js.map +1 -1
  15. package/dist/esm/export/export-service.js +21 -10
  16. package/dist/esm/export/export-service.js.map +1 -1
  17. package/dist/esm/fabric/fabric-animation.js +56 -4
  18. package/dist/esm/fabric/fabric-animation.js.map +1 -1
  19. package/dist/esm/image/image-loader.js +9 -16
  20. package/dist/esm/image/image-loader.js.map +1 -1
  21. package/dist/esm/image/image-resampler.js +7 -2
  22. package/dist/esm/image/image-resampler.js.map +1 -1
  23. package/dist/esm/image/layout-manager.js +2 -20
  24. package/dist/esm/image/layout-manager.js.map +1 -1
  25. package/dist/esm/image/transform-controller.js.map +1 -1
  26. package/dist/esm/image-editor.js +383 -47
  27. package/dist/esm/image-editor.js.map +1 -1
  28. package/dist/esm/mask/mask-factory.js +53 -29
  29. package/dist/esm/mask/mask-factory.js.map +1 -1
  30. package/dist/esm/mask/mask-list.js +9 -3
  31. package/dist/esm/mask/mask-list.js.map +1 -1
  32. package/dist/esm/mosaic/mosaic-controller.js +670 -0
  33. package/dist/esm/mosaic/mosaic-controller.js.map +1 -0
  34. package/dist/esm/mosaic/mosaic-geometry.js +81 -0
  35. package/dist/esm/mosaic/mosaic-geometry.js.map +1 -0
  36. package/dist/esm/mosaic/mosaic-pixelate.js +71 -0
  37. package/dist/esm/mosaic/mosaic-pixelate.js.map +1 -0
  38. package/dist/esm/ui/dom-bindings.js +10 -3
  39. package/dist/esm/ui/dom-bindings.js.map +1 -1
  40. package/dist/esm/utils/number.js.map +1 -1
  41. package/dist/types/animation/animation-queue.d.ts.map +1 -1
  42. package/dist/types/core/default-options.d.ts +34 -6
  43. package/dist/types/core/default-options.d.ts.map +1 -1
  44. package/dist/types/core/errors.d.ts +1 -1
  45. package/dist/types/core/operation-guard.d.ts +2 -0
  46. package/dist/types/core/operation-guard.d.ts.map +1 -1
  47. package/dist/types/core/public-types.d.ts +123 -13
  48. package/dist/types/core/public-types.d.ts.map +1 -1
  49. package/dist/types/core/state-serializer.d.ts +3 -1
  50. package/dist/types/core/state-serializer.d.ts.map +1 -1
  51. package/dist/types/crop/crop-controller.d.ts.map +1 -1
  52. package/dist/types/export/export-service.d.ts.map +1 -1
  53. package/dist/types/fabric/fabric-animation.d.ts.map +1 -1
  54. package/dist/types/image/image-loader.d.ts +2 -4
  55. package/dist/types/image/image-loader.d.ts.map +1 -1
  56. package/dist/types/image/image-resampler.d.ts +1 -1
  57. package/dist/types/image/image-resampler.d.ts.map +1 -1
  58. package/dist/types/image/layout-manager.d.ts +5 -49
  59. package/dist/types/image/layout-manager.d.ts.map +1 -1
  60. package/dist/types/image/transform-controller.d.ts +1 -2
  61. package/dist/types/image/transform-controller.d.ts.map +1 -1
  62. package/dist/types/image-editor.d.ts +20 -9
  63. package/dist/types/image-editor.d.ts.map +1 -1
  64. package/dist/types/index.d.cts +1 -1
  65. package/dist/types/index.d.cts.map +1 -1
  66. package/dist/types/index.d.ts +1 -1
  67. package/dist/types/index.d.ts.map +1 -1
  68. package/dist/types/mask/mask-factory.d.ts +24 -21
  69. package/dist/types/mask/mask-factory.d.ts.map +1 -1
  70. package/dist/types/mask/mask-list.d.ts.map +1 -1
  71. package/dist/types/mosaic/mosaic-controller.d.ts +82 -0
  72. package/dist/types/mosaic/mosaic-controller.d.ts.map +1 -0
  73. package/dist/types/mosaic/mosaic-geometry.d.ts +29 -0
  74. package/dist/types/mosaic/mosaic-geometry.d.ts.map +1 -0
  75. package/dist/types/mosaic/mosaic-pixelate.d.ts +23 -0
  76. package/dist/types/mosaic/mosaic-pixelate.d.ts.map +1 -0
  77. package/dist/types/ui/dom-bindings.d.ts +3 -1
  78. package/dist/types/ui/dom-bindings.d.ts.map +1 -1
  79. package/dist/types/utils/number.d.ts +1 -2
  80. package/dist/types/utils/number.d.ts.map +1 -1
  81. package/dist/umd/image-editor.umd.js +1 -1
  82. package/dist/umd/image-editor.umd.js.map +1 -1
  83. package/package.json +1 -1
@@ -52,20 +52,27 @@ class AnimationQueue {
52
52
  return this.add(() => Promise.resolve()).then(() => undefined, () => undefined);
53
53
  }
54
54
  async drainQueue() {
55
- if (this.queue.length === 0) {
56
- this.running = false;
55
+ if (this.running)
57
56
  return;
58
- }
59
57
  this.running = true;
60
- const entry = this.queue.shift();
61
58
  try {
62
- await entry.run();
63
- entry.resolve();
59
+ while (this.queue.length > 0) {
60
+ const entry = this.queue.shift();
61
+ try {
62
+ await entry.run();
63
+ entry.resolve();
64
+ }
65
+ catch (error) {
66
+ entry.reject(error);
67
+ }
68
+ }
64
69
  }
65
- catch (error) {
66
- entry.reject(error);
70
+ finally {
71
+ this.running = false;
72
+ if (this.queue.length > 0) {
73
+ void this.drainQueue();
74
+ }
67
75
  }
68
- void this.drainQueue();
69
76
  }
70
77
  }
71
78
 
@@ -92,6 +99,61 @@ function reportError(options, error, message) {
92
99
  }
93
100
  }
94
101
 
102
+ const FORMAT_ALIAS_TABLE = Object.freeze({
103
+ jpeg: 'jpeg',
104
+ jpg: 'jpeg',
105
+ 'image/jpeg': 'jpeg',
106
+ png: 'png',
107
+ 'image/png': 'png',
108
+ webp: 'webp',
109
+ 'image/webp': 'webp',
110
+ });
111
+ const MIME_TABLE = Object.freeze({
112
+ jpeg: 'image/jpeg',
113
+ png: 'image/png',
114
+ webp: 'image/webp',
115
+ });
116
+ function normalizeImageFormat(input) {
117
+ var _a;
118
+ return (_a = tryNormalizeImageFormat(input)) !== null && _a !== void 0 ? _a : 'jpeg';
119
+ }
120
+ function tryNormalizeImageFormat(input) {
121
+ var _a;
122
+ if (!input)
123
+ return null;
124
+ const key = String(input).toLowerCase();
125
+ if (Object.prototype.hasOwnProperty.call(FORMAT_ALIAS_TABLE, key)) {
126
+ return (_a = FORMAT_ALIAS_TABLE[key]) !== null && _a !== void 0 ? _a : null;
127
+ }
128
+ return null;
129
+ }
130
+ function mimeTypeFor(format) {
131
+ return MIME_TABLE[format];
132
+ }
133
+ function clampQuality(quality, fallback) {
134
+ const numeric = Number(quality);
135
+ if (!Number.isFinite(numeric))
136
+ return fallback;
137
+ return Math.max(0, Math.min(1, numeric));
138
+ }
139
+ function resolveExportFormat(options, downsampleQuality) {
140
+ var _a;
141
+ const providedOptions = options !== null && options !== void 0 ? options : {};
142
+ const fileType = providedOptions.fileType;
143
+ const formatAlias = providedOptions.format;
144
+ const requested = fileType || formatAlias;
145
+ const format = normalizeImageFormat(requested);
146
+ const mimeType = mimeTypeFor(format);
147
+ if (format === 'png') {
148
+ return { format, mimeType, quality: undefined };
149
+ }
150
+ const rawQuality = (_a = providedOptions.quality) !== null && _a !== void 0 ? _a : downsampleQuality;
151
+ const quality = clampQuality(rawQuality, downsampleQuality);
152
+ return { format, mimeType, quality };
153
+ }
154
+
155
+ const EMPTY_DEFAULT_MASK_CONFIG = Object.freeze({});
156
+ const DEFAULT_LAYOUT_MODE = 'expand';
95
157
  const DEFAULT_OPTIONS = {
96
158
  canvasWidth: 800,
97
159
  canvasHeight: 600,
@@ -101,9 +163,8 @@ const DEFAULT_OPTIONS = {
101
163
  maxScale: 5.0,
102
164
  scaleStep: 0.05,
103
165
  rotationStep: 90,
104
- expandCanvasToImage: true,
105
- fitImageToCanvas: false,
106
- coverImageToCanvas: false,
166
+ defaultLayoutMode: DEFAULT_LAYOUT_MODE,
167
+ layoutMode: DEFAULT_LAYOUT_MODE,
107
168
  downsampleOnLoad: true,
108
169
  downsampleMaxWidth: 4000,
109
170
  downsampleMaxHeight: 3000,
@@ -118,6 +179,7 @@ const DEFAULT_OPTIONS = {
118
179
  mergeMaskByDefault: true,
119
180
  defaultMaskWidth: 50,
120
181
  defaultMaskHeight: 80,
182
+ defaultMaskConfig: EMPTY_DEFAULT_MASK_CONFIG,
121
183
  maskRotatable: false,
122
184
  maskLabelOnSelect: true,
123
185
  maskLabelOffset: 3,
@@ -159,6 +221,16 @@ const DEFAULT_CROP = {
159
221
  preserveMasksAfterCrop: false,
160
222
  allowRotationOfCropRect: false,
161
223
  exportFileType: 'source'};
224
+ const DEFAULT_MOSAIC_CONFIG = Object.freeze({
225
+ brushSize: 48,
226
+ blockSize: 8,
227
+ previewStroke: '#333',
228
+ previewStrokeWidth: 1,
229
+ previewStrokeDashArray: Object.freeze([4, 4]),
230
+ previewFill: 'rgba(0,0,0,0)',
231
+ outputFileType: 'source',
232
+ outputQuality: undefined,
233
+ });
162
234
  const KNOWN_TOP_LEVEL_KEYS = new Set([
163
235
  'canvasWidth',
164
236
  'canvasHeight',
@@ -168,9 +240,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
168
240
  'maxScale',
169
241
  'scaleStep',
170
242
  'rotationStep',
171
- 'expandCanvasToImage',
172
- 'fitImageToCanvas',
173
- 'coverImageToCanvas',
243
+ 'defaultLayoutMode',
174
244
  'downsampleOnLoad',
175
245
  'downsampleMaxWidth',
176
246
  'downsampleMaxHeight',
@@ -185,6 +255,7 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
185
255
  'mergeMaskByDefault',
186
256
  'defaultMaskWidth',
187
257
  'defaultMaskHeight',
258
+ 'defaultMaskConfig',
188
259
  'maskRotatable',
189
260
  'maskLabelOnSelect',
190
261
  'maskLabelOffset',
@@ -205,10 +276,44 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
205
276
  'onWarning',
206
277
  'label',
207
278
  'crop',
279
+ 'defaultMosaicConfig',
208
280
  ]);
209
281
  function normalizeCallback(value) {
210
282
  return typeof value === 'function' ? value : null;
211
283
  }
284
+ function isLayoutMode(value) {
285
+ return value === 'fit' || value === 'cover' || value === 'expand';
286
+ }
287
+ function normalizeLayoutMode(value) {
288
+ return isLayoutMode(value) ? value : DEFAULT_LAYOUT_MODE;
289
+ }
290
+ function isConfigObject(value) {
291
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
292
+ }
293
+ function copyDefaultMaskConfigValue(value) {
294
+ return Array.isArray(value) ? [...value] : value;
295
+ }
296
+ function normalizeDefaultMaskConfig(value) {
297
+ if (!isConfigObject(value))
298
+ return EMPTY_DEFAULT_MASK_CONFIG;
299
+ const normalized = {};
300
+ for (const [key, optionValue] of Object.entries(value)) {
301
+ if (key === 'onCreate' || key === 'fabricGenerator' || key === 'styles')
302
+ continue;
303
+ normalized[key] = copyDefaultMaskConfigValue(optionValue);
304
+ }
305
+ const styles = value.styles;
306
+ if (isConfigObject(styles)) {
307
+ const copiedStyles = {};
308
+ for (const [key, styleValue] of Object.entries(styles)) {
309
+ copiedStyles[key] = copyDefaultMaskConfigValue(styleValue);
310
+ }
311
+ Object.freeze(copiedStyles);
312
+ normalized.styles = copiedStyles;
313
+ }
314
+ Object.freeze(normalized);
315
+ return normalized;
316
+ }
212
317
  function normalizePositiveInteger(value, fallback) {
213
318
  const numeric = Number(value);
214
319
  if (!Number.isFinite(numeric) || numeric <= 0)
@@ -264,6 +369,151 @@ function normalizeOptionalQuality(value) {
264
369
  return undefined;
265
370
  return Math.max(0, Math.min(1, numeric));
266
371
  }
372
+ function hasOwn(object, key) {
373
+ return Object.prototype.hasOwnProperty.call(object, key);
374
+ }
375
+ function isFiniteNumber$1(value) {
376
+ return typeof value === 'number' && Number.isFinite(value);
377
+ }
378
+ function normalizeMosaicPositiveNumber(value, fallback) {
379
+ return isFiniteNumber$1(value) && value > 0 ? value : fallback;
380
+ }
381
+ function normalizeMosaicBlockSize(value, fallback) {
382
+ return isFiniteNumber$1(value) && value > 0 ? Math.max(1, Math.floor(value)) : fallback;
383
+ }
384
+ function normalizeMosaicNonNegativeNumber(value, fallback) {
385
+ return isFiniteNumber$1(value) && value >= 0 ? value : fallback;
386
+ }
387
+ function normalizeMosaicDashArray(value, fallback) {
388
+ if (value === null)
389
+ return null;
390
+ if (Array.isArray(value) &&
391
+ value.every((entry) => typeof entry === 'number' && Number.isFinite(entry) && entry >= 0)) {
392
+ return [...value];
393
+ }
394
+ return fallback ? [...fallback] : null;
395
+ }
396
+ function normalizeMosaicOutputFileType(value, fallback) {
397
+ var _a;
398
+ if (value === 'source')
399
+ return 'source';
400
+ if (typeof value !== 'string')
401
+ return fallback;
402
+ return (_a = tryNormalizeImageFormat(value)) !== null && _a !== void 0 ? _a : fallback;
403
+ }
404
+ function normalizeMosaicOutputQuality(value, fallback) {
405
+ if (value === undefined || value === null)
406
+ return undefined;
407
+ if (!isFiniteNumber$1(value))
408
+ return fallback;
409
+ return Math.max(0, Math.min(1, value));
410
+ }
411
+ function cloneResolvedMosaicConfig(config) {
412
+ return {
413
+ ...config,
414
+ previewStrokeDashArray: config.previewStrokeDashArray
415
+ ? [...config.previewStrokeDashArray]
416
+ : null,
417
+ };
418
+ }
419
+ function normalizeMosaicConfig(input, fallback) {
420
+ if (!isConfigObject(input))
421
+ return cloneResolvedMosaicConfig(fallback);
422
+ return mergeMosaicConfigPatch(fallback, input);
423
+ }
424
+ function mergeMosaicConfigPatch(current, patch, fallback = current) {
425
+ const raw = isConfigObject(patch) ? patch : {};
426
+ const next = cloneResolvedMosaicConfig(current);
427
+ if (hasOwn(raw, 'brushSize')) {
428
+ next.brushSize = normalizeMosaicPositiveNumber(raw.brushSize, fallback.brushSize);
429
+ }
430
+ if (hasOwn(raw, 'blockSize')) {
431
+ next.blockSize = normalizeMosaicBlockSize(raw.blockSize, fallback.blockSize);
432
+ }
433
+ if (hasOwn(raw, 'previewStroke')) {
434
+ next.previewStroke =
435
+ typeof raw.previewStroke === 'string' ? raw.previewStroke : fallback.previewStroke;
436
+ }
437
+ if (hasOwn(raw, 'previewStrokeWidth')) {
438
+ next.previewStrokeWidth = normalizeMosaicNonNegativeNumber(raw.previewStrokeWidth, fallback.previewStrokeWidth);
439
+ }
440
+ if (hasOwn(raw, 'previewStrokeDashArray')) {
441
+ next.previewStrokeDashArray = normalizeMosaicDashArray(raw.previewStrokeDashArray, fallback.previewStrokeDashArray);
442
+ }
443
+ if (hasOwn(raw, 'previewFill')) {
444
+ next.previewFill =
445
+ typeof raw.previewFill === 'string' ? raw.previewFill : fallback.previewFill;
446
+ }
447
+ if (hasOwn(raw, 'outputFileType')) {
448
+ next.outputFileType = normalizeMosaicOutputFileType(raw.outputFileType, fallback.outputFileType);
449
+ }
450
+ if (hasOwn(raw, 'outputQuality')) {
451
+ next.outputQuality = normalizeMosaicOutputQuality(raw.outputQuality, fallback.outputQuality);
452
+ }
453
+ return next;
454
+ }
455
+ function getInvalidMosaicConfigFields(input) {
456
+ const raw = isConfigObject(input) ? input : {};
457
+ const invalid = [];
458
+ if (hasOwn(raw, 'brushSize') &&
459
+ !(typeof raw.brushSize === 'number' && Number.isFinite(raw.brushSize) && raw.brushSize > 0)) {
460
+ invalid.push('brushSize');
461
+ }
462
+ if (hasOwn(raw, 'blockSize') &&
463
+ !(typeof raw.blockSize === 'number' && Number.isFinite(raw.blockSize) && raw.blockSize > 0)) {
464
+ invalid.push('blockSize');
465
+ }
466
+ if (hasOwn(raw, 'previewStroke') && typeof raw.previewStroke !== 'string') {
467
+ invalid.push('previewStroke');
468
+ }
469
+ if (hasOwn(raw, 'previewStrokeWidth') &&
470
+ !(typeof raw.previewStrokeWidth === 'number' &&
471
+ Number.isFinite(raw.previewStrokeWidth) &&
472
+ raw.previewStrokeWidth >= 0)) {
473
+ invalid.push('previewStrokeWidth');
474
+ }
475
+ if (hasOwn(raw, 'previewStrokeDashArray')) {
476
+ const value = raw.previewStrokeDashArray;
477
+ const valid = value === null ||
478
+ (Array.isArray(value) &&
479
+ value.every((entry) => typeof entry === 'number' && Number.isFinite(entry) && entry >= 0));
480
+ if (!valid)
481
+ invalid.push('previewStrokeDashArray');
482
+ }
483
+ if (hasOwn(raw, 'previewFill') && typeof raw.previewFill !== 'string') {
484
+ invalid.push('previewFill');
485
+ }
486
+ if (hasOwn(raw, 'outputFileType')) {
487
+ const value = raw.outputFileType;
488
+ const valid = value === 'source' || (typeof value === 'string' && tryNormalizeImageFormat(value));
489
+ if (!valid)
490
+ invalid.push('outputFileType');
491
+ }
492
+ if (hasOwn(raw, 'outputQuality') &&
493
+ raw.outputQuality !== undefined &&
494
+ raw.outputQuality !== null &&
495
+ !(typeof raw.outputQuality === 'number' && Number.isFinite(raw.outputQuality))) {
496
+ invalid.push('outputQuality');
497
+ }
498
+ return invalid;
499
+ }
500
+ function areResolvedMosaicConfigsEqual(left, right) {
501
+ const leftDash = left.previewStrokeDashArray;
502
+ const rightDash = right.previewStrokeDashArray;
503
+ const dashEqual = leftDash === rightDash ||
504
+ (Array.isArray(leftDash) &&
505
+ Array.isArray(rightDash) &&
506
+ leftDash.length === rightDash.length &&
507
+ leftDash.every((value, index) => value === rightDash[index]));
508
+ return (left.brushSize === right.brushSize &&
509
+ left.blockSize === right.blockSize &&
510
+ left.previewStroke === right.previewStroke &&
511
+ left.previewStrokeWidth === right.previewStrokeWidth &&
512
+ dashEqual &&
513
+ left.previewFill === right.previewFill &&
514
+ left.outputFileType === right.outputFileType &&
515
+ left.outputQuality === right.outputQuality);
516
+ }
267
517
  function resolveOptions(input) {
268
518
  var _a, _b, _c, _d;
269
519
  const raw = input !== null && input !== void 0 ? input : {};
@@ -271,7 +521,7 @@ function resolveOptions(input) {
271
521
  for (const key of Object.keys(raw)) {
272
522
  if (!KNOWN_TOP_LEVEL_KEYS.has(key))
273
523
  continue;
274
- if (key === 'label' || key === 'crop')
524
+ if (key === 'label' || key === 'crop' || key === 'defaultMosaicConfig')
275
525
  continue;
276
526
  if (key === 'onImageLoadStart' ||
277
527
  key === 'onImageLoaded' ||
@@ -300,6 +550,12 @@ function resolveOptions(input) {
300
550
  resolved.exportAreaByDefault = normalizeExportArea(value);
301
551
  continue;
302
552
  }
553
+ if (key === 'defaultLayoutMode') {
554
+ const layoutMode = normalizeLayoutMode(value);
555
+ resolved.defaultLayoutMode = layoutMode;
556
+ resolved.layoutMode = layoutMode;
557
+ continue;
558
+ }
303
559
  if (key === 'canvasWidth') {
304
560
  resolved.canvasWidth = normalizePositiveInteger(value, DEFAULT_OPTIONS.canvasWidth);
305
561
  continue;
@@ -352,6 +608,10 @@ function resolveOptions(input) {
352
608
  resolved.defaultMaskHeight = normalizePositiveFiniteNumber(value, DEFAULT_OPTIONS.defaultMaskHeight);
353
609
  continue;
354
610
  }
611
+ if (key === 'defaultMaskConfig') {
612
+ resolved.defaultMaskConfig = normalizeDefaultMaskConfig(value);
613
+ continue;
614
+ }
355
615
  if (key === 'maskLabelOffset') {
356
616
  resolved.maskLabelOffset = normalizeNonNegativeFiniteNumber(value, DEFAULT_OPTIONS.maskLabelOffset);
357
617
  continue;
@@ -403,11 +663,17 @@ function resolveOptions(input) {
403
663
  exportQuality: normalizeOptionalQuality(userCrop.exportQuality),
404
664
  };
405
665
  Object.freeze(crop);
406
- return {
666
+ const defaultMosaicConfig = normalizeMosaicConfig(raw.defaultMosaicConfig, DEFAULT_MOSAIC_CONFIG);
667
+ if (defaultMosaicConfig.previewStrokeDashArray) {
668
+ Object.freeze(defaultMosaicConfig.previewStrokeDashArray);
669
+ }
670
+ Object.freeze(defaultMosaicConfig);
671
+ return Object.freeze({
407
672
  ...resolved,
408
673
  label,
409
674
  crop,
410
- };
675
+ defaultMosaicConfig,
676
+ });
411
677
  }
412
678
 
413
679
  class OperationGuard {
@@ -442,6 +708,12 @@ class OperationGuard {
442
708
  writable: true,
443
709
  value: null
444
710
  });
711
+ Object.defineProperty(this, "animationAborters", {
712
+ enumerable: true,
713
+ configurable: true,
714
+ writable: true,
715
+ value: new Set()
716
+ });
445
717
  }
446
718
  isAnimating() {
447
719
  return this.isAnimationActive;
@@ -470,6 +742,28 @@ class OperationGuard {
470
742
  this.isLoadingActive = false;
471
743
  this.currentOperationName = null;
472
744
  this.currentOperationToken = null;
745
+ for (const abort of this.animationAborters) {
746
+ try {
747
+ abort();
748
+ }
749
+ catch {
750
+ }
751
+ }
752
+ this.animationAborters.clear();
753
+ }
754
+ registerAnimationAborter(abort) {
755
+ if (this.isDisposedFlag) {
756
+ try {
757
+ abort();
758
+ }
759
+ catch {
760
+ }
761
+ return () => undefined;
762
+ }
763
+ this.animationAborters.add(abort);
764
+ return () => {
765
+ this.animationAborters.delete(abort);
766
+ };
473
767
  }
474
768
  beginLoading() {
475
769
  this.isLoadingActive = true;
@@ -560,6 +854,7 @@ const SNAPSHOT_CUSTOM_KEYS = [
560
854
  'borderColor',
561
855
  'cornerColor',
562
856
  'cornerSize',
857
+ 'isMosaicPreview',
563
858
  ];
564
859
  function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
565
860
  if (!Array.isArray(jsonObjects))
@@ -611,6 +906,8 @@ function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
611
906
  jsonObject.isCropRect = true;
612
907
  if (liveObject.maskLabel === true)
613
908
  jsonObject.maskLabel = true;
909
+ if (liveObject.isMosaicPreview === true)
910
+ jsonObject.isMosaicPreview = true;
614
911
  }
615
912
  }
616
913
  function isActiveSelectionObject(object) {
@@ -621,9 +918,7 @@ function isActiveSelectionObject(object) {
621
918
  return true;
622
919
  const isType = object.isType;
623
920
  return (typeof isType === 'function' &&
624
- (isType.call(object, 'ActiveSelection') ||
625
- isType.call(object, 'activeSelection') ||
626
- isType.call(object, 'activeselection')));
921
+ (isType.call(object, 'ActiveSelection') || isType.call(object, 'activeSelection')));
627
922
  }
628
923
  function saveState(input) {
629
924
  var _a, _b, _c;
@@ -640,7 +935,7 @@ function saveState(input) {
640
935
  const jsonObj = canvas.toJSON(SNAPSHOT_CUSTOM_KEYS);
641
936
  copySnapshotCustomPropsFromCanvas(canvas.getObjects(), jsonObj.objects);
642
937
  if (Array.isArray(jsonObj.objects)) {
643
- jsonObj.objects = jsonObj.objects.filter((o) => o.isCropRect !== true && o.maskLabel !== true);
938
+ jsonObj.objects = jsonObj.objects.filter((o) => o.isCropRect !== true && o.maskLabel !== true && o.isMosaicPreview !== true);
644
939
  }
645
940
  jsonObj._editorState = {
646
941
  currentScale,
@@ -1312,59 +1607,6 @@ function getObjectBBox(object) {
1312
1607
  };
1313
1608
  }
1314
1609
 
1315
- const FORMAT_ALIAS_TABLE = Object.freeze({
1316
- jpeg: 'jpeg',
1317
- jpg: 'jpeg',
1318
- 'image/jpeg': 'jpeg',
1319
- png: 'png',
1320
- 'image/png': 'png',
1321
- webp: 'webp',
1322
- 'image/webp': 'webp',
1323
- });
1324
- const MIME_TABLE = Object.freeze({
1325
- jpeg: 'image/jpeg',
1326
- png: 'image/png',
1327
- webp: 'image/webp',
1328
- });
1329
- function normalizeImageFormat(input) {
1330
- var _a;
1331
- return (_a = tryNormalizeImageFormat(input)) !== null && _a !== void 0 ? _a : 'jpeg';
1332
- }
1333
- function tryNormalizeImageFormat(input) {
1334
- var _a;
1335
- if (!input)
1336
- return null;
1337
- const key = String(input).toLowerCase();
1338
- if (Object.prototype.hasOwnProperty.call(FORMAT_ALIAS_TABLE, key)) {
1339
- return (_a = FORMAT_ALIAS_TABLE[key]) !== null && _a !== void 0 ? _a : null;
1340
- }
1341
- return null;
1342
- }
1343
- function mimeTypeFor(format) {
1344
- return MIME_TABLE[format];
1345
- }
1346
- function clampQuality(quality, fallback) {
1347
- const numeric = Number(quality);
1348
- if (!Number.isFinite(numeric))
1349
- return fallback;
1350
- return Math.max(0, Math.min(1, numeric));
1351
- }
1352
- function resolveExportFormat(options, downsampleQuality) {
1353
- var _a;
1354
- const providedOptions = options !== null && options !== void 0 ? options : {};
1355
- const fileType = providedOptions.fileType;
1356
- const formatAlias = providedOptions.format;
1357
- const requested = fileType || formatAlias;
1358
- const format = normalizeImageFormat(requested);
1359
- const mimeType = mimeTypeFor(format);
1360
- if (format === 'png') {
1361
- return { format, mimeType, quality: undefined };
1362
- }
1363
- const rawQuality = (_a = providedOptions.quality) !== null && _a !== void 0 ? _a : downsampleQuality;
1364
- const quality = clampQuality(rawQuality, downsampleQuality);
1365
- return { format, mimeType, quality };
1366
- }
1367
-
1368
1610
  const CROP_RECT_FILL = 'rgba(0,0,0,0.12)';
1369
1611
  const CROP_RECT_STROKE = '#00aaff';
1370
1612
  const CROP_RECT_DASH = [6, 4];
@@ -1479,14 +1721,16 @@ function maskIntersectsRegion(mask, region) {
1479
1721
  function capturePreservedMasks(canvas, cropRegion, maskBackups = []) {
1480
1722
  var _a;
1481
1723
  const records = [];
1482
- const styleBackupByMask = new Map(maskBackups.map((backup) => [backup.object, backup]));
1724
+ const styleBackupByMask = maskBackups.length > 0
1725
+ ? new Map(maskBackups.map((backup) => [backup.object, backup]))
1726
+ : null;
1483
1727
  const masks = canvas.getObjects().filter(isMaskObject);
1484
1728
  for (const mask of masks) {
1485
1729
  try {
1486
1730
  mask.setCoords();
1487
1731
  const intersects = maskIntersectsRegion(mask, cropRegion);
1488
1732
  if (intersects) {
1489
- const styleBackup = (_a = styleBackupByMask.get(mask)) !== null && _a !== void 0 ? _a : captureMaskStyleBackup(mask);
1733
+ const styleBackup = (_a = styleBackupByMask === null || styleBackupByMask === void 0 ? void 0 : styleBackupByMask.get(mask)) !== null && _a !== void 0 ? _a : captureMaskStyleBackup(mask);
1490
1734
  records.push({
1491
1735
  mask,
1492
1736
  left: finiteNumberOrFallback(mask.left, 0),
@@ -1762,62 +2006,948 @@ async function applyCrop(context) {
1762
2006
  }
1763
2007
  }
1764
2008
 
1765
- function resolveMultiplier(requested, fallback) {
1766
- const num = Number(requested);
1767
- if (Number.isFinite(num) && num > 0)
1768
- return num;
1769
- const fallbackValue = Number(fallback);
1770
- return Number.isFinite(fallbackValue) && fallbackValue > 0 ? fallbackValue : 1;
1771
- }
1772
- function resolveExportArea(requested, fallback) {
1773
- if (requested === 'canvas' || requested === 'image')
1774
- return requested;
1775
- return fallback === 'canvas' ? 'canvas' : 'image';
1776
- }
1777
- function resolveExportOptions(context, options) {
1778
- const providedOptions = options !== null && options !== void 0 ? options : {};
2009
+ function computeDownsampleDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
2010
+ if (!isPositiveFinite$1(srcWidth) ||
2011
+ !isPositiveFinite$1(srcHeight) ||
2012
+ !isPositiveFinite$1(maxWidth) ||
2013
+ !isPositiveFinite$1(maxHeight)) {
2014
+ return {
2015
+ width: Math.max(1, Math.round(srcWidth) || 1),
2016
+ height: Math.max(1, Math.round(srcHeight) || 1),
2017
+ needsResize: false,
2018
+ };
2019
+ }
2020
+ const needsResize = srcWidth > maxWidth || srcHeight > maxHeight;
2021
+ if (!needsResize) {
2022
+ return { width: srcWidth, height: srcHeight, needsResize: false };
2023
+ }
2024
+ const ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
1779
2025
  return {
1780
- exportArea: resolveExportArea(providedOptions.exportArea, context.options.exportAreaByDefault),
1781
- mergeMask: typeof providedOptions.mergeMask === 'boolean'
1782
- ? providedOptions.mergeMask
1783
- : context.options.mergeMaskByDefault,
1784
- multiplier: resolveMultiplier(providedOptions.multiplier, context.options.exportMultiplier),
1785
- format: resolveExportFormat(providedOptions, context.options.downsampleQuality),
2026
+ width: Math.max(1, Math.round(srcWidth * ratio)),
2027
+ height: Math.max(1, Math.round(srcHeight * ratio)),
2028
+ needsResize: true,
1786
2029
  };
1787
2030
  }
1788
- function readCanvasDimension(canvas, getterName, propertyName) {
1789
- const canvasLike = canvas;
1790
- const getter = canvasLike[getterName];
1791
- const value = typeof getter === 'function' ? getter.call(canvasLike) : canvasLike[propertyName];
1792
- return Math.max(1, Math.ceil(Number.isFinite(value) ? Number(value) : 1));
2031
+ function isPositiveFinite$1(value) {
2032
+ return Number.isFinite(value) && value > 0;
1793
2033
  }
1794
- function assertExportPixelBudget(context, multiplier, region) {
1795
- var _a, _b;
1796
- const sourceWidth = (_a = region === null || region === void 0 ? void 0 : region.width) !== null && _a !== void 0 ? _a : readCanvasDimension(context.canvas, 'getWidth', 'width');
1797
- const sourceHeight = (_b = region === null || region === void 0 ? void 0 : region.height) !== null && _b !== void 0 ? _b : readCanvasDimension(context.canvas, 'getHeight', 'height');
1798
- const outputWidth = Math.max(1, Math.ceil(sourceWidth * multiplier));
1799
- const outputHeight = Math.max(1, Math.ceil(sourceHeight * multiplier));
1800
- const pixelCount = outputWidth * outputHeight;
1801
- const maxPixels = context.options.maxExportPixels;
1802
- if (!Number.isFinite(pixelCount) || pixelCount > maxPixels) {
1803
- throw new RangeError(`[ImageEditor] Export size ${outputWidth}x${outputHeight} ` +
1804
- `(${pixelCount} pixels) exceeds maxExportPixels (${maxPixels}).`);
2034
+ function selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType) {
2035
+ if (downsampleMimeType)
2036
+ return downsampleMimeType;
2037
+ if (preserveSourceFormat && (sourceMime === 'image/png' || sourceMime === 'image/webp')) {
2038
+ return sourceMime;
1805
2039
  }
2040
+ return 'image/jpeg';
1806
2041
  }
1807
- function computeExportRegion(context, exportArea) {
1808
- if (exportArea === 'canvas')
1809
- return { region: null, partialEdges: null };
1810
- const originalImage = context.getOriginalImage();
1811
- if (!originalImage)
1812
- return { region: null, partialEdges: null };
1813
- const bounds = getObjectBBox(originalImage);
1814
- const canvasLike = context.canvas;
1815
- const canvasWidth = typeof canvasLike.getWidth === 'function' ? canvasLike.getWidth() : canvasLike.width;
1816
- const canvasHeight = typeof canvasLike.getHeight === 'function' ? canvasLike.getHeight() : canvasLike.height;
1817
- if (!hasMeaningfulCanvasRegion(bounds, canvasWidth, canvasHeight)) {
1818
- throw new ExportError('exportImageBase64 failed: image export region is empty.');
2042
+ function detectSourceMimeType(dataUrl) {
2043
+ const match = /^data:(image\/[a-z0-9+\-.]+)\s*;/i.exec(dataUrl);
2044
+ return match ? match[1].toLowerCase() : null;
2045
+ }
2046
+ function resampleImage(imageElement, maxWidth, maxHeight, sourceMime, preserveSourceFormat, downsampleMimeType, quality, ownerDocument) {
2047
+ var _a;
2048
+ const { width, height } = computeDownsampleDimensions(imageElement.naturalWidth, imageElement.naturalHeight, maxWidth, maxHeight);
2049
+ const mimeType = selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType);
2050
+ const documentForCanvas = (_a = ownerDocument !== null && ownerDocument !== void 0 ? ownerDocument : imageElement.ownerDocument) !== null && _a !== void 0 ? _a : (typeof document !== 'undefined' ? document : null);
2051
+ if (!documentForCanvas) {
2052
+ throw new DownsampleError('Failed to obtain an owner document for downsampling.');
1819
2053
  }
1820
- return {
2054
+ const offscreenCanvas = documentForCanvas.createElement('canvas');
2055
+ offscreenCanvas.width = width;
2056
+ offscreenCanvas.height = height;
2057
+ const context = offscreenCanvas.getContext('2d');
2058
+ if (!context) {
2059
+ throw new DownsampleError('Failed to obtain a 2D context for downsampling.');
2060
+ }
2061
+ context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, width, height);
2062
+ const dataUrl = mimeType === 'image/png'
2063
+ ? offscreenCanvas.toDataURL(mimeType)
2064
+ : offscreenCanvas.toDataURL(mimeType, quality);
2065
+ return { dataUrl, width, height, mimeType };
2066
+ }
2067
+
2068
+ function withTimeout(promise, ms, label) {
2069
+ return new Promise((resolve, reject) => {
2070
+ const start = Date.now();
2071
+ const timeoutId = setTimeout(() => {
2072
+ reject(new ImageLoadTimeoutError(label, Date.now() - start));
2073
+ }, ms);
2074
+ promise.then((value) => {
2075
+ clearTimeout(timeoutId);
2076
+ resolve(value);
2077
+ }, (err) => {
2078
+ clearTimeout(timeoutId);
2079
+ reject(err);
2080
+ });
2081
+ });
2082
+ }
2083
+
2084
+ const MATRIX_DETERMINANT_EPSILON = 1e-8;
2085
+ const MATRIX_SCALE_EPSILON = 1e-8;
2086
+ function toMatrix2D(matrix) {
2087
+ if (matrix.length < 6)
2088
+ return null;
2089
+ const a = matrix[0];
2090
+ const b = matrix[1];
2091
+ const c = matrix[2];
2092
+ const d = matrix[3];
2093
+ const e = matrix[4];
2094
+ const f = matrix[5];
2095
+ if (!Number.isFinite(a) ||
2096
+ !Number.isFinite(b) ||
2097
+ !Number.isFinite(c) ||
2098
+ !Number.isFinite(d) ||
2099
+ !Number.isFinite(e) ||
2100
+ !Number.isFinite(f)) {
2101
+ return null;
2102
+ }
2103
+ return { a: a, b: b, c: c, d: d, e: e, f: f };
2104
+ }
2105
+ function invertMatrix(matrix) {
2106
+ const determinant = matrix.a * matrix.d - matrix.b * matrix.c;
2107
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < MATRIX_DETERMINANT_EPSILON) {
2108
+ return null;
2109
+ }
2110
+ return {
2111
+ a: matrix.d / determinant,
2112
+ b: -matrix.b / determinant,
2113
+ c: -matrix.c / determinant,
2114
+ d: matrix.a / determinant,
2115
+ e: (matrix.c * matrix.f - matrix.d * matrix.e) / determinant,
2116
+ f: (matrix.b * matrix.e - matrix.a * matrix.f) / determinant,
2117
+ };
2118
+ }
2119
+ function transformPoint(point, matrix) {
2120
+ return {
2121
+ x: matrix.a * point.x + matrix.c * point.y + matrix.e,
2122
+ y: matrix.b * point.x + matrix.d * point.y + matrix.f,
2123
+ };
2124
+ }
2125
+ function getSourceRadiusFromMatrix(matrix, canvasRadius) {
2126
+ const scaleX = Math.hypot(matrix.a, matrix.b);
2127
+ const scaleY = Math.hypot(matrix.c, matrix.d);
2128
+ const minScale = Math.min(scaleX > MATRIX_SCALE_EPSILON ? scaleX : Number.POSITIVE_INFINITY, scaleY > MATRIX_SCALE_EPSILON ? scaleY : Number.POSITIVE_INFINITY);
2129
+ if (!Number.isFinite(minScale) || minScale <= 0)
2130
+ return canvasRadius;
2131
+ return canvasRadius / minScale;
2132
+ }
2133
+ function getMosaicImagePoint(fabric, image, canvasPoint, brushDiameterCanvasPx) {
2134
+ const width = Number(image.width) || 0;
2135
+ const height = Number(image.height) || 0;
2136
+ const brushDiameter = Number(brushDiameterCanvasPx);
2137
+ if (width <= 0 ||
2138
+ height <= 0 ||
2139
+ !Number.isFinite(canvasPoint.x) ||
2140
+ !Number.isFinite(canvasPoint.y) ||
2141
+ !Number.isFinite(brushDiameter) ||
2142
+ brushDiameter <= 0) {
2143
+ return null;
2144
+ }
2145
+ const matrix = toMatrix2D(image.calcTransformMatrix());
2146
+ if (!matrix)
2147
+ return null;
2148
+ const inverse = invertMatrix(matrix);
2149
+ if (!inverse)
2150
+ return null;
2151
+ const localPoint = transformPoint(canvasPoint, inverse);
2152
+ const sourceX = localPoint.x + width / 2;
2153
+ const sourceY = localPoint.y + height / 2;
2154
+ if (sourceX < 0 || sourceY < 0 || sourceX > width || sourceY > height) {
2155
+ return null;
2156
+ }
2157
+ return {
2158
+ sourceX,
2159
+ sourceY,
2160
+ sourceRadius: getSourceRadiusFromMatrix(matrix, brushDiameter / 2),
2161
+ };
2162
+ }
2163
+
2164
+ function normalizeBlockSize(value) {
2165
+ return Number.isFinite(value) && value > 0 ? Math.max(1, Math.floor(value)) : 1;
2166
+ }
2167
+ function isInsideCircle(x, y, centerX, centerY, radiusSquared) {
2168
+ const dx = x - centerX;
2169
+ const dy = y - centerY;
2170
+ return dx * dx + dy * dy <= radiusSquared;
2171
+ }
2172
+ function pixelOffset(width, x, y) {
2173
+ return (y * width + x) * 4;
2174
+ }
2175
+ function applyCircularMosaicToImageData(options) {
2176
+ var _a, _b, _c, _d;
2177
+ const { imageData } = options;
2178
+ const { width, height, data } = imageData;
2179
+ const centerX = Number(options.centerX);
2180
+ const centerY = Number(options.centerY);
2181
+ const radius = Number(options.radius);
2182
+ if (!Number.isFinite(centerX) ||
2183
+ !Number.isFinite(centerY) ||
2184
+ !Number.isFinite(radius) ||
2185
+ radius <= 0 ||
2186
+ width <= 0 ||
2187
+ height <= 0) {
2188
+ return false;
2189
+ }
2190
+ const blockSize = normalizeBlockSize(options.blockSize);
2191
+ const minX = Math.max(0, Math.floor(centerX - radius));
2192
+ const maxX = Math.min(width - 1, Math.ceil(centerX + radius));
2193
+ const minY = Math.max(0, Math.floor(centerY - radius));
2194
+ const maxY = Math.min(height - 1, Math.ceil(centerY + radius));
2195
+ if (minX > maxX || minY > maxY)
2196
+ return false;
2197
+ const radiusSquared = radius * radius;
2198
+ let processed = false;
2199
+ for (let blockY = minY; blockY <= maxY; blockY += blockSize) {
2200
+ for (let blockX = minX; blockX <= maxX; blockX += blockSize) {
2201
+ const blockMaxX = Math.min(maxX, blockX + blockSize - 1);
2202
+ const blockMaxY = Math.min(maxY, blockY + blockSize - 1);
2203
+ let sampleOffset = -1;
2204
+ for (let y = blockY; y <= blockMaxY && sampleOffset < 0; y += 1) {
2205
+ for (let x = blockX; x <= blockMaxX; x += 1) {
2206
+ if (!isInsideCircle(x, y, centerX, centerY, radiusSquared))
2207
+ continue;
2208
+ sampleOffset = pixelOffset(width, x, y);
2209
+ break;
2210
+ }
2211
+ }
2212
+ if (sampleOffset < 0)
2213
+ continue;
2214
+ const red = (_a = data[sampleOffset]) !== null && _a !== void 0 ? _a : 0;
2215
+ const green = (_b = data[sampleOffset + 1]) !== null && _b !== void 0 ? _b : 0;
2216
+ const blue = (_c = data[sampleOffset + 2]) !== null && _c !== void 0 ? _c : 0;
2217
+ const alpha = (_d = data[sampleOffset + 3]) !== null && _d !== void 0 ? _d : 0;
2218
+ for (let y = blockY; y <= blockMaxY; y += 1) {
2219
+ for (let x = blockX; x <= blockMaxX; x += 1) {
2220
+ if (!isInsideCircle(x, y, centerX, centerY, radiusSquared))
2221
+ continue;
2222
+ const offset = pixelOffset(width, x, y);
2223
+ data[offset] = red;
2224
+ data[offset + 1] = green;
2225
+ data[offset + 2] = blue;
2226
+ data[offset + 3] = alpha;
2227
+ processed = true;
2228
+ }
2229
+ }
2230
+ }
2231
+ }
2232
+ return processed;
2233
+ }
2234
+
2235
+ const MAX_PENDING_MOSAIC_POINTS = 4096;
2236
+ function getCanvasDocument$2(context) {
2237
+ var _a, _b, _c, _d, _e;
2238
+ const element = (_b = (_a = context.canvas).getElement) === null || _b === void 0 ? void 0 : _b.call(_a);
2239
+ return ((_e = (_c = element === null || element === void 0 ? void 0 : element.ownerDocument) !== null && _c !== void 0 ? _c : (_d = context.canvas.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : document);
2240
+ }
2241
+ function isFinitePoint(value) {
2242
+ const point = value;
2243
+ return (!!point &&
2244
+ typeof point.x === 'number' &&
2245
+ Number.isFinite(point.x) &&
2246
+ typeof point.y === 'number' &&
2247
+ Number.isFinite(point.y));
2248
+ }
2249
+ function getPointerFromFabricEvent(canvas, event) {
2250
+ const fabricEvent = event;
2251
+ if (isFinitePoint(fabricEvent.scenePoint))
2252
+ return fabricEvent.scenePoint;
2253
+ if (isFinitePoint(fabricEvent.pointer))
2254
+ return fabricEvent.pointer;
2255
+ if (isFinitePoint(fabricEvent.absolutePointer))
2256
+ return fabricEvent.absolutePointer;
2257
+ if (fabricEvent.e && typeof canvas.getPointer === 'function') {
2258
+ const pointer = canvas.getPointer(fabricEvent.e);
2259
+ if (isFinitePoint(pointer))
2260
+ return pointer;
2261
+ }
2262
+ return null;
2263
+ }
2264
+ function safeRender(canvas) {
2265
+ try {
2266
+ canvas.requestRenderAll();
2267
+ }
2268
+ catch {
2269
+ try {
2270
+ canvas.renderAll();
2271
+ }
2272
+ catch {
2273
+ }
2274
+ }
2275
+ }
2276
+ function createPreviewCircle(context) {
2277
+ var _a;
2278
+ const config = context.getMosaicConfig();
2279
+ const circle = new context.fabric.Circle({
2280
+ left: 0,
2281
+ top: 0,
2282
+ radius: config.brushSize / 2,
2283
+ originX: 'center',
2284
+ originY: 'center',
2285
+ fill: config.previewFill,
2286
+ stroke: config.previewStroke,
2287
+ strokeWidth: config.previewStrokeWidth,
2288
+ strokeDashArray: (_a = config.previewStrokeDashArray) !== null && _a !== void 0 ? _a : undefined,
2289
+ selectable: false,
2290
+ evented: false,
2291
+ excludeFromExport: true,
2292
+ objectCaching: false,
2293
+ visible: false,
2294
+ });
2295
+ circle.isMosaicPreview = true;
2296
+ return circle;
2297
+ }
2298
+ function ensurePreviewCircle(context, session) {
2299
+ var _a;
2300
+ const { canvas } = context;
2301
+ const circle = (_a = session.previewCircle) !== null && _a !== void 0 ? _a : createPreviewCircle(context);
2302
+ session.previewCircle = circle;
2303
+ if (!canvas.getObjects().includes(circle)) {
2304
+ canvas.add(circle);
2305
+ }
2306
+ canvas.bringObjectToFront(circle);
2307
+ updateMosaicPreview(context);
2308
+ return circle;
2309
+ }
2310
+ function removePreviewCircle(context, session) {
2311
+ const circle = session.previewCircle;
2312
+ if (!circle)
2313
+ return;
2314
+ try {
2315
+ context.canvas.remove(circle);
2316
+ }
2317
+ catch {
2318
+ }
2319
+ session.previewCircle = null;
2320
+ }
2321
+ function createPreviewImage(context, sourceImage, rasterCache) {
2322
+ const image = new context.fabric.FabricImage(rasterCache.offscreenCanvas, {
2323
+ selectable: false,
2324
+ evented: false,
2325
+ excludeFromExport: true,
2326
+ objectCaching: false,
2327
+ visible: true,
2328
+ });
2329
+ copyBaseImageProperties(image, sourceImage);
2330
+ image.set({
2331
+ selectable: false,
2332
+ evented: false,
2333
+ excludeFromExport: true,
2334
+ objectCaching: false,
2335
+ visible: true,
2336
+ });
2337
+ image.isMosaicPreview = true;
2338
+ return image;
2339
+ }
2340
+ function placePreviewImageAfterBase(context, previewImage, sourceImage) {
2341
+ var _a, _b;
2342
+ const sourceIndex = context.canvas.getObjects().indexOf(sourceImage);
2343
+ if (sourceIndex < 0)
2344
+ return;
2345
+ try {
2346
+ (_b = (_a = context.canvas).moveObjectTo) === null || _b === void 0 ? void 0 : _b.call(_a, previewImage, sourceIndex + 1);
2347
+ }
2348
+ catch {
2349
+ }
2350
+ }
2351
+ function ensurePreviewImage(context, session, sourceImage) {
2352
+ var _a;
2353
+ const rasterCache = session.rasterCache;
2354
+ if (!rasterCache)
2355
+ return null;
2356
+ const previewImage = (_a = session.previewImage) !== null && _a !== void 0 ? _a : createPreviewImage(context, sourceImage, rasterCache);
2357
+ session.previewImage = previewImage;
2358
+ copyBaseImageProperties(previewImage, sourceImage);
2359
+ previewImage.set({
2360
+ selectable: false,
2361
+ evented: false,
2362
+ excludeFromExport: true,
2363
+ objectCaching: false,
2364
+ visible: true,
2365
+ });
2366
+ previewImage.dirty = true;
2367
+ if (!context.canvas.getObjects().includes(previewImage)) {
2368
+ context.canvas.add(previewImage);
2369
+ }
2370
+ placePreviewImageAfterBase(context, previewImage, sourceImage);
2371
+ const circle = session.previewCircle;
2372
+ if (circle && context.canvas.getObjects().includes(circle)) {
2373
+ context.canvas.bringObjectToFront(circle);
2374
+ }
2375
+ return previewImage;
2376
+ }
2377
+ function removePreviewImage(context, session) {
2378
+ const image = session.previewImage;
2379
+ if (!image)
2380
+ return;
2381
+ try {
2382
+ context.canvas.remove(image);
2383
+ }
2384
+ catch {
2385
+ }
2386
+ session.previewImage = null;
2387
+ }
2388
+ function hidePreview(context) {
2389
+ var _a;
2390
+ const circle = (_a = context.getMosaicSession()) === null || _a === void 0 ? void 0 : _a.previewCircle;
2391
+ if (!circle)
2392
+ return;
2393
+ circle.set({ visible: false });
2394
+ safeRender(context.canvas);
2395
+ }
2396
+ function movePreview(context, point) {
2397
+ const session = context.getMosaicSession();
2398
+ if (!session)
2399
+ return;
2400
+ const circle = ensurePreviewCircle(context, session);
2401
+ circle.set({ left: point.x, top: point.y, visible: true });
2402
+ safeRender(context.canvas);
2403
+ }
2404
+ function attachCanvasHandler(context, session, eventName, callback) {
2405
+ context.canvas.on(eventName, callback);
2406
+ session.handlers.push({ eventName, callback });
2407
+ }
2408
+ function detachCanvasHandlers(context, session) {
2409
+ for (const record of session.handlers) {
2410
+ try {
2411
+ context.canvas.off(record.eventName, record.callback);
2412
+ }
2413
+ catch {
2414
+ }
2415
+ }
2416
+ session.handlers = [];
2417
+ }
2418
+ function restoreObjectStates(session) {
2419
+ for (const record of session.prevObjectStates) {
2420
+ try {
2421
+ record.object.set({ evented: record.evented, selectable: record.selectable });
2422
+ }
2423
+ catch {
2424
+ }
2425
+ }
2426
+ session.prevObjectStates = [];
2427
+ }
2428
+ function getImageSource(image) {
2429
+ var _a;
2430
+ const imageWithSource = image;
2431
+ try {
2432
+ const src = (_a = imageWithSource.getSrc) === null || _a === void 0 ? void 0 : _a.call(imageWithSource);
2433
+ if (typeof src === 'string' && src.length > 0)
2434
+ return src;
2435
+ }
2436
+ catch {
2437
+ }
2438
+ return typeof imageWithSource.src === 'string' && imageWithSource.src.length > 0
2439
+ ? imageWithSource.src
2440
+ : null;
2441
+ }
2442
+ function imageDimension(value) {
2443
+ const numeric = Number(value);
2444
+ return Number.isFinite(numeric) && numeric > 0 ? Math.floor(numeric) : 0;
2445
+ }
2446
+ function decodeImageSource(ownerDocument, source) {
2447
+ return new Promise((resolve, reject) => {
2448
+ const imageElement = ownerDocument.createElement('img');
2449
+ const cleanup = () => {
2450
+ if (typeof imageElement.removeEventListener === 'function') {
2451
+ imageElement.removeEventListener('load', handleLoad);
2452
+ imageElement.removeEventListener('error', handleError);
2453
+ }
2454
+ else {
2455
+ imageElement.onload = null;
2456
+ imageElement.onerror = null;
2457
+ }
2458
+ };
2459
+ const handleLoad = () => {
2460
+ const width = imageDimension(imageElement.naturalWidth || imageElement.width);
2461
+ const height = imageDimension(imageElement.naturalHeight || imageElement.height);
2462
+ cleanup();
2463
+ if (width <= 0 || height <= 0) {
2464
+ reject(new Error('Mosaic image decode failed: source image has no dimensions.'));
2465
+ return;
2466
+ }
2467
+ resolve({ element: imageElement, width, height });
2468
+ };
2469
+ const handleError = (event) => {
2470
+ cleanup();
2471
+ const message = typeof event === 'string'
2472
+ ? `Mosaic image decode failed: ${event}`
2473
+ : 'Mosaic image decode failed.';
2474
+ reject(new Error(message));
2475
+ };
2476
+ if (!source.startsWith('data:')) {
2477
+ imageElement.crossOrigin = 'anonymous';
2478
+ }
2479
+ if (typeof imageElement.addEventListener === 'function') {
2480
+ imageElement.addEventListener('load', handleLoad, { once: true });
2481
+ imageElement.addEventListener('error', handleError, { once: true });
2482
+ }
2483
+ else {
2484
+ imageElement.onload = handleLoad;
2485
+ imageElement.onerror = handleError;
2486
+ }
2487
+ imageElement.src = source;
2488
+ });
2489
+ }
2490
+ function toSupportedMimeType(mimeType) {
2491
+ return mimeType === 'image/jpeg' || mimeType === 'image/png' || mimeType === 'image/webp'
2492
+ ? mimeType
2493
+ : null;
2494
+ }
2495
+ function mimeToFormat(mimeType) {
2496
+ if (mimeType === 'image/jpeg')
2497
+ return 'jpeg';
2498
+ if (mimeType === 'image/webp')
2499
+ return 'webp';
2500
+ return 'png';
2501
+ }
2502
+ function resolveMosaicOutputFormat(context, source) {
2503
+ var _a, _b, _c, _d;
2504
+ const config = context.getMosaicConfig();
2505
+ const requested = config.outputFileType;
2506
+ const format = requested === 'source'
2507
+ ? mimeToFormat((_b = (_a = context.getCurrentImageMimeType()) !== null && _a !== void 0 ? _a : toSupportedMimeType(detectSourceMimeType(source))) !== null && _b !== void 0 ? _b : 'image/png')
2508
+ : ((_c = tryNormalizeImageFormat(String(requested))) !== null && _c !== void 0 ? _c : 'png');
2509
+ const mimeType = mimeTypeFor(format);
2510
+ if (format === 'png')
2511
+ return { mimeType };
2512
+ return {
2513
+ mimeType,
2514
+ quality: (_d = config.outputQuality) !== null && _d !== void 0 ? _d : context.options.downsampleQuality,
2515
+ };
2516
+ }
2517
+ async function createFabricImageFromDataUrl(context, dataUrl) {
2518
+ return await withTimeout(context.fabric.FabricImage.fromURL(dataUrl, { crossOrigin: 'anonymous' }), context.options.imageLoadTimeoutMs, 'Mosaic FabricImage.fromURL');
2519
+ }
2520
+ function copyBaseImageProperties(target, source) {
2521
+ target.set({
2522
+ left: source.left,
2523
+ top: source.top,
2524
+ scaleX: source.scaleX,
2525
+ scaleY: source.scaleY,
2526
+ angle: source.angle,
2527
+ skewX: source.skewX,
2528
+ skewY: source.skewY,
2529
+ flipX: source.flipX,
2530
+ flipY: source.flipY,
2531
+ originX: source.originX,
2532
+ originY: source.originY,
2533
+ selectable: source.selectable,
2534
+ evented: source.evented,
2535
+ hasControls: source.hasControls,
2536
+ hoverCursor: source.hoverCursor,
2537
+ });
2538
+ target.setCoords();
2539
+ }
2540
+ function replaceBaseImage(context, oldImage, newImage, mimeType) {
2541
+ const { canvas } = context;
2542
+ let oldRemoved = false;
2543
+ let newAdded = false;
2544
+ try {
2545
+ copyBaseImageProperties(newImage, oldImage);
2546
+ canvas.remove(oldImage);
2547
+ oldRemoved = true;
2548
+ canvas.add(newImage);
2549
+ newAdded = true;
2550
+ canvas.sendObjectToBack(newImage);
2551
+ context.setOriginalImage(newImage);
2552
+ context.setCurrentImageMimeType(mimeType);
2553
+ canvas.renderAll();
2554
+ }
2555
+ catch (error) {
2556
+ try {
2557
+ if (newAdded)
2558
+ canvas.remove(newImage);
2559
+ if (oldRemoved && !canvas.getObjects().includes(oldImage)) {
2560
+ canvas.add(oldImage);
2561
+ canvas.sendObjectToBack(oldImage);
2562
+ }
2563
+ context.setOriginalImage(oldImage);
2564
+ }
2565
+ catch {
2566
+ }
2567
+ throw error;
2568
+ }
2569
+ }
2570
+ function pushMosaicHistory(context, after) {
2571
+ var _a;
2572
+ const before = (_a = context.getLastSnapshot()) !== null && _a !== void 0 ? _a : after;
2573
+ if (!before || !after || before === after)
2574
+ return;
2575
+ context.historyManager.push(new Command(async () => {
2576
+ await context.loadFromState(after);
2577
+ }, async () => {
2578
+ await context.loadFromState(before);
2579
+ }));
2580
+ context.setLastSnapshot(after);
2581
+ }
2582
+ async function getOrCreateRasterCache(context, session, source) {
2583
+ if (session.rasterCache)
2584
+ return session.rasterCache;
2585
+ const ownerDocument = getCanvasDocument$2(context);
2586
+ const decoded = await decodeImageSource(ownerDocument, source);
2587
+ const offscreenCanvas = ownerDocument.createElement('canvas');
2588
+ offscreenCanvas.width = decoded.width;
2589
+ offscreenCanvas.height = decoded.height;
2590
+ const renderingContext = offscreenCanvas.getContext('2d');
2591
+ if (!renderingContext) {
2592
+ reportError(context.options, new Error('Mosaic could not obtain a 2D canvas context.'), 'Mosaic apply failed.');
2593
+ return null;
2594
+ }
2595
+ renderingContext.drawImage(decoded.element, 0, 0, decoded.width, decoded.height);
2596
+ let imageData;
2597
+ try {
2598
+ imageData = renderingContext.getImageData(0, 0, decoded.width, decoded.height);
2599
+ }
2600
+ catch (error) {
2601
+ reportError(context.options, error, 'Mosaic apply failed because the source image pixels could not be read.');
2602
+ return null;
2603
+ }
2604
+ const rasterCache = {
2605
+ offscreenCanvas,
2606
+ renderingContext,
2607
+ imageData,
2608
+ source,
2609
+ width: decoded.width,
2610
+ height: decoded.height,
2611
+ };
2612
+ session.rasterCache = rasterCache;
2613
+ return rasterCache;
2614
+ }
2615
+ function applyMosaicImagePoint(context, session, sourceImage, imagePoint) {
2616
+ const rasterCache = session.rasterCache;
2617
+ if (!rasterCache)
2618
+ return false;
2619
+ const config = context.getMosaicConfig();
2620
+ const previousPoint = session.lastImagePoint;
2621
+ const points = previousPoint
2622
+ ? interpolateMosaicPoints(previousPoint, imagePoint)
2623
+ : [imagePoint];
2624
+ let changed = false;
2625
+ for (const point of points) {
2626
+ changed =
2627
+ applyCircularMosaicToImageData({
2628
+ imageData: rasterCache.imageData,
2629
+ centerX: point.sourceX,
2630
+ centerY: point.sourceY,
2631
+ radius: point.sourceRadius,
2632
+ blockSize: config.blockSize,
2633
+ }) || changed;
2634
+ }
2635
+ session.lastImagePoint = imagePoint;
2636
+ if (changed) {
2637
+ session.hasUncommittedChanges = true;
2638
+ rasterCache.renderingContext.putImageData(rasterCache.imageData, 0, 0);
2639
+ ensurePreviewImage(context, session, sourceImage);
2640
+ safeRender(context.canvas);
2641
+ }
2642
+ return changed;
2643
+ }
2644
+ function interpolateMosaicPoints(start, end) {
2645
+ const dx = end.sourceX - start.sourceX;
2646
+ const dy = end.sourceY - start.sourceY;
2647
+ const distance = Math.hypot(dx, dy);
2648
+ const minRadius = Math.min(start.sourceRadius, end.sourceRadius);
2649
+ const spacing = Math.max(1, minRadius / 2);
2650
+ const steps = Math.max(1, Math.ceil(distance / spacing));
2651
+ const points = [];
2652
+ for (let index = 1; index <= steps; index += 1) {
2653
+ const t = index / steps;
2654
+ points.push({
2655
+ sourceX: start.sourceX + dx * t,
2656
+ sourceY: start.sourceY + dy * t,
2657
+ sourceRadius: start.sourceRadius + (end.sourceRadius - start.sourceRadius) * t,
2658
+ });
2659
+ }
2660
+ return points;
2661
+ }
2662
+ async function applyMosaicPointToCache(context, expectedSession, canvasPoint) {
2663
+ const session = context.getMosaicSession();
2664
+ if (!session || session !== expectedSession)
2665
+ return;
2666
+ const originalImage = context.getOriginalImage();
2667
+ if (!originalImage || !context.isImageLoaded())
2668
+ return;
2669
+ const config = context.getMosaicConfig();
2670
+ const imagePoint = getMosaicImagePoint(context.fabric, originalImage, canvasPoint, config.brushSize);
2671
+ if (!imagePoint) {
2672
+ session.lastImagePoint = null;
2673
+ return;
2674
+ }
2675
+ const source = getImageSource(originalImage);
2676
+ if (!source) {
2677
+ reportWarning(context.options, new Error('Mosaic cannot read the current image source.'), 'Mosaic skipped because the image source is unavailable.');
2678
+ return;
2679
+ }
2680
+ const rasterCache = await getOrCreateRasterCache(context, session, source);
2681
+ if (!rasterCache)
2682
+ return;
2683
+ applyMosaicImagePoint(context, session, originalImage, imagePoint);
2684
+ }
2685
+ async function commitMosaicChanges(context, session, callbackContext) {
2686
+ var _a;
2687
+ session.commitRequested = false;
2688
+ session.lastImagePoint = null;
2689
+ if (!session.hasUncommittedChanges || !session.rasterCache)
2690
+ return;
2691
+ const originalImage = context.getOriginalImage();
2692
+ if (!originalImage || !context.isImageLoaded())
2693
+ return;
2694
+ const source = (_a = getImageSource(originalImage)) !== null && _a !== void 0 ? _a : session.rasterCache.source;
2695
+ const rasterCache = session.rasterCache;
2696
+ rasterCache.renderingContext.putImageData(rasterCache.imageData, 0, 0);
2697
+ const output = resolveMosaicOutputFormat(context, source);
2698
+ const nextDataUrl = output.quality === undefined
2699
+ ? rasterCache.offscreenCanvas.toDataURL(output.mimeType)
2700
+ : rasterCache.offscreenCanvas.toDataURL(output.mimeType, output.quality);
2701
+ const nextImage = await createFabricImageFromDataUrl(context, nextDataUrl);
2702
+ removePreviewCircle(context, session);
2703
+ removePreviewImage(context, session);
2704
+ try {
2705
+ replaceBaseImage(context, originalImage, nextImage, output.mimeType);
2706
+ const after = context.captureSnapshot();
2707
+ pushMosaicHistory(context, after);
2708
+ rasterCache.source = nextDataUrl;
2709
+ session.hasUncommittedChanges = false;
2710
+ }
2711
+ finally {
2712
+ if (context.getMosaicSession() === session) {
2713
+ ensurePreviewCircle(context, session);
2714
+ }
2715
+ }
2716
+ context.updateInputs();
2717
+ context.updateUi();
2718
+ context.emitImageChanged(callbackContext);
2719
+ }
2720
+ async function drainMosaicQueue(context, expectedSession) {
2721
+ const session = context.getMosaicSession();
2722
+ if (!session || session !== expectedSession || session.isApplying)
2723
+ return;
2724
+ session.isApplying = true;
2725
+ const callbackContext = context.buildCallbackContext('applyMosaic', false);
2726
+ context.emitBusyChangeIfChanged(callbackContext);
2727
+ context.updateUi();
2728
+ try {
2729
+ while (context.getMosaicSession() === session && session.pendingCanvasPoints.length > 0) {
2730
+ const point = session.pendingCanvasPoints.shift();
2731
+ if (point) {
2732
+ await applyMosaicPointToCache(context, session, point);
2733
+ }
2734
+ }
2735
+ if (context.getMosaicSession() === session && session.commitRequested) {
2736
+ await commitMosaicChanges(context, session, callbackContext);
2737
+ }
2738
+ }
2739
+ finally {
2740
+ if (context.getMosaicSession() === session) {
2741
+ session.isApplying = false;
2742
+ }
2743
+ context.emitBusyChangeIfChanged(callbackContext);
2744
+ context.updateUi();
2745
+ if (context.getMosaicSession() === session &&
2746
+ (session.pendingCanvasPoints.length > 0 || session.commitRequested)) {
2747
+ void drainMosaicQueue(context, session).catch((error) => {
2748
+ reportError(context.options, error, 'Mosaic apply failed.');
2749
+ });
2750
+ }
2751
+ }
2752
+ }
2753
+ function enqueueMosaicPoint(context, canvasPoint) {
2754
+ const session = context.getMosaicSession();
2755
+ if (!session)
2756
+ return;
2757
+ session.pendingCanvasPoints.push(canvasPoint);
2758
+ if (session.pendingCanvasPoints.length > MAX_PENDING_MOSAIC_POINTS) {
2759
+ session.pendingCanvasPoints.splice(0, session.pendingCanvasPoints.length - MAX_PENDING_MOSAIC_POINTS);
2760
+ }
2761
+ void drainMosaicQueue(context, session).catch((error) => {
2762
+ reportError(context.options, error, 'Mosaic apply failed.');
2763
+ });
2764
+ }
2765
+ function requestMosaicCommit(context, session) {
2766
+ session.commitRequested = true;
2767
+ void drainMosaicQueue(context, session).catch((error) => {
2768
+ reportError(context.options, error, 'Mosaic apply failed.');
2769
+ });
2770
+ }
2771
+ function installMosaicHandlers(context, session) {
2772
+ attachCanvasHandler(context, session, 'mouse:move', (event) => {
2773
+ const pointer = getPointerFromFabricEvent(context.canvas, event);
2774
+ if (!pointer) {
2775
+ hidePreview(context);
2776
+ return;
2777
+ }
2778
+ movePreview(context, pointer);
2779
+ const currentSession = context.getMosaicSession();
2780
+ if (currentSession === null || currentSession === void 0 ? void 0 : currentSession.isPointerDown) {
2781
+ enqueueMosaicPoint(context, pointer);
2782
+ }
2783
+ });
2784
+ attachCanvasHandler(context, session, 'mouse:out', () => {
2785
+ hidePreview(context);
2786
+ const currentSession = context.getMosaicSession();
2787
+ if (currentSession === null || currentSession === void 0 ? void 0 : currentSession.isPointerDown) {
2788
+ currentSession.isPointerDown = false;
2789
+ requestMosaicCommit(context, currentSession);
2790
+ }
2791
+ });
2792
+ attachCanvasHandler(context, session, 'mouse:down', (event) => {
2793
+ const pointer = getPointerFromFabricEvent(context.canvas, event);
2794
+ if (!pointer)
2795
+ return;
2796
+ const currentSession = context.getMosaicSession();
2797
+ if (!currentSession)
2798
+ return;
2799
+ currentSession.isPointerDown = true;
2800
+ currentSession.lastImagePoint = null;
2801
+ enqueueMosaicPoint(context, pointer);
2802
+ });
2803
+ attachCanvasHandler(context, session, 'mouse:up', (event) => {
2804
+ const currentSession = context.getMosaicSession();
2805
+ if (!currentSession)
2806
+ return;
2807
+ const pointer = getPointerFromFabricEvent(context.canvas, event);
2808
+ if (pointer) {
2809
+ movePreview(context, pointer);
2810
+ enqueueMosaicPoint(context, pointer);
2811
+ }
2812
+ currentSession.isPointerDown = false;
2813
+ requestMosaicCommit(context, currentSession);
2814
+ });
2815
+ }
2816
+ function enterMosaicMode(context) {
2817
+ if (context.getMosaicSession())
2818
+ return;
2819
+ if (!context.isImageLoaded() || !context.getOriginalImage())
2820
+ return;
2821
+ const { canvas } = context;
2822
+ context.hideAllMaskLabels();
2823
+ canvas.discardActiveObject();
2824
+ const prevSelection = !!canvas.selection;
2825
+ const prevDefaultCursor = canvas.defaultCursor;
2826
+ const prevObjectStates = canvas.getObjects().map((object) => {
2827
+ var _a, _b;
2828
+ return ({
2829
+ object,
2830
+ evented: (_a = object.evented) !== null && _a !== void 0 ? _a : true,
2831
+ selectable: (_b = object.selectable) !== null && _b !== void 0 ? _b : true,
2832
+ });
2833
+ });
2834
+ for (const record of prevObjectStates) {
2835
+ try {
2836
+ record.object.set({ evented: false, selectable: false });
2837
+ }
2838
+ catch {
2839
+ }
2840
+ }
2841
+ canvas.selection = false;
2842
+ canvas.defaultCursor = 'crosshair';
2843
+ const session = {
2844
+ previewCircle: null,
2845
+ previewImage: null,
2846
+ prevSelection,
2847
+ prevDefaultCursor,
2848
+ prevObjectStates,
2849
+ handlers: [],
2850
+ rasterCache: null,
2851
+ pendingCanvasPoints: [],
2852
+ isPointerDown: false,
2853
+ isApplying: false,
2854
+ commitRequested: false,
2855
+ hasUncommittedChanges: false,
2856
+ lastImagePoint: null,
2857
+ };
2858
+ context.setMosaicSession(session);
2859
+ ensurePreviewCircle(context, session);
2860
+ installMosaicHandlers(context, session);
2861
+ canvas.renderAll();
2862
+ }
2863
+ function exitMosaicMode(context) {
2864
+ var _a;
2865
+ const session = context.getMosaicSession();
2866
+ if (!session)
2867
+ return;
2868
+ detachCanvasHandlers(context, session);
2869
+ removePreviewCircle(context, session);
2870
+ removePreviewImage(context, session);
2871
+ restoreObjectStates(session);
2872
+ context.canvas.selection = !!session.prevSelection;
2873
+ context.canvas.defaultCursor = (_a = session.prevDefaultCursor) !== null && _a !== void 0 ? _a : 'default';
2874
+ context.setMosaicSession(null);
2875
+ context.canvas.renderAll();
2876
+ }
2877
+ function updateMosaicPreview(context) {
2878
+ var _a;
2879
+ const session = context.getMosaicSession();
2880
+ const circle = session === null || session === void 0 ? void 0 : session.previewCircle;
2881
+ if (!session || !circle)
2882
+ return;
2883
+ const config = context.getMosaicConfig();
2884
+ circle.set({
2885
+ radius: config.brushSize / 2,
2886
+ fill: config.previewFill,
2887
+ stroke: config.previewStroke,
2888
+ strokeWidth: config.previewStrokeWidth,
2889
+ strokeDashArray: (_a = config.previewStrokeDashArray) !== null && _a !== void 0 ? _a : undefined,
2890
+ });
2891
+ context.canvas.bringObjectToFront(circle);
2892
+ safeRender(context.canvas);
2893
+ }
2894
+
2895
+ function resolveMultiplier(requested, fallback) {
2896
+ const num = Number(requested);
2897
+ if (Number.isFinite(num) && num > 0)
2898
+ return num;
2899
+ const fallbackValue = Number(fallback);
2900
+ return Number.isFinite(fallbackValue) && fallbackValue > 0 ? fallbackValue : 1;
2901
+ }
2902
+ function resolveExportArea(requested, fallback) {
2903
+ if (requested === 'canvas' || requested === 'image')
2904
+ return requested;
2905
+ return fallback === 'canvas' ? 'canvas' : 'image';
2906
+ }
2907
+ function resolveExportOptions(context, options) {
2908
+ const providedOptions = options !== null && options !== void 0 ? options : {};
2909
+ return {
2910
+ exportArea: resolveExportArea(providedOptions.exportArea, context.options.exportAreaByDefault),
2911
+ mergeMask: typeof providedOptions.mergeMask === 'boolean'
2912
+ ? providedOptions.mergeMask
2913
+ : context.options.mergeMaskByDefault,
2914
+ multiplier: resolveMultiplier(providedOptions.multiplier, context.options.exportMultiplier),
2915
+ format: resolveExportFormat(providedOptions, context.options.downsampleQuality),
2916
+ };
2917
+ }
2918
+ function readCanvasDimension(canvas, getterName, propertyName) {
2919
+ const canvasLike = canvas;
2920
+ const getter = canvasLike[getterName];
2921
+ const value = typeof getter === 'function' ? getter.call(canvasLike) : canvasLike[propertyName];
2922
+ return Math.max(1, Math.ceil(Number.isFinite(value) ? Number(value) : 1));
2923
+ }
2924
+ function assertExportPixelBudget(context, multiplier, region) {
2925
+ var _a, _b;
2926
+ const sourceWidth = (_a = region === null || region === void 0 ? void 0 : region.width) !== null && _a !== void 0 ? _a : readCanvasDimension(context.canvas, 'getWidth', 'width');
2927
+ const sourceHeight = (_b = region === null || region === void 0 ? void 0 : region.height) !== null && _b !== void 0 ? _b : readCanvasDimension(context.canvas, 'getHeight', 'height');
2928
+ const outputWidth = Math.max(1, Math.ceil(sourceWidth * multiplier));
2929
+ const outputHeight = Math.max(1, Math.ceil(sourceHeight * multiplier));
2930
+ const pixelCount = outputWidth * outputHeight;
2931
+ const maxPixels = context.options.maxExportPixels;
2932
+ if (!Number.isFinite(pixelCount) || pixelCount > maxPixels) {
2933
+ throw new RangeError(`[ImageEditor] Export size ${outputWidth}x${outputHeight} ` +
2934
+ `(${pixelCount} pixels) exceeds maxExportPixels (${maxPixels}).`);
2935
+ }
2936
+ }
2937
+ function computeExportRegion(context, exportArea) {
2938
+ if (exportArea === 'canvas')
2939
+ return { region: null, partialEdges: null };
2940
+ const originalImage = context.getOriginalImage();
2941
+ if (!originalImage)
2942
+ return { region: null, partialEdges: null };
2943
+ const bounds = getObjectBBox(originalImage);
2944
+ const canvasLike = context.canvas;
2945
+ const canvasWidth = typeof canvasLike.getWidth === 'function' ? canvasLike.getWidth() : canvasLike.width;
2946
+ const canvasHeight = typeof canvasLike.getHeight === 'function' ? canvasLike.getHeight() : canvasLike.height;
2947
+ if (!hasMeaningfulCanvasRegion(bounds, canvasWidth, canvasHeight)) {
2948
+ throw new ExportError('exportImageBase64 failed: image export region is empty.');
2949
+ }
2950
+ return {
1821
2951
  region: getClampedCanvasRegion(bounds, canvasWidth, canvasHeight, {
1822
2952
  includePartialPixels: true,
1823
2953
  }),
@@ -2030,7 +3160,7 @@ function loadImageElement(dataUrl) {
2030
3160
  imageElement.src = dataUrl;
2031
3161
  });
2032
3162
  }
2033
- async function sealPartialTransparentEdges(dataUrl, edges) {
3163
+ async function sealPartialTransparentEdges(dataUrl, edges, target) {
2034
3164
  if (!hasPartialEdges(edges))
2035
3165
  return dataUrl;
2036
3166
  const imageElement = await loadImageElement(dataUrl);
@@ -2078,7 +3208,9 @@ async function sealPartialTransparentEdges(dataUrl, edges) {
2078
3208
  sealPixel(x, height - 1, x, height - 2);
2079
3209
  }
2080
3210
  canvasContext.putImageData(imageData, 0, 0);
2081
- return offscreenCanvas.toDataURL('image/png');
3211
+ return target.quality === undefined
3212
+ ? offscreenCanvas.toDataURL(target.mimeType)
3213
+ : offscreenCanvas.toDataURL(target.mimeType, target.quality);
2082
3214
  }
2083
3215
  function getJpegBackgroundColor(backgroundColor) {
2084
3216
  return resolveCanvasFillStyle(backgroundColor);
@@ -2115,6 +3247,11 @@ function createColorValidationContext() {
2115
3247
  return null;
2116
3248
  }
2117
3249
  }
3250
+ function getCanvasDocument$1(canvas) {
3251
+ var _a, _b, _c, _d, _e;
3252
+ const canvasLike = canvas;
3253
+ return ((_e = (_c = (_b = (_a = canvasLike.getElement) === null || _a === void 0 ? void 0 : _a.call(canvasLike)) === null || _b === void 0 ? void 0 : _b.ownerDocument) !== null && _c !== void 0 ? _c : (_d = canvasLike.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : document);
3254
+ }
2118
3255
  function isTransparentCssColor(value) {
2119
3256
  const normalized = value.trim().toLowerCase();
2120
3257
  if (normalized === 'transparent')
@@ -2158,12 +3295,11 @@ async function convertDataUrlToOpaqueJpeg(dataUrl, backgroundColor, quality) {
2158
3295
  }
2159
3296
  function dataUrlToBytes(dataUrl) {
2160
3297
  var _a;
2161
- const match = /^data:image\/[a-z0-9.+-]+;base64,([A-Za-z0-9+/=\s]+)$/i.exec(dataUrl);
2162
- if (!match || !((_a = match[1]) === null || _a === void 0 ? void 0 : _a.trim())) {
3298
+ const match = /^data:image\/[a-z0-9.+-]+;base64,([A-Za-z0-9+/=]+)$/i.exec(dataUrl);
3299
+ const base64 = (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : '';
3300
+ if (!base64) {
2163
3301
  throw new Error('exportImageFile received a malformed or empty image data URL.');
2164
3302
  }
2165
- const commaAt = dataUrl.indexOf(',');
2166
- const base64 = dataUrl.slice(commaAt + 1).replace(/\s/g, '');
2167
3303
  if (typeof globalThis.atob === 'function') {
2168
3304
  const binary = globalThis.atob(base64);
2169
3305
  const buffer = new ArrayBuffer(binary.length);
@@ -2222,7 +3358,10 @@ async function exportImageBase64(context, options) {
2222
3358
  const renderQuality = renderFormat === 'png' ? undefined : resolved.format.quality;
2223
3359
  let dataUrl = await withMaskExportState(context, resolved.mergeMask, async () => renderCanvasToDataUrl(context.canvas, renderFormat, renderQuality, resolved.multiplier, region));
2224
3360
  if (region) {
2225
- dataUrl = await sealPartialTransparentEdges(dataUrl, partialEdges);
3361
+ const sealedFormat = resolved.format.format === 'jpeg'
3362
+ ? { format: 'png', mimeType: 'image/png', quality: undefined }
3363
+ : resolved.format;
3364
+ dataUrl = await sealPartialTransparentEdges(dataUrl, partialEdges, sealedFormat);
2226
3365
  if (resolved.format.format === 'jpeg') {
2227
3366
  dataUrl = await convertDataUrlToOpaqueJpeg(dataUrl, context.options.backgroundColor, resolved.format.quality);
2228
3367
  }
@@ -2278,15 +3417,17 @@ function downloadImage(context, fileName) {
2278
3417
  .then((dataUrl) => {
2279
3418
  if (!dataUrl)
2280
3419
  return;
2281
- const link = document.createElement('a');
3420
+ const ownerDocument = getCanvasDocument$1(context.canvas);
3421
+ const link = ownerDocument.createElement('a');
2282
3422
  link.download = resolvedFileName;
2283
3423
  link.href = dataUrl;
2284
- document.body.appendChild(link);
3424
+ const body = ownerDocument.body;
3425
+ body.appendChild(link);
2285
3426
  try {
2286
3427
  link.click();
2287
3428
  }
2288
3429
  finally {
2289
- document.body.removeChild(link);
3430
+ body.removeChild(link);
2290
3431
  }
2291
3432
  })
2292
3433
  .catch((error) => {
@@ -2345,26 +3486,10 @@ async function mergeMasks(context) {
2345
3486
  console.warn('[ImageEditor] mergeMasks: rollback failed', rollbackError);
2346
3487
  }
2347
3488
  if (error instanceof MergeMasksError)
2348
- throw error;
2349
- const message = error instanceof Error ? `mergeMasks failed: ${error.message}` : 'mergeMasks failed';
2350
- throw new MergeMasksError(message, error);
2351
- }
2352
- }
2353
-
2354
- function withTimeout(promise, ms, label) {
2355
- return new Promise((resolve, reject) => {
2356
- const start = Date.now();
2357
- const timeoutId = setTimeout(() => {
2358
- reject(new ImageLoadTimeoutError(label, Date.now() - start));
2359
- }, ms);
2360
- promise.then((value) => {
2361
- clearTimeout(timeoutId);
2362
- resolve(value);
2363
- }, (err) => {
2364
- clearTimeout(timeoutId);
2365
- reject(err);
2366
- });
2367
- });
3489
+ throw error;
3490
+ const message = error instanceof Error ? `mergeMasks failed: ${error.message}` : 'mergeMasks failed';
3491
+ throw new MergeMasksError(message, error);
3492
+ }
2368
3493
  }
2369
3494
 
2370
3495
  function forceReflow(element) {
@@ -2373,26 +3498,8 @@ function forceReflow(element) {
2373
3498
  void element.offsetWidth;
2374
3499
  }
2375
3500
 
2376
- function selectLayoutStrategy(options) {
2377
- if (options.fitImageToCanvas)
2378
- return 'fit';
2379
- if (options.coverImageToCanvas)
2380
- return 'cover';
2381
- return 'expand';
2382
- }
2383
- function detectLayoutConflict(options) {
2384
- if (!options.fitImageToCanvas || !options.coverImageToCanvas)
2385
- return null;
2386
- const enabled = ['fit', 'cover'];
2387
- if (options.expandCanvasToImage)
2388
- enabled.push('expand');
2389
- const selected = selectLayoutStrategy(options);
2390
- return {
2391
- enabled,
2392
- selected,
2393
- message: `Layout flags ${enabled.map((s) => `\`${s}\``).join(', ')} are enabled simultaneously. ` +
2394
- `Using precedence \`fit > cover > expand\`; selected \`${selected}\`.`,
2395
- };
3501
+ function selectLayoutStrategy(mode) {
3502
+ return mode;
2396
3503
  }
2397
3504
  class ViewportCache {
2398
3505
  constructor() {
@@ -2596,60 +3703,6 @@ function applyCanvasDimensions(canvas, width, height, containerElement) {
2596
3703
  forceReflow(containerElement);
2597
3704
  }
2598
3705
 
2599
- function computeDownsampleDimensions(srcWidth, srcHeight, maxWidth, maxHeight) {
2600
- if (!isPositiveFinite$1(srcWidth) ||
2601
- !isPositiveFinite$1(srcHeight) ||
2602
- !isPositiveFinite$1(maxWidth) ||
2603
- !isPositiveFinite$1(maxHeight)) {
2604
- return {
2605
- width: Math.max(1, Math.round(srcWidth) || 1),
2606
- height: Math.max(1, Math.round(srcHeight) || 1),
2607
- needsResize: false,
2608
- };
2609
- }
2610
- const needsResize = srcWidth > maxWidth || srcHeight > maxHeight;
2611
- if (!needsResize) {
2612
- return { width: srcWidth, height: srcHeight, needsResize: false };
2613
- }
2614
- const ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight);
2615
- return {
2616
- width: Math.max(1, Math.round(srcWidth * ratio)),
2617
- height: Math.max(1, Math.round(srcHeight * ratio)),
2618
- needsResize: true,
2619
- };
2620
- }
2621
- function isPositiveFinite$1(value) {
2622
- return Number.isFinite(value) && value > 0;
2623
- }
2624
- function selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType) {
2625
- if (downsampleMimeType)
2626
- return downsampleMimeType;
2627
- if (preserveSourceFormat && (sourceMime === 'image/png' || sourceMime === 'image/webp')) {
2628
- return sourceMime;
2629
- }
2630
- return 'image/jpeg';
2631
- }
2632
- function detectSourceMimeType(dataUrl) {
2633
- const match = /^data:(image\/[a-z0-9+\-.]+)\s*;/i.exec(dataUrl);
2634
- return match ? match[1].toLowerCase() : null;
2635
- }
2636
- function resampleImage(imageElement, maxWidth, maxHeight, sourceMime, preserveSourceFormat, downsampleMimeType, quality) {
2637
- const { width, height } = computeDownsampleDimensions(imageElement.naturalWidth, imageElement.naturalHeight, maxWidth, maxHeight);
2638
- const mimeType = selectDownsampleMimeType(sourceMime, preserveSourceFormat, downsampleMimeType);
2639
- const offscreenCanvas = document.createElement('canvas');
2640
- offscreenCanvas.width = width;
2641
- offscreenCanvas.height = height;
2642
- const context = offscreenCanvas.getContext('2d');
2643
- if (!context) {
2644
- throw new DownsampleError('Failed to obtain a 2D context for downsampling.');
2645
- }
2646
- context.drawImage(imageElement, 0, 0, imageElement.naturalWidth, imageElement.naturalHeight, 0, 0, width, height);
2647
- const dataUrl = mimeType === 'image/png'
2648
- ? offscreenCanvas.toDataURL(mimeType)
2649
- : offscreenCanvas.toDataURL(mimeType, quality);
2650
- return { dataUrl, width, height, mimeType };
2651
- }
2652
-
2653
3706
  async function loadImage(context, imageBase64, loadOptions = {}) {
2654
3707
  if (typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) {
2655
3708
  return;
@@ -2661,14 +3714,10 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
2661
3714
  const containerScrollLeft = context.containerElement
2662
3715
  ? context.containerElement.scrollLeft
2663
3716
  : null;
2664
- const containerOverflow = context.containerElement
2665
- ? context.containerElement.style.overflow
2666
- : null;
2667
3717
  const bundle = {
2668
3718
  placeholderHidden,
2669
3719
  containerScrollTop,
2670
3720
  containerScrollLeft,
2671
- containerOverflow,
2672
3721
  originalImage: context.getOriginalImage(),
2673
3722
  isImageLoadedToCanvas: context.getIsImageLoadedToCanvas(),
2674
3723
  lastSnapshot: context.getLastSnapshot(),
@@ -2690,7 +3739,7 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
2690
3739
  decode.cleanup(true);
2691
3740
  throw error;
2692
3741
  }
2693
- const loadSource = maybeDownsample(imageElement, imageBase64, context.options);
3742
+ const loadSource = maybeDownsample(imageElement, imageBase64, context.options, getCanvasDocument(context.canvas));
2694
3743
  const fabricImage = await withTimeout(context.fabric.FabricImage.fromURL(loadSource.dataUrl, { crossOrigin: 'anonymous' }), context.options.imageLoadTimeoutMs, 'FabricImage.fromURL');
2695
3744
  context.canvas.discardActiveObject();
2696
3745
  context.canvas.clear();
@@ -2802,7 +3851,7 @@ function toSupportedImageMimeType(mimeType) {
2802
3851
  ? mimeType
2803
3852
  : null;
2804
3853
  }
2805
- function maybeDownsample(imageElement, originalDataUrl, options) {
3854
+ function maybeDownsample(imageElement, originalDataUrl, options, ownerDocument) {
2806
3855
  const originalMimeType = toSupportedImageMimeType(detectSourceMimeType(originalDataUrl));
2807
3856
  if (!options.downsampleOnLoad) {
2808
3857
  return { dataUrl: originalDataUrl, mimeType: originalMimeType };
@@ -2817,13 +3866,18 @@ function maybeDownsample(imageElement, originalDataUrl, options) {
2817
3866
  return { dataUrl: originalDataUrl, mimeType: originalMimeType };
2818
3867
  }
2819
3868
  const sourceMime = detectSourceMimeType(originalDataUrl);
2820
- const resampledImage = resampleImage(imageElement, options.downsampleMaxWidth, options.downsampleMaxHeight, sourceMime, options.preserveSourceFormat, options.downsampleMimeType, options.downsampleQuality);
3869
+ const resampledImage = resampleImage(imageElement, options.downsampleMaxWidth, options.downsampleMaxHeight, sourceMime, options.preserveSourceFormat, options.downsampleMimeType, options.downsampleQuality, ownerDocument);
2821
3870
  const actualMimeType = toSupportedImageMimeType(detectSourceMimeType(resampledImage.dataUrl));
2822
3871
  return {
2823
3872
  dataUrl: resampledImage.dataUrl,
2824
3873
  mimeType: actualMimeType !== null && actualMimeType !== void 0 ? actualMimeType : resampledImage.mimeType,
2825
3874
  };
2826
3875
  }
3876
+ function getCanvasDocument(canvas) {
3877
+ var _a, _b, _c, _d, _e;
3878
+ const canvasLike = canvas;
3879
+ return ((_e = (_c = (_b = (_a = canvasLike.getElement) === null || _a === void 0 ? void 0 : _a.call(canvasLike)) === null || _b === void 0 ? void 0 : _b.ownerDocument) !== null && _c !== void 0 ? _c : (_d = canvasLike.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : (typeof document !== 'undefined' ? document : undefined));
3880
+ }
2827
3881
  function computeLayout(context, fabricImage) {
2828
3882
  var _a, _b, _c, _d;
2829
3883
  const imageWidth = (_a = fabricImage.width) !== null && _a !== void 0 ? _a : 0;
@@ -2833,7 +3887,7 @@ function computeLayout(context, fabricImage) {
2833
3887
  width: context.options.canvasWidth,
2834
3888
  height: context.options.canvasHeight,
2835
3889
  }, scrollbarSize);
2836
- const strategy = selectLayoutStrategy(context.options);
3890
+ const strategy = selectLayoutStrategy(context.options.layoutMode);
2837
3891
  if (strategy === 'fit') {
2838
3892
  return computeFitLayout(imageWidth, imageHeight, context.options.canvasWidth, context.options.canvasHeight, viewport);
2839
3893
  }
@@ -2848,14 +3902,6 @@ function serializeCanvas(canvas) {
2848
3902
  return JSON.stringify(json);
2849
3903
  }
2850
3904
  async function replayRollback(context, bundle) {
2851
- if (context.containerElement && bundle.containerOverflow !== null) {
2852
- try {
2853
- context.containerElement.style.overflow = bundle.containerOverflow;
2854
- }
2855
- catch (rollbackError) {
2856
- console.warn('[ImageEditor] rollback: overflow restore failed', rollbackError);
2857
- }
2858
- }
2859
3905
  try {
2860
3906
  await context.canvas.loadFromJSON(JSON.parse(bundle.canvasJson));
2861
3907
  context.canvas.renderAll();
@@ -2889,16 +3935,56 @@ async function replayRollback(context, bundle) {
2889
3935
  }
2890
3936
  }
2891
3937
 
3938
+ const ANIMATION_SETTLE_GRACE_MS = 1000;
2892
3939
  function animateProps(object, props, options, guard) {
2893
3940
  return new Promise((resolve, reject) => {
2894
3941
  const propCount = Object.keys(props).length;
2895
- if (propCount === 0) {
3942
+ if (propCount === 0 || guard.isDisposed()) {
2896
3943
  resolve();
2897
3944
  return;
2898
3945
  }
2899
3946
  let completed = 0;
3947
+ let settled = false;
3948
+ let aborters = [];
3949
+ let timeoutId = null;
3950
+ let unregisterAborter = null;
3951
+ const cleanup = () => {
3952
+ if (timeoutId !== null) {
3953
+ clearTimeout(timeoutId);
3954
+ timeoutId = null;
3955
+ }
3956
+ unregisterAborter === null || unregisterAborter === void 0 ? void 0 : unregisterAborter();
3957
+ unregisterAborter = null;
3958
+ };
3959
+ const settle = () => {
3960
+ if (settled)
3961
+ return;
3962
+ settled = true;
3963
+ cleanup();
3964
+ resolve();
3965
+ };
3966
+ const fail = (error) => {
3967
+ if (settled)
3968
+ return;
3969
+ settled = true;
3970
+ cleanup();
3971
+ reject(error);
3972
+ };
3973
+ const abortAndSettle = () => {
3974
+ for (const abort of aborters) {
3975
+ try {
3976
+ abort();
3977
+ }
3978
+ catch {
3979
+ }
3980
+ }
3981
+ settle();
3982
+ };
3983
+ const duration = Number.isFinite(options.duration) ? Math.max(0, options.duration) : 0;
3984
+ timeoutId = setTimeout(abortAndSettle, duration + ANIMATION_SETTLE_GRACE_MS);
3985
+ unregisterAborter = guard.registerAnimationAborter(abortAndSettle);
2900
3986
  try {
2901
- object.animate(props, {
3987
+ const animationResult = object.animate(props, {
2902
3988
  duration: options.duration,
2903
3989
  onChange: () => {
2904
3990
  var _a;
@@ -2908,15 +3994,27 @@ function animateProps(object, props, options, guard) {
2908
3994
  },
2909
3995
  onComplete: () => {
2910
3996
  if (++completed >= propCount)
2911
- resolve();
3997
+ settle();
2912
3998
  },
2913
3999
  });
4000
+ aborters = collectAnimationAborters(animationResult);
2914
4001
  }
2915
4002
  catch (error) {
2916
- reject(error);
4003
+ fail(error);
2917
4004
  }
2918
4005
  });
2919
4006
  }
4007
+ function collectAnimationAborters(animationResult) {
4008
+ const handles = Array.isArray(animationResult)
4009
+ ? animationResult
4010
+ : animationResult && typeof animationResult === 'object'
4011
+ ? Object.values(animationResult)
4012
+ : [animationResult];
4013
+ return handles.flatMap((handle) => {
4014
+ const abort = handle === null || handle === void 0 ? void 0 : handle.abort;
4015
+ return typeof abort === 'function' ? [() => abort.call(handle)] : [];
4016
+ });
4017
+ }
2920
4018
  function restoreOrigin(object, originX, originY) {
2921
4019
  try {
2922
4020
  object.set({ originX, originY });
@@ -3082,10 +4180,8 @@ function coercePoint(pt) {
3082
4180
  }
3083
4181
 
3084
4182
  const POLYGON_AREA_EPSILON = 1e-6;
3085
- let nextMaskUid = 0;
3086
4183
  function createMaskUid(maskId) {
3087
- nextMaskUid += 1;
3088
- return `mask-${maskId}-${nextMaskUid}`;
4184
+ return `mask-${maskId}`;
3089
4185
  }
3090
4186
  function isFabricObjectLike(value) {
3091
4187
  if (!value || typeof value !== 'object')
@@ -3093,6 +4189,26 @@ function isFabricObjectLike(value) {
3093
4189
  const candidate = value;
3094
4190
  return typeof candidate.set === 'function' && typeof candidate.on === 'function';
3095
4191
  }
4192
+ function isStyleObject(value) {
4193
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
4194
+ }
4195
+ function mergeMaskConfig(defaultMaskConfig, config) {
4196
+ const safeDefaultConfig = { ...defaultMaskConfig };
4197
+ const defaultStyles = safeDefaultConfig.styles;
4198
+ delete safeDefaultConfig.onCreate;
4199
+ delete safeDefaultConfig.fabricGenerator;
4200
+ delete safeDefaultConfig.styles;
4201
+ const configStyles = isStyleObject(config.styles) ? config.styles : {};
4202
+ const safeDefaultStyles = isStyleObject(defaultStyles) ? defaultStyles : {};
4203
+ return {
4204
+ ...safeDefaultConfig,
4205
+ ...config,
4206
+ styles: {
4207
+ ...safeDefaultStyles,
4208
+ ...configStyles,
4209
+ },
4210
+ };
4211
+ }
3096
4212
  function warnInvalidMask(options, reason) {
3097
4213
  reportWarning(options, null, `createMask skipped: ${reason}.`);
3098
4214
  }
@@ -3178,11 +4294,11 @@ function createMask(context, config = {}) {
3178
4294
  const { canvas, options, fabric: fabricModule } = context;
3179
4295
  if (!canvas)
3180
4296
  return null;
3181
- const shapeType = (_a = config.shape) !== null && _a !== void 0 ? _a : 'rect';
3182
- if (!validateNumericInputs(options, config))
4297
+ const mergedConfig = mergeMaskConfig(options.defaultMaskConfig, config);
4298
+ const shapeType = (_a = mergedConfig.shape) !== null && _a !== void 0 ? _a : 'rect';
4299
+ if (!validateNumericInputs(options, mergedConfig))
3183
4300
  return null;
3184
4301
  const resolvedConfig = {
3185
- shape: shapeType,
3186
4302
  width: options.defaultMaskWidth,
3187
4303
  height: options.defaultMaskHeight,
3188
4304
  color: 'rgba(0,0,0,0.5)',
@@ -3192,13 +4308,14 @@ function createMask(context, config = {}) {
3192
4308
  top: undefined,
3193
4309
  angle: 0,
3194
4310
  selectable: true,
3195
- ...config,
4311
+ ...mergedConfig,
4312
+ shape: shapeType,
3196
4313
  };
3197
4314
  const firstOffset = 10;
3198
4315
  let left;
3199
4316
  let top;
3200
4317
  const previousMask = context.getLastMask();
3201
- if (config.left === undefined && previousMask) {
4318
+ if (mergedConfig.left === undefined && previousMask) {
3202
4319
  const previousRight = ((_b = previousMask.left) !== null && _b !== void 0 ? _b : 0) +
3203
4320
  (typeof previousMask.getScaledWidth === 'function'
3204
4321
  ? previousMask.getScaledWidth()
@@ -3207,17 +4324,21 @@ function createMask(context, config = {}) {
3207
4324
  top = (_f = previousMask.top) !== null && _f !== void 0 ? _f : firstOffset;
3208
4325
  }
3209
4326
  else {
3210
- left = resolveNumeric(config.left, 'x', firstOffset, canvas, options);
3211
- top = resolveNumeric(config.top, 'y', firstOffset, canvas, options);
4327
+ left = resolveNumeric(mergedConfig.left, 'x', firstOffset, canvas, options);
4328
+ top = resolveNumeric(mergedConfig.top, 'y', firstOffset, canvas, options);
3212
4329
  }
3213
- resolvedConfig.width = resolveNumeric(config.width, 'x', options.defaultMaskWidth, canvas, options);
3214
- resolvedConfig.height = resolveNumeric(config.height, 'y', options.defaultMaskHeight, canvas, options);
3215
- const rx = config.rx !== undefined ? resolveNumeric(config.rx, 'x', 0, canvas, options) : undefined;
3216
- const ry = config.ry !== undefined ? resolveNumeric(config.ry, 'y', 0, canvas, options) : undefined;
4330
+ resolvedConfig.width = resolveNumeric(mergedConfig.width, 'x', options.defaultMaskWidth, canvas, options);
4331
+ resolvedConfig.height = resolveNumeric(mergedConfig.height, 'y', options.defaultMaskHeight, canvas, options);
4332
+ const rx = mergedConfig.rx !== undefined
4333
+ ? resolveNumeric(mergedConfig.rx, 'x', 0, canvas, options)
4334
+ : undefined;
4335
+ const ry = mergedConfig.ry !== undefined
4336
+ ? resolveNumeric(mergedConfig.ry, 'y', 0, canvas, options)
4337
+ : undefined;
3217
4338
  const radius = shapeType === 'circle'
3218
- ? resolveNumeric(config.radius, 'x', Math.min(resolvedConfig.width, resolvedConfig.height) / 2, canvas, options)
4339
+ ? resolveNumeric(mergedConfig.radius, 'x', Math.min(resolvedConfig.width, resolvedConfig.height) / 2, canvas, options)
3219
4340
  : undefined;
3220
- const polygonPoints = shapeType === 'polygon' ? resolvePolygonPoints(options, config.points) : null;
4341
+ const polygonPoints = shapeType === 'polygon' ? resolvePolygonPoints(options, mergedConfig.points) : null;
3221
4342
  if (!validateFiniteField(options, 'left', left) ||
3222
4343
  !validateFiniteField(options, 'top', top) ||
3223
4344
  !validatePositiveField(options, 'width', resolvedConfig.width) ||
@@ -3233,7 +4354,7 @@ function createMask(context, config = {}) {
3233
4354
  (shapeType === 'polygon' && polygonPoints === null)) {
3234
4355
  return null;
3235
4356
  }
3236
- if (options.expandCanvasToImage) {
4357
+ if (options.layoutMode === 'expand') {
3237
4358
  const requiredWidth = Math.ceil(left + resolvedConfig.width + 10);
3238
4359
  const requiredHeight = Math.ceil(top + resolvedConfig.height + 10);
3239
4360
  const nextWidth = Math.max(canvas.getWidth(), requiredWidth);
@@ -3248,8 +4369,8 @@ function createMask(context, config = {}) {
3248
4369
  }
3249
4370
  }
3250
4371
  let mask;
3251
- if (typeof resolvedConfig.fabricGenerator === 'function') {
3252
- const generated = resolvedConfig.fabricGenerator(resolvedConfig, canvas, options);
4372
+ if (typeof config.fabricGenerator === 'function') {
4373
+ const generated = config.fabricGenerator(resolvedConfig, canvas, options);
3253
4374
  if (!isFabricObjectLike(generated)) {
3254
4375
  reportWarning(options, generated, 'createMask skipped: fabricGenerator did not return a Fabric object.');
3255
4376
  return null;
@@ -3325,16 +4446,17 @@ function createMask(context, config = {}) {
3325
4446
  }
3326
4447
  }
3327
4448
  const maskObject = mask;
3328
- maskObject.selectable = 'selectable' in config ? !!config.selectable : true;
3329
- maskObject.evented = 'evented' in config ? !!config.evented : true;
3330
- maskObject.hasControls = 'hasControls' in config ? !!config.hasControls : true;
4449
+ maskObject.selectable = 'selectable' in mergedConfig ? !!mergedConfig.selectable : true;
4450
+ maskObject.evented = 'evented' in mergedConfig ? !!mergedConfig.evented : true;
4451
+ maskObject.hasControls = 'hasControls' in mergedConfig ? !!mergedConfig.hasControls : true;
3331
4452
  maskObject.transparentCorners =
3332
- 'transparentCorners' in config ? !!config.transparentCorners : false;
3333
- maskObject.strokeUniform = 'strokeUniform' in config ? !!config.strokeUniform : true;
4453
+ 'transparentCorners' in mergedConfig ? !!mergedConfig.transparentCorners : false;
4454
+ maskObject.strokeUniform =
4455
+ 'strokeUniform' in mergedConfig ? !!mergedConfig.strokeUniform : true;
3334
4456
  maskObject.lockRotation = !options.maskRotatable;
3335
- maskObject.borderColor = (_o = config.borderColor) !== null && _o !== void 0 ? _o : 'red';
3336
- maskObject.cornerColor = (_p = config.cornerColor) !== null && _p !== void 0 ? _p : 'black';
3337
- maskObject.cornerSize = (_q = config.cornerSize) !== null && _q !== void 0 ? _q : 8;
4457
+ maskObject.borderColor = (_o = mergedConfig.borderColor) !== null && _o !== void 0 ? _o : 'red';
4458
+ maskObject.cornerColor = (_p = mergedConfig.cornerColor) !== null && _p !== void 0 ? _p : 'black';
4459
+ maskObject.cornerSize = (_q = mergedConfig.cornerSize) !== null && _q !== void 0 ? _q : 8;
3338
4460
  const styles = ((_r = resolvedConfig.styles) !== null && _r !== void 0 ? _r : {});
3339
4461
  if ('stroke' in styles) {
3340
4462
  maskObject.stroke = styles.stroke;
@@ -3369,9 +4491,9 @@ function createMask(context, config = {}) {
3369
4491
  }
3370
4492
  canvas.renderAll();
3371
4493
  context.saveCanvasState();
3372
- if (typeof resolvedConfig.onCreate === 'function') {
4494
+ if (typeof config.onCreate === 'function') {
3373
4495
  try {
3374
- resolvedConfig.onCreate(maskObject, canvas);
4496
+ config.onCreate(maskObject, canvas);
3375
4497
  }
3376
4498
  catch (error) {
3377
4499
  reportWarning(options, error, 'createMask onCreate callback threw.');
@@ -3527,11 +4649,17 @@ function hideAllMaskLabels(context) {
3527
4649
  });
3528
4650
  }
3529
4651
 
4652
+ function getMaskListDocument(context) {
4653
+ var _a, _b, _c, _d, _e;
4654
+ const canvasLike = context.canvas;
4655
+ return ((_e = (_c = (_b = (_a = canvasLike === null || canvasLike === void 0 ? void 0 : canvasLike.getElement) === null || _a === void 0 ? void 0 : _a.call(canvasLike)) === null || _b === void 0 ? void 0 : _b.ownerDocument) !== null && _c !== void 0 ? _c : (_d = canvasLike === null || canvasLike === void 0 ? void 0 : canvasLike.lowerCanvasEl) === null || _d === void 0 ? void 0 : _d.ownerDocument) !== null && _e !== void 0 ? _e : document);
4656
+ }
3530
4657
  function renderMaskList(context) {
3531
4658
  const listId = context.getListElementId();
3532
4659
  if (!listId)
3533
4660
  return;
3534
- const listEl = document.getElementById(listId);
4661
+ const ownerDocument = getMaskListDocument(context);
4662
+ const listEl = ownerDocument.getElementById(listId);
3535
4663
  if (!listEl || !context.canvas)
3536
4664
  return;
3537
4665
  listEl.innerHTML = '';
@@ -3540,7 +4668,7 @@ function renderMaskList(context) {
3540
4668
  .getObjects()
3541
4669
  .filter(isMaskObject)
3542
4670
  .forEach((mask) => {
3543
- const listItemElement = document.createElement('li');
4671
+ const listItemElement = ownerDocument.createElement('li');
3544
4672
  listItemElement.className = 'list-group-item mask-item';
3545
4673
  listItemElement.textContent = mask.maskName;
3546
4674
  listItemElement.dataset.maskId = String(mask.maskId);
@@ -3563,7 +4691,7 @@ function updateMaskListSelection(context, selectedMask) {
3563
4691
  const listId = context.getListElementId();
3564
4692
  if (!listId)
3565
4693
  return;
3566
- const listEl = document.getElementById(listId);
4694
+ const listEl = getMaskListDocument(context).getElementById(listId);
3567
4695
  if (!listEl)
3568
4696
  return;
3569
4697
  const selectedId = selectedMask ? String(selectedMask.maskId) : null;
@@ -3574,7 +4702,7 @@ function updateMaskListSelection(context, selectedMask) {
3574
4702
  }
3575
4703
 
3576
4704
  class DomBindings {
3577
- constructor(resolveElementId, isDisposed) {
4705
+ constructor(resolveElementId, isDisposed, resolveDocument = () => document) {
3578
4706
  Object.defineProperty(this, "registry", {
3579
4707
  enumerable: true,
3580
4708
  configurable: true,
@@ -3593,14 +4721,21 @@ class DomBindings {
3593
4721
  writable: true,
3594
4722
  value: void 0
3595
4723
  });
4724
+ Object.defineProperty(this, "resolveDocument", {
4725
+ enumerable: true,
4726
+ configurable: true,
4727
+ writable: true,
4728
+ value: void 0
4729
+ });
3596
4730
  this.resolveElementId = resolveElementId;
3597
4731
  this.isDisposed = isDisposed;
4732
+ this.resolveDocument = resolveDocument;
3598
4733
  }
3599
4734
  bindIfExists(key, eventType, handler) {
3600
4735
  const id = this.resolveElementId(key);
3601
4736
  if (!id)
3602
4737
  return false;
3603
- const element = document.getElementById(id);
4738
+ const element = this.resolveDocument().getElementById(id);
3604
4739
  if (!element)
3605
4740
  return false;
3606
4741
  const wrapped = (event) => {
@@ -3617,7 +4752,7 @@ class DomBindings {
3617
4752
  const id = this.resolveElementId(entry.elementKey);
3618
4753
  if (!id)
3619
4754
  continue;
3620
- const element = document.getElementById(id);
4755
+ const element = this.resolveDocument().getElementById(id);
3621
4756
  if (!element)
3622
4757
  continue;
3623
4758
  try {
@@ -3698,8 +4833,8 @@ function resetFileInput(input) {
3698
4833
  }
3699
4834
 
3700
4835
  const LAYOUT_EPSILON = 0.5;
3701
- const INTERNAL_OPERATION_TOKEN = Symbol.for('ImageEditorInternalOperation');
3702
- const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol.for('ImageEditorAllowDuringAnimationQueue');
4836
+ const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
4837
+ const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol('ImageEditorAllowDuringAnimationQueue');
3703
4838
  const CROP_MODE_CONTROL_KEYS = [
3704
4839
  'scalePercentageInput',
3705
4840
  'rotateLeftDegreesInput',
@@ -3720,9 +4855,85 @@ const CROP_MODE_CONTROL_KEYS = [
3720
4855
  'enterCropModeButton',
3721
4856
  'applyCropButton',
3722
4857
  'cancelCropButton',
4858
+ 'enterMosaicModeButton',
4859
+ 'exitMosaicModeButton',
4860
+ 'mosaicBrushSizeInput',
4861
+ 'mosaicBlockSizeInput',
3723
4862
  ];
3724
4863
  const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
3725
4864
  const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
4865
+ const MOSAIC_MODE_CONTROL_KEYS = [
4866
+ 'scalePercentageInput',
4867
+ 'rotateLeftDegreesInput',
4868
+ 'rotateRightDegreesInput',
4869
+ 'rotateLeftButton',
4870
+ 'rotateRightButton',
4871
+ 'createMaskButton',
4872
+ 'removeSelectedMaskButton',
4873
+ 'removeAllMasksButton',
4874
+ 'mergeMasksButton',
4875
+ 'downloadImageButton',
4876
+ 'zoomInButton',
4877
+ 'zoomOutButton',
4878
+ 'resetImageTransformButton',
4879
+ 'undoButton',
4880
+ 'redoButton',
4881
+ 'imageInput',
4882
+ 'enterCropModeButton',
4883
+ 'applyCropButton',
4884
+ 'cancelCropButton',
4885
+ 'enterMosaicModeButton',
4886
+ 'exitMosaicModeButton',
4887
+ 'mosaicBrushSizeInput',
4888
+ 'mosaicBlockSizeInput',
4889
+ ];
4890
+ const MOSAIC_MODE_ENABLED_KEYS = [
4891
+ 'exitMosaicModeButton',
4892
+ 'mosaicBrushSizeInput',
4893
+ 'mosaicBlockSizeInput',
4894
+ ];
4895
+ const MOSAIC_SESSION_ALLOWED_OPERATIONS = new Set([
4896
+ 'exitMosaicMode',
4897
+ 'applyMosaic',
4898
+ 'setMosaicConfig',
4899
+ 'resetMosaicConfig',
4900
+ 'setMosaicBrushSize',
4901
+ 'setMosaicBlockSize',
4902
+ 'saveState',
4903
+ ]);
4904
+ const SCROLLBAR_SETTLE_EPSILON = 1;
4905
+ const IMAGE_EDITOR_OPERATIONS = new Set([
4906
+ 'init',
4907
+ 'loadImage',
4908
+ 'loadFromState',
4909
+ 'saveState',
4910
+ 'scaleImage',
4911
+ 'rotateImage',
4912
+ 'resetImageTransform',
4913
+ 'createMask',
4914
+ 'removeSelectedMask',
4915
+ 'removeAllMasks',
4916
+ 'mergeMasks',
4917
+ 'enterCropMode',
4918
+ 'applyCrop',
4919
+ 'cancelCrop',
4920
+ 'enterMosaicMode',
4921
+ 'exitMosaicMode',
4922
+ 'applyMosaic',
4923
+ 'setMosaicConfig',
4924
+ 'resetMosaicConfig',
4925
+ 'setMosaicBrushSize',
4926
+ 'setMosaicBlockSize',
4927
+ 'undo',
4928
+ 'redo',
4929
+ 'exportImageBase64',
4930
+ 'exportImageFile',
4931
+ 'downloadImage',
4932
+ 'dispose',
4933
+ ]);
4934
+ function isImageEditorOperation(value) {
4935
+ return value !== null && IMAGE_EDITOR_OPERATIONS.has(value);
4936
+ }
3726
4937
  class ImageEditor {
3727
4938
  constructor(fabricModuleOrOptions = {}, options = {}) {
3728
4939
  var _a;
@@ -3744,6 +4955,24 @@ class ImageEditor {
3744
4955
  writable: true,
3745
4956
  value: void 0
3746
4957
  });
4958
+ Object.defineProperty(this, "currentLayoutMode", {
4959
+ enumerable: true,
4960
+ configurable: true,
4961
+ writable: true,
4962
+ value: 'expand'
4963
+ });
4964
+ Object.defineProperty(this, "defaultMosaicConfig", {
4965
+ enumerable: true,
4966
+ configurable: true,
4967
+ writable: true,
4968
+ value: void 0
4969
+ });
4970
+ Object.defineProperty(this, "currentMosaicConfig", {
4971
+ enumerable: true,
4972
+ configurable: true,
4973
+ writable: true,
4974
+ value: void 0
4975
+ });
3747
4976
  Object.defineProperty(this, "canvas", {
3748
4977
  enumerable: true,
3749
4978
  configurable: true,
@@ -3882,6 +5111,12 @@ class ImageEditor {
3882
5111
  writable: true,
3883
5112
  value: null
3884
5113
  });
5114
+ Object.defineProperty(this, "mosaicSession", {
5115
+ enumerable: true,
5116
+ configurable: true,
5117
+ writable: true,
5118
+ value: null
5119
+ });
3885
5120
  Object.defineProperty(this, "domBindings", {
3886
5121
  enumerable: true,
3887
5122
  configurable: true,
@@ -3922,9 +5157,15 @@ class ImageEditor {
3922
5157
  this.fabricModule = (_a = detected.fabric) !== null && _a !== void 0 ? _a : {};
3923
5158
  this.isFabricLoaded = detected.isFabricLoaded;
3924
5159
  this.options = resolveOptions(detected.options);
3925
- const layoutConflict = detectLayoutConflict(this.options);
3926
- if (layoutConflict) {
3927
- reportWarning(this.options, null, layoutConflict.message);
5160
+ this.currentLayoutMode = this.options.layoutMode;
5161
+ this.defaultMosaicConfig = this.options.defaultMosaicConfig;
5162
+ this.currentMosaicConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
5163
+ const rawDefaultLayoutMode = detected.options
5164
+ .defaultLayoutMode;
5165
+ if (rawDefaultLayoutMode !== undefined && !isLayoutMode(rawDefaultLayoutMode)) {
5166
+ reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported defaultLayoutMode ` +
5167
+ `${JSON.stringify(rawDefaultLayoutMode)}. ` +
5168
+ 'Expected "fit", "cover", or "expand".'), 'Invalid defaultLayoutMode fell back to "expand".');
3928
5169
  }
3929
5170
  this.operationGuard = new OperationGuard();
3930
5171
  this.animQueue = new AnimationQueue();
@@ -3966,10 +5207,14 @@ class ImageEditor {
3966
5207
  enterCropModeButton: 'enterCropModeButton',
3967
5208
  applyCropButton: 'applyCropButton',
3968
5209
  cancelCropButton: 'cancelCropButton',
5210
+ enterMosaicModeButton: 'enterMosaicModeButton',
5211
+ exitMosaicModeButton: 'exitMosaicModeButton',
5212
+ mosaicBrushSizeInput: 'mosaicBrushSizeInput',
5213
+ mosaicBlockSizeInput: 'mosaicBlockSizeInput',
3969
5214
  uploadArea: 'uploadArea',
3970
5215
  };
3971
5216
  this.elements = { ...defaults, ...idMap };
3972
- this.domBindings = new DomBindings((key) => this.elements[key], () => this.isDisposed);
5217
+ 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; });
3973
5218
  this.initCanvas();
3974
5219
  this.transformController = new TransformController(this.buildTransformContext());
3975
5220
  this.bindDomEvents();
@@ -4120,6 +5365,26 @@ class ImageEditor {
4120
5365
  this.bindElementIfExists('cancelCropButton', 'click', () => {
4121
5366
  this.cancelCrop();
4122
5367
  });
5368
+ this.bindElementIfExists('enterMosaicModeButton', 'click', () => {
5369
+ this.enterMosaicMode();
5370
+ });
5371
+ this.bindElementIfExists('exitMosaicModeButton', 'click', () => {
5372
+ this.exitMosaicMode();
5373
+ });
5374
+ const bindMosaicSizeInput = (key, applyValue) => {
5375
+ const handler = (event) => {
5376
+ const parsed = parseFloat(event.target.value);
5377
+ applyValue(parsed);
5378
+ };
5379
+ this.bindElementIfExists(key, 'input', handler);
5380
+ this.bindElementIfExists(key, 'change', handler);
5381
+ };
5382
+ bindMosaicSizeInput('mosaicBrushSizeInput', (value) => {
5383
+ this.setMosaicBrushSize(value);
5384
+ });
5385
+ bindMosaicSizeInput('mosaicBlockSizeInput', (value) => {
5386
+ this.setMosaicBlockSize(value);
5387
+ });
4123
5388
  }
4124
5389
  bindElementIfExists(key, event, handler) {
4125
5390
  var _a;
@@ -4155,6 +5420,9 @@ class ImageEditor {
4155
5420
  }
4156
5421
  }
4157
5422
  async loadImage(base64, options = {}) {
5423
+ return this.loadImageInternal(base64, options);
5424
+ }
5425
+ async loadImageInternal(base64, options = {}) {
4158
5426
  if (!this.isFabricLoaded || !this.canvas)
4159
5427
  return;
4160
5428
  if (this.isDisposed)
@@ -4174,7 +5442,7 @@ class ImageEditor {
4174
5442
  const loadImageContext = {
4175
5443
  fabric: this.fabricModule,
4176
5444
  canvas: this.canvas,
4177
- options: this.options,
5445
+ options: this.getRuntimeOptions(),
4178
5446
  containerElement: this.containerElement,
4179
5447
  placeholderElement: this.placeholderElement,
4180
5448
  viewportCache: this.viewportCache,
@@ -4266,6 +5534,11 @@ class ImageEditor {
4266
5534
  !CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
4267
5535
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
4268
5536
  }
5537
+ if (this.mosaicSession &&
5538
+ !this.operationGuard.isOwnOperation(token) &&
5539
+ !MOSAIC_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
5540
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while mosaic mode is active.`);
5541
+ }
4269
5542
  if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
4270
5543
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
4271
5544
  }
@@ -4275,10 +5548,17 @@ class ImageEditor {
4275
5548
  this.assertIdleForOperation(operationName, options);
4276
5549
  return true;
4277
5550
  }
4278
- catch {
5551
+ catch (error) {
5552
+ if (!this.isExpectedIdleGuardError(error, operationName)) {
5553
+ throw error;
5554
+ }
4279
5555
  return false;
4280
5556
  }
4281
5557
  }
5558
+ isExpectedIdleGuardError(error, operationName) {
5559
+ return (error instanceof Error &&
5560
+ error.message.startsWith(`[ImageEditor] Cannot run "${operationName}" `));
5561
+ }
4282
5562
  assertCanQueueAnimation(operationName, options) {
4283
5563
  this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
4284
5564
  }
@@ -4290,17 +5570,26 @@ class ImageEditor {
4290
5570
  ((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
4291
5571
  }
4292
5572
  isBusy() {
4293
- return this.operationGuard.isBusy() || this.animQueue.isBusy() || this.cropSession !== null;
5573
+ return (this.operationGuard.isBusy() ||
5574
+ this.animQueue.isBusy() ||
5575
+ this.cropSession !== null ||
5576
+ this.mosaicSession !== null);
4294
5577
  }
4295
5578
  setLayoutMode(mode) {
4296
- if (mode !== 'fit' && mode !== 'cover' && mode !== 'expand') {
5579
+ if (!isLayoutMode(mode)) {
4297
5580
  reportWarning(this.options, new TypeError(`[ImageEditor] Unsupported layout mode ${JSON.stringify(mode)}. ` +
4298
5581
  'Expected "fit", "cover", or "expand".'), 'Ignored invalid layout mode.');
4299
5582
  return;
4300
5583
  }
4301
- this.options.fitImageToCanvas = mode === 'fit';
4302
- this.options.coverImageToCanvas = mode === 'cover';
4303
- this.options.expandCanvasToImage = mode === 'expand';
5584
+ this.currentLayoutMode = mode;
5585
+ }
5586
+ getRuntimeOptions() {
5587
+ if (this.currentLayoutMode === this.options.layoutMode)
5588
+ return this.options;
5589
+ return Object.freeze({
5590
+ ...this.options,
5591
+ layoutMode: this.currentLayoutMode,
5592
+ });
4304
5593
  }
4305
5594
  buildCallbackContext(operation, isInternalOperation = false) {
4306
5595
  return { operation, isInternalOperation };
@@ -4309,7 +5598,7 @@ class ImageEditor {
4309
5598
  const internal = this.getInternalOperationToken(options);
4310
5599
  const activeOperation = this.operationGuard.activeOperationName();
4311
5600
  if (internal && activeOperation) {
4312
- return this.buildCallbackContext(activeOperation, true);
5601
+ return this.buildCallbackContext(isImageEditorOperation(activeOperation) ? activeOperation : fallback, true);
4313
5602
  }
4314
5603
  return this.buildCallbackContext(fallback, false);
4315
5604
  }
@@ -4376,6 +5665,7 @@ class ImageEditor {
4376
5665
  currentRotation: this.currentRotation,
4377
5666
  isBusy: this.isBusy(),
4378
5667
  isCropMode: this.cropSession !== null,
5668
+ isMosaicMode: this.mosaicSession !== null,
4379
5669
  canUndo: this.historyManager.canUndo(),
4380
5670
  canRedo: this.historyManager.canRedo(),
4381
5671
  canvasWidth,
@@ -4463,7 +5753,7 @@ class ImageEditor {
4463
5753
  const boundingRect = this.originalImage.getBoundingRect();
4464
5754
  const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
4465
5755
  const viewport = this.measureLayoutViewport(scrollbarSize);
4466
- if (this.options.fitImageToCanvas || this.options.coverImageToCanvas) {
5756
+ if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
4467
5757
  const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
4468
5758
  this.setCanvasSizePx(canvasSize.width, canvasSize.height);
4469
5759
  return;
@@ -4485,14 +5775,14 @@ class ImageEditor {
4485
5775
  const canvasH = Math.ceil(this.canvas.getHeight());
4486
5776
  const clipsImage = boundingRect.width > canvasW + LAYOUT_EPSILON ||
4487
5777
  boundingRect.height > canvasH + LAYOUT_EPSILON;
4488
- if (this.options.fitImageToCanvas || this.options.coverImageToCanvas) {
5778
+ if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
4489
5779
  const staleOverflowWidth = canvasW > viewport.width + LAYOUT_EPSILON &&
4490
5780
  boundingRect.width <= viewport.width + LAYOUT_EPSILON;
4491
5781
  const staleOverflowHeight = canvasH > viewport.height + LAYOUT_EPSILON &&
4492
5782
  boundingRect.height <= viewport.height + LAYOUT_EPSILON;
4493
5783
  return clipsImage || staleOverflowWidth || staleOverflowHeight;
4494
5784
  }
4495
- if (this.options.expandCanvasToImage) {
5785
+ if (this.currentLayoutMode === 'expand') {
4496
5786
  const expectedW = Math.max(viewport.width, Math.ceil(boundingRect.width));
4497
5787
  const expectedH = Math.max(viewport.height, Math.ceil(boundingRect.height));
4498
5788
  return (Math.abs(canvasW - expectedW) > LAYOUT_EPSILON ||
@@ -4500,6 +5790,33 @@ class ImageEditor {
4500
5790
  }
4501
5791
  return clipsImage;
4502
5792
  }
5793
+ settleFitCoverScrollbarsAfterStateRestore() {
5794
+ if (!this.canvas ||
5795
+ !this.containerElement ||
5796
+ (this.currentLayoutMode !== 'fit' && this.currentLayoutMode !== 'cover')) {
5797
+ return;
5798
+ }
5799
+ const canvasW = Math.ceil(this.canvas.getWidth());
5800
+ const canvasH = Math.ceil(this.canvas.getHeight());
5801
+ if (canvasW <= 1 || canvasH <= 1)
5802
+ return;
5803
+ const clientW = Math.floor(this.containerElement.clientWidth || 0);
5804
+ const clientH = Math.floor(this.containerElement.clientHeight || 0);
5805
+ if (clientW <= 0 || clientH <= 0)
5806
+ return;
5807
+ const scrollW = Math.ceil(this.containerElement.scrollWidth || 0);
5808
+ const scrollH = Math.ceil(this.containerElement.scrollHeight || 0);
5809
+ const hasHorizontalScrollbar = scrollW > clientW + LAYOUT_EPSILON;
5810
+ const hasVerticalScrollbar = scrollH > clientH + LAYOUT_EPSILON;
5811
+ if (!hasHorizontalScrollbar && !hasVerticalScrollbar)
5812
+ return;
5813
+ const nudgeWidth = hasVerticalScrollbar && Math.abs(canvasW - clientW) <= SCROLLBAR_SETTLE_EPSILON;
5814
+ const nudgeHeight = hasHorizontalScrollbar && Math.abs(canvasH - clientH) <= SCROLLBAR_SETTLE_EPSILON;
5815
+ if (!nudgeWidth && !nudgeHeight)
5816
+ return;
5817
+ this.setCanvasSizePx(nudgeWidth ? canvasW - 1 : canvasW, nudgeHeight ? canvasH - 1 : canvasH);
5818
+ this.setCanvasSizePx(canvasW, canvasH);
5819
+ }
4503
5820
  captureImageDisplayGeometry() {
4504
5821
  if (!this.canvas || !this.originalImage)
4505
5822
  return null;
@@ -4564,11 +5881,7 @@ class ImageEditor {
4564
5881
  afterTransformSnap: () => {
4565
5882
  if (this.isDisposed || !this.canvas || !this.originalImage)
4566
5883
  return;
4567
- if (this.options.expandCanvasToImage ||
4568
- this.options.coverImageToCanvas ||
4569
- this.options.fitImageToCanvas) {
4570
- this.updateCanvasSizeToImageBounds();
4571
- }
5884
+ this.updateCanvasSizeToImageBounds();
4572
5885
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
4573
5886
  this.canvas
4574
5887
  .getObjects()
@@ -4736,14 +6049,13 @@ class ImageEditor {
4736
6049
  this.currentImageMimeType = null;
4737
6050
  }
4738
6051
  this.isImageLoadedToCanvas = !!this.originalImage;
4739
- if (this.originalImage &&
4740
- (this.options.expandCanvasToImage ||
4741
- this.options.coverImageToCanvas ||
4742
- this.options.fitImageToCanvas) &&
4743
- this.shouldNormalizeCanvasSizeAfterStateRestore()) {
6052
+ if (this.originalImage && this.shouldNormalizeCanvasSizeAfterStateRestore()) {
4744
6053
  this.updateCanvasSizeToImageBounds();
4745
6054
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
4746
6055
  }
6056
+ if (this.originalImage) {
6057
+ this.settleFitCoverScrollbarsAfterStateRestore();
6058
+ }
4747
6059
  const restoredMasks = restoredState.objects.filter(isMaskObject);
4748
6060
  this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
4749
6061
  restoredMasks.forEach((maskObject) => {
@@ -4803,16 +6115,12 @@ class ImageEditor {
4803
6115
  if (after === before) {
4804
6116
  return;
4805
6117
  }
4806
- let executedOnce = false;
4807
6118
  const cmd = new Command(async () => {
4808
- if (executedOnce) {
4809
- await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
4810
- }
4811
- executedOnce = true;
6119
+ await this.loadFromStateInternal(after, this.withAnimationQueueBypass());
4812
6120
  }, async () => {
4813
6121
  await this.loadFromStateInternal(before, this.withAnimationQueueBypass());
4814
6122
  });
4815
- this.historyManager.execute(cmd);
6123
+ this.historyManager.push(cmd);
4816
6124
  this.lastSnapshot = after;
4817
6125
  }
4818
6126
  catch (error) {
@@ -4927,7 +6235,7 @@ class ImageEditor {
4927
6235
  return {
4928
6236
  fabric: this.fabricModule,
4929
6237
  canvas: this.canvas,
4930
- options: this.options,
6238
+ options: this.getRuntimeOptions(),
4931
6239
  getLastMask: () => this.lastMask,
4932
6240
  setLastMask: (maskObject) => {
4933
6241
  this.lastMask = maskObject;
@@ -5128,7 +6436,7 @@ class ImageEditor {
5128
6436
  containerElement: this.containerElement,
5129
6437
  loadImage: async (base64, providedOptions) => {
5130
6438
  const geometry = this.captureImageDisplayGeometry();
5131
- await this.loadImage(base64, this.withInternalOperationOptions(operationToken, providedOptions));
6439
+ await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
5132
6440
  this.restoreMergedImageDisplayGeometry(geometry);
5133
6441
  },
5134
6442
  saveState: () => this.captureSnapshotInternal(),
@@ -5141,8 +6449,9 @@ class ImageEditor {
5141
6449
  }
5142
6450
  captureSnapshotInternal() {
5143
6451
  var _a;
5144
- if (!this.canvas)
5145
- return '';
6452
+ if (!this.canvas) {
6453
+ throw new Error('[ImageEditor] Cannot capture canvas snapshot before init or after dispose.');
6454
+ }
5146
6455
  const activeMask = this.getActiveMaskForSnapshot();
5147
6456
  this.hideAllMaskLabels();
5148
6457
  return saveState({
@@ -5161,9 +6470,134 @@ class ImageEditor {
5161
6470
  const activeObject = this.canvas.getActiveObject();
5162
6471
  if (activeObject && isMaskObject(activeObject))
5163
6472
  return activeObject;
5164
- return ((_a = this.canvas
6473
+ const labeledMasks = this.canvas
5165
6474
  .getObjects()
5166
- .find((object) => isMaskObject(object) && !!object.labelObject)) !== null && _a !== void 0 ? _a : null);
6475
+ .filter((object) => isMaskObject(object) && !!object.labelObject);
6476
+ return labeledMasks.length === 1 ? ((_a = labeledMasks[0]) !== null && _a !== void 0 ? _a : null) : null;
6477
+ }
6478
+ enterMosaicMode() {
6479
+ if (!this.canvas || !this.originalImage)
6480
+ return;
6481
+ if (this.mosaicSession)
6482
+ return;
6483
+ if (!this.isImageLoaded())
6484
+ return;
6485
+ if (!this.canRunIdleOperation('enterMosaicMode'))
6486
+ return;
6487
+ enterMosaicMode(this.buildMosaicControllerContext());
6488
+ this.updateInputs();
6489
+ this.updateUi();
6490
+ const callbackContext = this.buildCallbackContext('enterMosaicMode', false);
6491
+ this.emitBusyChangeIfChanged(callbackContext);
6492
+ this.emitImageChanged(callbackContext);
6493
+ }
6494
+ exitMosaicMode() {
6495
+ if (!this.canvas || !this.mosaicSession)
6496
+ return;
6497
+ if (!this.canRunIdleOperation('exitMosaicMode'))
6498
+ return;
6499
+ exitMosaicMode(this.buildMosaicControllerContext());
6500
+ this.updateInputs();
6501
+ this.updateUi();
6502
+ const callbackContext = this.buildCallbackContext('exitMosaicMode', false);
6503
+ this.emitBusyChangeIfChanged(callbackContext);
6504
+ this.emitImageChanged(callbackContext);
6505
+ }
6506
+ isMosaicMode() {
6507
+ return this.mosaicSession !== null;
6508
+ }
6509
+ getMosaicConfig() {
6510
+ return cloneResolvedMosaicConfig(this.currentMosaicConfig);
6511
+ }
6512
+ setMosaicConfig(config) {
6513
+ this.applyMosaicConfigPatch(config, 'setMosaicConfig');
6514
+ }
6515
+ resetMosaicConfig() {
6516
+ if (this.isDisposed)
6517
+ return;
6518
+ const nextConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
6519
+ if (areResolvedMosaicConfigsEqual(this.currentMosaicConfig, nextConfig))
6520
+ return;
6521
+ this.currentMosaicConfig = nextConfig;
6522
+ if (this.mosaicSession && this.canvas) {
6523
+ updateMosaicPreview(this.buildMosaicControllerContext());
6524
+ }
6525
+ this.updateInputs();
6526
+ this.updateUi();
6527
+ this.emitImageChanged(this.buildCallbackContext('resetMosaicConfig', false));
6528
+ }
6529
+ setMosaicBrushSize(size) {
6530
+ this.applyMosaicConfigPatch({ brushSize: size }, 'setMosaicBrushSize');
6531
+ }
6532
+ setMosaicBlockSize(size) {
6533
+ this.applyMosaicConfigPatch({ blockSize: size }, 'setMosaicBlockSize');
6534
+ }
6535
+ applyMosaicConfigPatch(config, operation) {
6536
+ if (this.isDisposed)
6537
+ return;
6538
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
6539
+ reportWarning(this.options, new TypeError('[ImageEditor] Invalid Mosaic config object.'), 'Ignored invalid Mosaic config.');
6540
+ return;
6541
+ }
6542
+ const invalidFields = getInvalidMosaicConfigFields(config);
6543
+ if (invalidFields.length > 0) {
6544
+ reportWarning(this.options, new TypeError(`[ImageEditor] Ignored invalid Mosaic config field(s): ` +
6545
+ `${invalidFields.join(', ')}.`), 'Ignored invalid Mosaic config fields.');
6546
+ }
6547
+ const nextConfig = mergeMosaicConfigPatch(this.currentMosaicConfig, config);
6548
+ if (areResolvedMosaicConfigsEqual(this.currentMosaicConfig, nextConfig))
6549
+ return;
6550
+ this.currentMosaicConfig = nextConfig;
6551
+ if (this.mosaicSession && this.canvas) {
6552
+ updateMosaicPreview(this.buildMosaicControllerContext());
6553
+ }
6554
+ this.updateInputs();
6555
+ this.updateUi();
6556
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
6557
+ }
6558
+ buildMosaicControllerContext() {
6559
+ return {
6560
+ fabric: this.fabricModule,
6561
+ canvas: this.canvas,
6562
+ options: this.options,
6563
+ historyManager: this.historyManager,
6564
+ getMosaicConfig: () => cloneResolvedMosaicConfig(this.currentMosaicConfig),
6565
+ isImageLoaded: () => this.isImageLoaded(),
6566
+ getOriginalImage: () => this.originalImage,
6567
+ setOriginalImage: (image) => {
6568
+ this.originalImage = image;
6569
+ },
6570
+ getCurrentImageMimeType: () => this.currentImageMimeType,
6571
+ setCurrentImageMimeType: (mimeType) => {
6572
+ this.currentImageMimeType = mimeType;
6573
+ },
6574
+ getLastSnapshot: () => this.lastSnapshot,
6575
+ setLastSnapshot: (snapshot) => {
6576
+ this.lastSnapshot = snapshot;
6577
+ },
6578
+ captureSnapshot: () => this.captureSnapshotInternal(),
6579
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withAnimationQueueBypass()),
6580
+ updateUi: () => {
6581
+ this.updateUi();
6582
+ },
6583
+ updateInputs: () => {
6584
+ this.updateInputs();
6585
+ },
6586
+ hideAllMaskLabels: () => {
6587
+ this.hideAllMaskLabels();
6588
+ },
6589
+ emitImageChanged: (context) => {
6590
+ this.emitImageChanged(context);
6591
+ },
6592
+ emitBusyChangeIfChanged: (context) => {
6593
+ this.emitBusyChangeIfChanged(context);
6594
+ },
6595
+ buildCallbackContext: (operation, isInternal) => this.buildCallbackContext(operation, isInternal),
6596
+ getMosaicSession: () => this.mosaicSession,
6597
+ setMosaicSession: (session) => {
6598
+ this.mosaicSession = session;
6599
+ },
6600
+ };
5167
6601
  }
5168
6602
  enterCropMode() {
5169
6603
  if (!this.canvas || !this.originalImage)
@@ -5236,7 +6670,7 @@ class ImageEditor {
5236
6670
  },
5237
6671
  saveState: () => this.captureSnapshotInternal(),
5238
6672
  loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
5239
- loadImage: (base64, providedOptions) => this.loadImage(base64, this.withInternalOperationOptions(operationToken, providedOptions)),
6673
+ loadImage: (base64, providedOptions) => this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {})),
5240
6674
  getMaskCounter: () => this.maskCounter,
5241
6675
  setMaskCounter: (n) => {
5242
6676
  this.maskCounter = n;
@@ -5248,13 +6682,28 @@ class ImageEditor {
5248
6682
  }
5249
6683
  updateInputs() {
5250
6684
  const scaleId = this.elements.scalePercentageInput;
5251
- if (!scaleId)
5252
- return;
5253
- const scaleInputElement = document.getElementById(scaleId);
5254
- if (scaleInputElement)
5255
- scaleInputElement.value = String(Math.round(this.currentScale * 100));
6685
+ if (scaleId) {
6686
+ const scaleInputElement = document.getElementById(scaleId);
6687
+ if (scaleInputElement) {
6688
+ scaleInputElement.value = String(Math.round(this.currentScale * 100));
6689
+ }
6690
+ }
6691
+ const mosaicConfig = this.getMosaicConfig();
6692
+ const mosaicBrushSizeInputId = this.elements.mosaicBrushSizeInput;
6693
+ if (mosaicBrushSizeInputId) {
6694
+ const brushInput = document.getElementById(mosaicBrushSizeInputId);
6695
+ if (brushInput)
6696
+ brushInput.value = String(mosaicConfig.brushSize);
6697
+ }
6698
+ const mosaicBlockSizeInputId = this.elements.mosaicBlockSizeInput;
6699
+ if (mosaicBlockSizeInputId) {
6700
+ const blockInput = document.getElementById(mosaicBlockSizeInputId);
6701
+ if (blockInput)
6702
+ blockInput.value = String(mosaicConfig.blockSize);
6703
+ }
5256
6704
  }
5257
6705
  updateUi() {
6706
+ var _a;
5258
6707
  if (!this.canvas)
5259
6708
  return;
5260
6709
  const hasImage = !!this.originalImage;
@@ -5266,13 +6715,22 @@ class ImageEditor {
5266
6715
  const canUndo = this.historyManager.canUndo();
5267
6716
  const canRedo = this.historyManager.canRedo();
5268
6717
  const isInCropMode = this.cropSession !== null;
6718
+ const isInMosaicMode = this.mosaicSession !== null;
5269
6719
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
6720
+ const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
5270
6721
  if (isInCropMode) {
5271
6722
  CROP_MODE_CONTROL_KEYS.forEach((key) => {
5272
6723
  this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
5273
6724
  });
5274
6725
  return;
5275
6726
  }
6727
+ if (isInMosaicMode) {
6728
+ MOSAIC_MODE_CONTROL_KEYS.forEach((key) => {
6729
+ this.setControlEnabled(key, !isBusy && !isMosaicApplying && MOSAIC_MODE_ENABLED_KEYS.includes(key));
6730
+ });
6731
+ this.setControlEnabled('imageInput', false);
6732
+ return;
6733
+ }
5276
6734
  this.setControlEnabled('scalePercentageInput', hasImage && !isBusy);
5277
6735
  this.setControlEnabled('rotateLeftDegreesInput', hasImage && !isBusy);
5278
6736
  this.setControlEnabled('rotateRightDegreesInput', hasImage && !isBusy);
@@ -5289,6 +6747,10 @@ class ImageEditor {
5289
6747
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
5290
6748
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
5291
6749
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
6750
+ this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
6751
+ this.setControlEnabled('exitMosaicModeButton', false);
6752
+ this.setControlEnabled('mosaicBrushSizeInput', !this.isDisposed);
6753
+ this.setControlEnabled('mosaicBlockSizeInput', !this.isDisposed);
5292
6754
  this.setControlEnabled('imageInput', !isBusy);
5293
6755
  this.setControlEnabled('applyCropButton', false);
5294
6756
  this.setControlEnabled('cancelCropButton', false);
@@ -5386,6 +6848,14 @@ class ImageEditor {
5386
6848
  }
5387
6849
  this.cropSession = null;
5388
6850
  }
6851
+ if (this.mosaicSession && this.canvas) {
6852
+ try {
6853
+ exitMosaicMode(this.buildMosaicControllerContext());
6854
+ }
6855
+ catch {
6856
+ }
6857
+ this.mosaicSession = null;
6858
+ }
5389
6859
  if (this.canvas) {
5390
6860
  try {
5391
6861
  void Promise.resolve(this.canvas.dispose()).catch(() => {