@bensitu/image-editor 2.1.0 → 2.2.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 (113) hide show
  1. package/README.md +235 -81
  2. package/dist/cjs/index.cjs +2969 -747
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/annotation/annotation-lock.js +7 -0
  5. package/dist/esm/annotation/annotation-lock.js.map +1 -0
  6. package/dist/esm/annotation/annotation-manager.js +217 -0
  7. package/dist/esm/annotation/annotation-manager.js.map +1 -0
  8. package/dist/esm/annotation/annotation-style.js +50 -0
  9. package/dist/esm/annotation/annotation-style.js.map +1 -0
  10. package/dist/esm/annotation/draw-controller.js +114 -0
  11. package/dist/esm/annotation/draw-controller.js.map +1 -0
  12. package/dist/esm/annotation/text-controller.js +234 -0
  13. package/dist/esm/annotation/text-controller.js.map +1 -0
  14. package/dist/esm/core/default-options.js +232 -3
  15. package/dist/esm/core/default-options.js.map +1 -1
  16. package/dist/esm/core/editor-object-kind.js +37 -0
  17. package/dist/esm/core/editor-object-kind.js.map +1 -0
  18. package/dist/esm/core/errors.js +19 -0
  19. package/dist/esm/core/errors.js.map +1 -1
  20. package/dist/esm/core/layer-order.js +100 -0
  21. package/dist/esm/core/layer-order.js.map +1 -0
  22. package/dist/esm/core/public-types.js +34 -1
  23. package/dist/esm/core/public-types.js.map +1 -1
  24. package/dist/esm/core/state-serializer.js +104 -24
  25. package/dist/esm/core/state-serializer.js.map +1 -1
  26. package/dist/esm/crop/crop-controller.js +2 -0
  27. package/dist/esm/crop/crop-controller.js.map +1 -1
  28. package/dist/esm/export/export-format.js.map +1 -1
  29. package/dist/esm/export/export-service.js +123 -135
  30. package/dist/esm/export/export-service.js.map +1 -1
  31. package/dist/esm/export/overlay-merge-service.js +75 -0
  32. package/dist/esm/export/overlay-merge-service.js.map +1 -0
  33. package/dist/esm/history/history-manager.js +2 -2
  34. package/dist/esm/history/history-manager.js.map +1 -1
  35. package/dist/esm/image/image-loader.js +18 -49
  36. package/dist/esm/image/image-loader.js.map +1 -1
  37. package/dist/esm/image/transform-controller.js.map +1 -1
  38. package/dist/esm/image-editor.js +1063 -60
  39. package/dist/esm/image-editor.js.map +1 -1
  40. package/dist/esm/index.js +1 -1
  41. package/dist/esm/index.js.map +1 -1
  42. package/dist/esm/mask/mask-factory.js +39 -14
  43. package/dist/esm/mask/mask-factory.js.map +1 -1
  44. package/dist/esm/mask/mask-label-manager.js +2 -0
  45. package/dist/esm/mask/mask-label-manager.js.map +1 -1
  46. package/dist/esm/mask/mask-list.js.map +1 -1
  47. package/dist/esm/mask/mask-style.js.map +1 -1
  48. package/dist/esm/mosaic/mosaic-controller.js +24 -28
  49. package/dist/esm/mosaic/mosaic-controller.js.map +1 -1
  50. package/dist/esm/utils/image-element-loader.js +55 -0
  51. package/dist/esm/utils/image-element-loader.js.map +1 -0
  52. package/dist/esm/utils/pointer.js +28 -0
  53. package/dist/esm/utils/pointer.js.map +1 -0
  54. package/dist/types/annotation/annotation-lock.d.ts +12 -0
  55. package/dist/types/annotation/annotation-lock.d.ts.map +1 -0
  56. package/dist/types/annotation/annotation-manager.d.ts +33 -0
  57. package/dist/types/annotation/annotation-manager.d.ts.map +1 -0
  58. package/dist/types/annotation/annotation-style.d.ts +13 -0
  59. package/dist/types/annotation/annotation-style.d.ts.map +1 -0
  60. package/dist/types/annotation/draw-controller.d.ts +43 -0
  61. package/dist/types/annotation/draw-controller.d.ts.map +1 -0
  62. package/dist/types/annotation/text-controller.d.ts +47 -0
  63. package/dist/types/annotation/text-controller.d.ts.map +1 -0
  64. package/dist/types/core/default-options.d.ts +14 -2
  65. package/dist/types/core/default-options.d.ts.map +1 -1
  66. package/dist/types/core/editor-object-kind.d.ts +29 -0
  67. package/dist/types/core/editor-object-kind.d.ts.map +1 -0
  68. package/dist/types/core/errors.d.ts +11 -1
  69. package/dist/types/core/errors.d.ts.map +1 -1
  70. package/dist/types/core/layer-order.d.ts +21 -0
  71. package/dist/types/core/layer-order.d.ts.map +1 -0
  72. package/dist/types/core/public-types.d.ts +222 -24
  73. package/dist/types/core/public-types.d.ts.map +1 -1
  74. package/dist/types/core/state-serializer.d.ts +30 -5
  75. package/dist/types/core/state-serializer.d.ts.map +1 -1
  76. package/dist/types/crop/crop-controller.d.ts +6 -7
  77. package/dist/types/crop/crop-controller.d.ts.map +1 -1
  78. package/dist/types/export/export-format.d.ts +5 -33
  79. package/dist/types/export/export-format.d.ts.map +1 -1
  80. package/dist/types/export/export-service.d.ts +24 -15
  81. package/dist/types/export/export-service.d.ts.map +1 -1
  82. package/dist/types/export/overlay-merge-service.d.ts +38 -0
  83. package/dist/types/export/overlay-merge-service.d.ts.map +1 -0
  84. package/dist/types/history/history-manager.d.ts +11 -14
  85. package/dist/types/history/history-manager.d.ts.map +1 -1
  86. package/dist/types/image/image-loader.d.ts +22 -17
  87. package/dist/types/image/image-loader.d.ts.map +1 -1
  88. package/dist/types/image/image-resampler.d.ts +1 -1
  89. package/dist/types/image/transform-controller.d.ts +5 -7
  90. package/dist/types/image/transform-controller.d.ts.map +1 -1
  91. package/dist/types/image-editor.d.ts +75 -7
  92. package/dist/types/image-editor.d.ts.map +1 -1
  93. package/dist/types/index.d.cts +3 -3
  94. package/dist/types/index.d.cts.map +1 -1
  95. package/dist/types/index.d.ts +3 -3
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/dist/types/mask/mask-factory.d.ts.map +1 -1
  98. package/dist/types/mask/mask-label-manager.d.ts +10 -9
  99. package/dist/types/mask/mask-label-manager.d.ts.map +1 -1
  100. package/dist/types/mask/mask-list.d.ts +11 -12
  101. package/dist/types/mask/mask-list.d.ts.map +1 -1
  102. package/dist/types/mask/mask-style.d.ts +19 -20
  103. package/dist/types/mask/mask-style.d.ts.map +1 -1
  104. package/dist/types/mosaic/mosaic-controller.d.ts +3 -3
  105. package/dist/types/mosaic/mosaic-controller.d.ts.map +1 -1
  106. package/dist/types/ui/visibility-state.d.ts +2 -2
  107. package/dist/types/utils/image-element-loader.d.ts +19 -0
  108. package/dist/types/utils/image-element-loader.d.ts.map +1 -0
  109. package/dist/types/utils/pointer.d.ts +16 -0
  110. package/dist/types/utils/pointer.d.ts.map +1 -0
  111. package/dist/umd/image-editor.umd.js +1 -1
  112. package/dist/umd/image-editor.umd.js.map +1 -1
  113. package/package.json +1 -1
@@ -176,7 +176,8 @@ const DEFAULT_OPTIONS = {
176
176
  exportMultiplier: 1,
177
177
  maxExportPixels: 50000000,
178
178
  exportAreaByDefault: 'image',
179
- mergeMaskByDefault: true,
179
+ mergeMasksByDefault: true,
180
+ mergeAnnotationsByDefault: true,
180
181
  defaultMaskWidth: 50,
181
182
  defaultMaskHeight: 80,
182
183
  defaultMaskConfig: EMPTY_DEFAULT_MASK_CONFIG,
@@ -184,6 +185,8 @@ const DEFAULT_OPTIONS = {
184
185
  maskLabelOnSelect: true,
185
186
  maskLabelOffset: 3,
186
187
  maskName: 'mask',
188
+ textAnnotationName: 'text',
189
+ drawAnnotationName: 'draw',
187
190
  groupSelection: false,
188
191
  showPlaceholder: true,
189
192
  initialImageBase64: null,
@@ -195,6 +198,7 @@ const DEFAULT_OPTIONS = {
195
198
  onBusyChange: null,
196
199
  onEditorDisposed: null,
197
200
  onMasksChanged: null,
201
+ onAnnotationsChanged: null,
198
202
  onSelectionChange: null,
199
203
  onError: null,
200
204
  onWarning: null,
@@ -231,6 +235,37 @@ const DEFAULT_MOSAIC_CONFIG = Object.freeze({
231
235
  outputFileType: 'source',
232
236
  outputQuality: undefined,
233
237
  });
238
+ const DEFAULT_TEXT_ANNOTATION_CONFIG = Object.freeze({
239
+ text: 'Text',
240
+ left: undefined,
241
+ top: undefined,
242
+ width: 200,
243
+ fontSize: 32,
244
+ fontFamily: 'sans-serif',
245
+ fontWeight: 'normal',
246
+ fill: '#ff0000',
247
+ backgroundColor: 'rgba(255,255,255,0)',
248
+ textAlign: 'left',
249
+ angle: 0,
250
+ selectable: true,
251
+ evented: true,
252
+ editable: true,
253
+ enterEditing: true,
254
+ annotationHidden: false,
255
+ annotationLocked: false,
256
+ styles: Object.freeze({}),
257
+ });
258
+ const DEFAULT_DRAW_CONFIG = Object.freeze({
259
+ brushSize: 8,
260
+ color: '#ff0000',
261
+ opacity: 1,
262
+ lineCap: 'round',
263
+ lineJoin: 'round',
264
+ selectable: true,
265
+ evented: true,
266
+ annotationHidden: false,
267
+ annotationLocked: false,
268
+ });
234
269
  const KNOWN_TOP_LEVEL_KEYS = new Set([
235
270
  'canvasWidth',
236
271
  'canvasHeight',
@@ -252,7 +287,8 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
252
287
  'exportMultiplier',
253
288
  'maxExportPixels',
254
289
  'exportAreaByDefault',
255
- 'mergeMaskByDefault',
290
+ 'mergeMasksByDefault',
291
+ 'mergeAnnotationsByDefault',
256
292
  'defaultMaskWidth',
257
293
  'defaultMaskHeight',
258
294
  'defaultMaskConfig',
@@ -260,6 +296,8 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
260
296
  'maskLabelOnSelect',
261
297
  'maskLabelOffset',
262
298
  'maskName',
299
+ 'textAnnotationName',
300
+ 'drawAnnotationName',
263
301
  'groupSelection',
264
302
  'showPlaceholder',
265
303
  'initialImageBase64',
@@ -271,12 +309,15 @@ const KNOWN_TOP_LEVEL_KEYS = new Set([
271
309
  'onBusyChange',
272
310
  'onEditorDisposed',
273
311
  'onMasksChanged',
312
+ 'onAnnotationsChanged',
274
313
  'onSelectionChange',
275
314
  'onError',
276
315
  'onWarning',
277
316
  'label',
278
317
  'crop',
279
318
  'defaultMosaicConfig',
319
+ 'defaultTextConfig',
320
+ 'defaultDrawConfig',
280
321
  ]);
281
322
  function normalizeCallback(value) {
282
323
  return typeof value === 'function' ? value : null;
@@ -514,6 +555,180 @@ function areResolvedMosaicConfigsEqual(left, right) {
514
555
  left.outputFileType === right.outputFileType &&
515
556
  left.outputQuality === right.outputQuality);
516
557
  }
558
+ function cloneResolvedTextAnnotationConfig(config) {
559
+ return {
560
+ ...config,
561
+ styles: { ...config.styles },
562
+ };
563
+ }
564
+ function cloneResolvedDrawConfig(config) {
565
+ return { ...config };
566
+ }
567
+ function normalizeTextAlign(value, fallback) {
568
+ return value === 'left' || value === 'center' || value === 'right' || value === 'justify'
569
+ ? value
570
+ : fallback;
571
+ }
572
+ function normalizePositiveNumber(value, fallback) {
573
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
574
+ }
575
+ function normalizeBoolean(value, fallback) {
576
+ return typeof value === 'boolean' ? value : fallback;
577
+ }
578
+ function normalizeString(value, fallback) {
579
+ return typeof value === 'string' ? value : fallback;
580
+ }
581
+ function normalizeTextLeftTop(value) {
582
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
583
+ }
584
+ function normalizeTextboxStyles(value) {
585
+ if (!isConfigObject(value))
586
+ return {};
587
+ return { ...value };
588
+ }
589
+ function mergeTextAnnotationConfigPatch(current, patch, fallback = current) {
590
+ const raw = isConfigObject(patch) ? patch : {};
591
+ const next = cloneResolvedTextAnnotationConfig(current);
592
+ if (hasOwn(raw, 'text'))
593
+ next.text = normalizeString(raw.text, fallback.text);
594
+ if (hasOwn(raw, 'left'))
595
+ next.left = normalizeTextLeftTop(raw.left);
596
+ if (hasOwn(raw, 'top'))
597
+ next.top = normalizeTextLeftTop(raw.top);
598
+ if (hasOwn(raw, 'width'))
599
+ next.width = normalizePositiveNumber(raw.width, fallback.width);
600
+ if (hasOwn(raw, 'fontSize')) {
601
+ next.fontSize = normalizePositiveNumber(raw.fontSize, fallback.fontSize);
602
+ }
603
+ if (hasOwn(raw, 'fontFamily')) {
604
+ next.fontFamily = normalizeString(raw.fontFamily, fallback.fontFamily);
605
+ }
606
+ if (hasOwn(raw, 'fontWeight')) {
607
+ next.fontWeight =
608
+ typeof raw.fontWeight === 'string' || typeof raw.fontWeight === 'number'
609
+ ? raw.fontWeight
610
+ : fallback.fontWeight;
611
+ }
612
+ if (hasOwn(raw, 'fill'))
613
+ next.fill = normalizeString(raw.fill, fallback.fill);
614
+ if (hasOwn(raw, 'backgroundColor')) {
615
+ next.backgroundColor = normalizeString(raw.backgroundColor, fallback.backgroundColor);
616
+ }
617
+ if (hasOwn(raw, 'textAlign'))
618
+ next.textAlign = normalizeTextAlign(raw.textAlign, fallback.textAlign);
619
+ if (hasOwn(raw, 'angle'))
620
+ next.angle = normalizeFiniteNumber(raw.angle, fallback.angle);
621
+ if (hasOwn(raw, 'selectable'))
622
+ next.selectable = normalizeBoolean(raw.selectable, fallback.selectable);
623
+ if (hasOwn(raw, 'evented'))
624
+ next.evented = normalizeBoolean(raw.evented, fallback.evented);
625
+ if (hasOwn(raw, 'editable'))
626
+ next.editable = normalizeBoolean(raw.editable, fallback.editable);
627
+ if (hasOwn(raw, 'enterEditing')) {
628
+ next.enterEditing = normalizeBoolean(raw.enterEditing, fallback.enterEditing);
629
+ }
630
+ if (hasOwn(raw, 'annotationHidden')) {
631
+ next.annotationHidden = normalizeBoolean(raw.annotationHidden, fallback.annotationHidden);
632
+ }
633
+ if (hasOwn(raw, 'annotationLocked')) {
634
+ next.annotationLocked = normalizeBoolean(raw.annotationLocked, fallback.annotationLocked);
635
+ }
636
+ if (hasOwn(raw, 'styles')) {
637
+ next.styles = {
638
+ ...next.styles,
639
+ ...normalizeTextboxStyles(raw.styles),
640
+ };
641
+ }
642
+ return next;
643
+ }
644
+ function normalizeTextAnnotationConfig(input, fallback) {
645
+ if (!isConfigObject(input))
646
+ return cloneResolvedTextAnnotationConfig(fallback);
647
+ return mergeTextAnnotationConfigPatch(fallback, input);
648
+ }
649
+ function normalizeLineCap(value, fallback) {
650
+ return value === 'butt' || value === 'round' || value === 'square' ? value : fallback;
651
+ }
652
+ function normalizeLineJoin(value, fallback) {
653
+ return value === 'bevel' || value === 'round' || value === 'miter' ? value : fallback;
654
+ }
655
+ function normalizeOpacity(value, fallback) {
656
+ if (typeof value !== 'number' || !Number.isFinite(value))
657
+ return fallback;
658
+ return Math.max(0, Math.min(1, value));
659
+ }
660
+ function mergeDrawConfigPatch(current, patch, fallback = current) {
661
+ const raw = isConfigObject(patch) ? patch : {};
662
+ const next = cloneResolvedDrawConfig(current);
663
+ if (hasOwn(raw, 'brushSize')) {
664
+ next.brushSize = normalizePositiveNumber(raw.brushSize, fallback.brushSize);
665
+ }
666
+ if (hasOwn(raw, 'color'))
667
+ next.color = normalizeString(raw.color, fallback.color);
668
+ if (hasOwn(raw, 'opacity'))
669
+ next.opacity = normalizeOpacity(raw.opacity, fallback.opacity);
670
+ if (hasOwn(raw, 'lineCap'))
671
+ next.lineCap = normalizeLineCap(raw.lineCap, fallback.lineCap);
672
+ if (hasOwn(raw, 'lineJoin'))
673
+ next.lineJoin = normalizeLineJoin(raw.lineJoin, fallback.lineJoin);
674
+ if (hasOwn(raw, 'selectable'))
675
+ next.selectable = normalizeBoolean(raw.selectable, fallback.selectable);
676
+ if (hasOwn(raw, 'evented'))
677
+ next.evented = normalizeBoolean(raw.evented, fallback.evented);
678
+ if (hasOwn(raw, 'annotationHidden')) {
679
+ next.annotationHidden = normalizeBoolean(raw.annotationHidden, fallback.annotationHidden);
680
+ }
681
+ if (hasOwn(raw, 'annotationLocked')) {
682
+ next.annotationLocked = normalizeBoolean(raw.annotationLocked, fallback.annotationLocked);
683
+ }
684
+ return next;
685
+ }
686
+ function normalizeDrawConfig(input, fallback) {
687
+ if (!isConfigObject(input))
688
+ return cloneResolvedDrawConfig(fallback);
689
+ return mergeDrawConfigPatch(fallback, input);
690
+ }
691
+ function areResolvedTextAnnotationConfigsEqual(left, right) {
692
+ return JSON.stringify(left) === JSON.stringify(right);
693
+ }
694
+ function areResolvedDrawConfigsEqual(left, right) {
695
+ return (left.brushSize === right.brushSize &&
696
+ left.color === right.color &&
697
+ left.opacity === right.opacity &&
698
+ left.lineCap === right.lineCap &&
699
+ left.lineJoin === right.lineJoin &&
700
+ left.selectable === right.selectable &&
701
+ left.evented === right.evented &&
702
+ left.annotationHidden === right.annotationHidden &&
703
+ left.annotationLocked === right.annotationLocked);
704
+ }
705
+ function getInvalidTextAnnotationConfigFields(input) {
706
+ const raw = isConfigObject(input) ? input : {};
707
+ const invalid = [];
708
+ if (hasOwn(raw, 'text') && typeof raw.text !== 'string')
709
+ invalid.push('text');
710
+ if (hasOwn(raw, 'width') && !isFiniteNumber$1(raw.width))
711
+ invalid.push('width');
712
+ if (hasOwn(raw, 'fontSize') && !isFiniteNumber$1(raw.fontSize))
713
+ invalid.push('fontSize');
714
+ if (hasOwn(raw, 'fontFamily') && typeof raw.fontFamily !== 'string')
715
+ invalid.push('fontFamily');
716
+ if (hasOwn(raw, 'fill') && typeof raw.fill !== 'string') {
717
+ invalid.push('fill');
718
+ }
719
+ return invalid;
720
+ }
721
+ function getInvalidDrawConfigFields(input) {
722
+ const raw = isConfigObject(input) ? input : {};
723
+ const invalid = [];
724
+ if (hasOwn(raw, 'brushSize') && !isFiniteNumber$1(raw.brushSize))
725
+ invalid.push('brushSize');
726
+ if (hasOwn(raw, 'color') && typeof raw.color !== 'string')
727
+ invalid.push('color');
728
+ if (hasOwn(raw, 'opacity') && !isFiniteNumber$1(raw.opacity))
729
+ invalid.push('opacity');
730
+ return invalid;
731
+ }
517
732
  function resolveOptions(input) {
518
733
  var _a, _b, _c, _d;
519
734
  const raw = input !== null && input !== void 0 ? input : {};
@@ -521,8 +736,13 @@ function resolveOptions(input) {
521
736
  for (const key of Object.keys(raw)) {
522
737
  if (!KNOWN_TOP_LEVEL_KEYS.has(key))
523
738
  continue;
524
- if (key === 'label' || key === 'crop' || key === 'defaultMosaicConfig')
739
+ if (key === 'label' ||
740
+ key === 'crop' ||
741
+ key === 'defaultMosaicConfig' ||
742
+ key === 'defaultTextConfig' ||
743
+ key === 'defaultDrawConfig') {
525
744
  continue;
745
+ }
526
746
  if (key === 'onImageLoadStart' ||
527
747
  key === 'onImageLoaded' ||
528
748
  key === 'onImageCleared' ||
@@ -530,6 +750,7 @@ function resolveOptions(input) {
530
750
  key === 'onBusyChange' ||
531
751
  key === 'onEditorDisposed' ||
532
752
  key === 'onMasksChanged' ||
753
+ key === 'onAnnotationsChanged' ||
533
754
  key === 'onSelectionChange' ||
534
755
  key === 'onError' ||
535
756
  key === 'onWarning') {
@@ -625,6 +846,7 @@ function resolveOptions(input) {
625
846
  resolved.onBusyChange = normalizeCallback(raw.onBusyChange);
626
847
  resolved.onEditorDisposed = normalizeCallback(raw.onEditorDisposed);
627
848
  resolved.onMasksChanged = normalizeCallback(raw.onMasksChanged);
849
+ resolved.onAnnotationsChanged = normalizeCallback(raw.onAnnotationsChanged);
628
850
  resolved.onSelectionChange = normalizeCallback(raw.onSelectionChange);
629
851
  resolved.onError = normalizeCallback(raw.onError);
630
852
  resolved.onWarning = normalizeCallback(raw.onWarning);
@@ -668,11 +890,18 @@ function resolveOptions(input) {
668
890
  Object.freeze(defaultMosaicConfig.previewStrokeDashArray);
669
891
  }
670
892
  Object.freeze(defaultMosaicConfig);
893
+ const defaultTextConfig = normalizeTextAnnotationConfig(raw.defaultTextConfig, DEFAULT_TEXT_ANNOTATION_CONFIG);
894
+ Object.freeze(defaultTextConfig.styles);
895
+ Object.freeze(defaultTextConfig);
896
+ const defaultDrawConfig = normalizeDrawConfig(raw.defaultDrawConfig, DEFAULT_DRAW_CONFIG);
897
+ Object.freeze(defaultDrawConfig);
671
898
  return Object.freeze({
672
899
  ...resolved,
673
900
  label,
674
901
  crop,
675
902
  defaultMosaicConfig,
903
+ defaultTextConfig,
904
+ defaultDrawConfig,
676
905
  });
677
906
  }
678
907
 
@@ -833,11 +1062,83 @@ class OperationGuard {
833
1062
  }
834
1063
  }
835
1064
 
1065
+ function isBaseImageObject(object) {
1066
+ return (!!object &&
1067
+ typeof object === 'object' &&
1068
+ object.editorObjectKind === 'baseImage');
1069
+ }
836
1070
  function isMaskObject(object) {
837
- return 'maskId' in object && typeof object.maskId === 'number';
1071
+ const candidate = object;
1072
+ return (!!candidate &&
1073
+ candidate.editorObjectKind === 'mask' &&
1074
+ typeof candidate.maskId === 'number' &&
1075
+ typeof candidate.maskUid === 'string' &&
1076
+ typeof candidate.maskName === 'string');
1077
+ }
1078
+ function isAnnotationObject(object) {
1079
+ const candidate = object;
1080
+ return (!!candidate &&
1081
+ candidate.editorObjectKind === 'annotation' &&
1082
+ typeof candidate.annotationId === 'number' &&
1083
+ typeof candidate.annotationType === 'string' &&
1084
+ typeof candidate.annotationName === 'string');
1085
+ }
1086
+ function isTextAnnotationObject(object) {
1087
+ return isAnnotationObject(object) && object.annotationType === 'text';
1088
+ }
1089
+ function isDrawAnnotationObject(object) {
1090
+ return isAnnotationObject(object) && object.annotationType === 'draw';
1091
+ }
1092
+ function isSessionObject(object) {
1093
+ const candidate = object;
1094
+ return (!!candidate &&
1095
+ candidate.editorObjectKind === 'session' &&
1096
+ typeof candidate.sessionObjectType === 'string');
1097
+ }
1098
+ function isEditableOverlayObject(object) {
1099
+ return isMaskObject(object) || isAnnotationObject(object);
1100
+ }
1101
+
1102
+ function markBaseImageObject(image) {
1103
+ const baseImage = image;
1104
+ baseImage.editorObjectKind = 'baseImage';
1105
+ return baseImage;
1106
+ }
1107
+ function markMaskObject(object, meta) {
1108
+ const mask = object;
1109
+ mask.editorObjectKind = 'mask';
1110
+ mask.maskId = meta.maskId;
1111
+ mask.maskUid = meta.maskUid;
1112
+ mask.maskName = meta.maskName;
1113
+ mask.originalAlpha = meta.originalAlpha;
1114
+ if ('originalStroke' in meta)
1115
+ mask.originalStroke = meta.originalStroke;
1116
+ if (typeof meta.originalStrokeWidth === 'number') {
1117
+ mask.originalStrokeWidth = meta.originalStrokeWidth;
1118
+ }
1119
+ return mask;
1120
+ }
1121
+ function markAnnotationObject(object, meta) {
1122
+ var _a, _b;
1123
+ const annotation = object;
1124
+ annotation.editorObjectKind = 'annotation';
1125
+ annotation.annotationId = meta.annotationId;
1126
+ annotation.annotationType = meta.annotationType;
1127
+ annotation.annotationName = meta.annotationName;
1128
+ annotation.annotationHidden = (_a = meta.annotationHidden) !== null && _a !== void 0 ? _a : false;
1129
+ annotation.annotationLocked = (_b = meta.annotationLocked) !== null && _b !== void 0 ? _b : false;
1130
+ return annotation;
1131
+ }
1132
+ function markSessionObject(object, sessionObjectType) {
1133
+ const sessionObject = object;
1134
+ sessionObject.editorObjectKind = 'session';
1135
+ sessionObject.sessionObjectType = sessionObjectType;
1136
+ return sessionObject;
838
1137
  }
839
1138
 
840
1139
  const SNAPSHOT_CUSTOM_KEYS = [
1140
+ 'editorObjectKind',
1141
+ 'sessionObjectType',
841
1142
  'maskId',
842
1143
  'maskUid',
843
1144
  'maskName',
@@ -855,6 +1156,11 @@ const SNAPSHOT_CUSTOM_KEYS = [
855
1156
  'cornerColor',
856
1157
  'cornerSize',
857
1158
  'isMosaicPreview',
1159
+ 'annotationId',
1160
+ 'annotationType',
1161
+ 'annotationName',
1162
+ 'annotationHidden',
1163
+ 'annotationLocked',
858
1164
  ];
859
1165
  function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
860
1166
  if (!Array.isArray(jsonObjects))
@@ -864,6 +1170,12 @@ function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
864
1170
  const jsonObject = jsonObjects[index];
865
1171
  if (!liveObject || !jsonObject)
866
1172
  continue;
1173
+ if (typeof liveObject.editorObjectKind === 'string') {
1174
+ jsonObject.editorObjectKind = liveObject.editorObjectKind;
1175
+ }
1176
+ if (typeof liveObject.sessionObjectType === 'string') {
1177
+ jsonObject.sessionObjectType = liveObject.sessionObjectType;
1178
+ }
867
1179
  if (typeof liveObject.maskId === 'number')
868
1180
  jsonObject.maskId = liveObject.maskId;
869
1181
  if (typeof liveObject.maskUid === 'string')
@@ -908,9 +1220,24 @@ function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
908
1220
  jsonObject.maskLabel = true;
909
1221
  if (liveObject.isMosaicPreview === true)
910
1222
  jsonObject.isMosaicPreview = true;
1223
+ if (typeof liveObject.annotationId === 'number') {
1224
+ jsonObject.annotationId = liveObject.annotationId;
1225
+ }
1226
+ if (typeof liveObject.annotationType === 'string') {
1227
+ jsonObject.annotationType = liveObject.annotationType;
1228
+ }
1229
+ if (typeof liveObject.annotationName === 'string') {
1230
+ jsonObject.annotationName = liveObject.annotationName;
1231
+ }
1232
+ if (typeof liveObject.annotationHidden === 'boolean') {
1233
+ jsonObject.annotationHidden = liveObject.annotationHidden;
1234
+ }
1235
+ if (typeof liveObject.annotationLocked === 'boolean') {
1236
+ jsonObject.annotationLocked = liveObject.annotationLocked;
1237
+ }
911
1238
  }
912
1239
  }
913
- function isActiveSelectionObject(object) {
1240
+ function isActiveSelectionObject$2(object) {
914
1241
  if (!object)
915
1242
  return false;
916
1243
  const type = typeof object.type === 'string' ? object.type.toLowerCase() : '';
@@ -929,22 +1256,34 @@ function saveState(input) {
929
1256
  : typeof input.activeMaskId === 'number'
930
1257
  ? input.activeMaskId
931
1258
  : null;
932
- if (isActiveSelectionObject(activeObject)) {
1259
+ const activeAnnotationId = activeObject && isAnnotationObject(activeObject)
1260
+ ? activeObject.annotationId
1261
+ : typeof input.activeAnnotationId === 'number'
1262
+ ? input.activeAnnotationId
1263
+ : null;
1264
+ if (isActiveSelectionObject$2(activeObject)) {
933
1265
  canvas.discardActiveObject();
934
1266
  }
935
1267
  const jsonObj = canvas.toJSON(SNAPSHOT_CUSTOM_KEYS);
936
1268
  copySnapshotCustomPropsFromCanvas(canvas.getObjects(), jsonObj.objects);
937
1269
  if (Array.isArray(jsonObj.objects)) {
938
- jsonObj.objects = jsonObj.objects.filter((o) => o.isCropRect !== true && o.maskLabel !== true && o.isMosaicPreview !== true);
1270
+ jsonObj.objects = jsonObj.objects.filter((o) => o.editorObjectKind !== 'session' &&
1271
+ o.isCropRect !== true &&
1272
+ o.maskLabel !== true &&
1273
+ o.isMosaicPreview !== true);
939
1274
  }
940
1275
  jsonObj._editorState = {
941
1276
  currentScale,
942
1277
  currentRotation,
943
1278
  baseImageScale,
944
1279
  currentImageMimeType: (_c = input.currentImageMimeType) !== null && _c !== void 0 ? _c : null,
1280
+ activeObjectKind: activeMaskId !== null ? 'mask' : activeAnnotationId !== null ? 'annotation' : null,
945
1281
  };
946
1282
  if (activeMaskId !== null)
947
1283
  jsonObj._editorState.activeMaskId = activeMaskId;
1284
+ if (activeAnnotationId !== null) {
1285
+ jsonObj._editorState.activeAnnotationId = activeAnnotationId;
1286
+ }
948
1287
  return JSON.stringify(jsonObj);
949
1288
  }
950
1289
  async function loadFromState(input) {
@@ -960,7 +1299,7 @@ async function loadFromState(input) {
960
1299
  }
961
1300
  await canvas.loadFromJSON(json);
962
1301
  const objects = canvas.getObjects();
963
- restoreMaskPropsFromJson(objects, (_a = json.objects) !== null && _a !== void 0 ? _a : []);
1302
+ restoreEditorObjectPropsFromJson(objects, (_a = json.objects) !== null && _a !== void 0 ? _a : []);
964
1303
  const editorState = json._editorState && typeof json._editorState === 'object'
965
1304
  ? {
966
1305
  currentScale: typeof json._editorState.currentScale === 'number'
@@ -977,6 +1316,16 @@ async function loadFromState(input) {
977
1316
  if (editorState && json._editorState && typeof json._editorState.activeMaskId === 'number') {
978
1317
  editorState.activeMaskId = json._editorState.activeMaskId;
979
1318
  }
1319
+ if (editorState &&
1320
+ json._editorState &&
1321
+ typeof json._editorState.activeAnnotationId === 'number') {
1322
+ editorState.activeAnnotationId = json._editorState.activeAnnotationId;
1323
+ }
1324
+ if (editorState && json._editorState && 'activeObjectKind' in json._editorState) {
1325
+ const kind = json._editorState.activeObjectKind;
1326
+ editorState.activeObjectKind =
1327
+ kind === 'mask' || kind === 'annotation' || kind === null ? kind : null;
1328
+ }
980
1329
  if (editorState && json._editorState && 'currentImageMimeType' in json._editorState) {
981
1330
  const mimeType = json._editorState.currentImageMimeType;
982
1331
  editorState.currentImageMimeType =
@@ -987,29 +1336,54 @@ async function loadFromState(input) {
987
1336
  const maxMaskId = objects
988
1337
  .filter(isMaskObject)
989
1338
  .reduce((max, maskObject) => Math.max(max, maskObject.maskId), 0);
990
- const originalImage = ((_b = objects.find(isOriginalImageObject)) !== null && _b !== void 0 ? _b : null);
1339
+ const maxAnnotationId = objects
1340
+ .filter(isAnnotationObject)
1341
+ .reduce((max, annotationObject) => Math.max(max, annotationObject.annotationId), 0);
1342
+ const masks = objects.filter(isMaskObject);
1343
+ const annotations = objects.filter(isAnnotationObject);
1344
+ const originalImage = (_b = objects.find(isBaseImageObject)) !== null && _b !== void 0 ? _b : null;
991
1345
  return {
992
1346
  editorState,
993
1347
  maxMaskId,
1348
+ maxAnnotationId,
994
1349
  originalImage,
995
1350
  objects,
1351
+ masks,
1352
+ annotations,
996
1353
  jsonString,
997
1354
  };
998
1355
  }
999
- function isOriginalImageObject(object) {
1000
- if (isMaskObject(object))
1001
- return false;
1002
- const type = typeof object.type === 'string' ? object.type.toLowerCase() : '';
1003
- if (type === 'image')
1004
- return true;
1005
- const isType = object.isType;
1006
- return typeof isType === 'function' && isType.call(object, 'image');
1007
- }
1008
- function restoreMaskPropsFromJson(canvasObjs, jsonObjs) {
1009
- var _a, _b, _c, _d, _e;
1356
+ function restoreEditorObjectPropsFromJson(canvasObjs, jsonObjs) {
1357
+ var _a, _b, _c, _d;
1358
+ jsonObjs.forEach((jObj, index) => {
1359
+ const canvasObj = canvasObjs[index];
1360
+ if (!canvasObj)
1361
+ return;
1362
+ if (jObj.editorObjectKind === 'baseImage') {
1363
+ markBaseImageObject(canvasObj);
1364
+ return;
1365
+ }
1366
+ if (jObj.editorObjectKind === 'annotation' &&
1367
+ typeof jObj.annotationId === 'number' &&
1368
+ typeof jObj.annotationType === 'string' &&
1369
+ typeof jObj.annotationName === 'string') {
1370
+ markAnnotationObject(canvasObj, {
1371
+ annotationId: jObj.annotationId,
1372
+ annotationType: jObj.annotationType === 'draw' ? 'draw' : 'text',
1373
+ annotationName: jObj.annotationName,
1374
+ annotationHidden: typeof jObj.annotationHidden === 'boolean' ? jObj.annotationHidden : false,
1375
+ annotationLocked: typeof jObj.annotationLocked === 'boolean' ? jObj.annotationLocked : false,
1376
+ });
1377
+ return;
1378
+ }
1379
+ if (jObj.editorObjectKind === 'session' && typeof jObj.sessionObjectType === 'string') {
1380
+ canvasObj.editorObjectKind = 'session';
1381
+ canvasObj.sessionObjectType = jObj.sessionObjectType;
1382
+ }
1383
+ });
1010
1384
  const consumedCanvasIndexes = new Set();
1011
1385
  for (const jObj of jsonObjs) {
1012
- if (typeof jObj.maskId !== 'number')
1386
+ if (jObj.editorObjectKind !== 'mask' || typeof jObj.maskId !== 'number')
1013
1387
  continue;
1014
1388
  const jType = String((_a = jObj.type) !== null && _a !== void 0 ? _a : '');
1015
1389
  const jLeft = Number((_b = jObj.left) !== null && _b !== void 0 ? _b : 0);
@@ -1038,15 +1412,19 @@ function restoreMaskPropsFromJson(canvasObjs, jsonObjs) {
1038
1412
  consumedCanvasIndexes.add(matchIndex);
1039
1413
  const match = canvasObjs[matchIndex];
1040
1414
  const maskObject = match;
1041
- maskObject.maskId = jObj.maskId;
1042
- if (typeof jObj.maskUid === 'string') {
1043
- maskObject.maskUid = jObj.maskUid;
1044
- }
1045
- maskObject.maskName = String((_d = jObj.maskName) !== null && _d !== void 0 ? _d : '');
1046
- maskObject.originalAlpha =
1047
- typeof jObj.originalAlpha === 'number'
1415
+ const originalStroke = 'originalStroke' in jObj
1416
+ ? jObj.originalStroke
1417
+ : undefined;
1418
+ markMaskObject(maskObject, {
1419
+ maskId: jObj.maskId,
1420
+ maskUid: typeof jObj.maskUid === 'string' ? jObj.maskUid : `mask-${jObj.maskId}`,
1421
+ maskName: typeof jObj.maskName === 'string' ? jObj.maskName : '',
1422
+ originalAlpha: typeof jObj.originalAlpha === 'number'
1048
1423
  ? jObj.originalAlpha
1049
- : ((_e = maskObject.opacity) !== null && _e !== void 0 ? _e : 0.5);
1424
+ : ((_d = maskObject.opacity) !== null && _d !== void 0 ? _d : 0.5),
1425
+ originalStroke,
1426
+ originalStrokeWidth: typeof jObj.originalStrokeWidth === 'number' ? jObj.originalStrokeWidth : undefined,
1427
+ });
1050
1428
  if ('originalStroke' in jObj) {
1051
1429
  maskObject.originalStroke = jObj.originalStroke;
1052
1430
  }
@@ -1134,8 +1512,8 @@ class HistoryManager {
1134
1512
  });
1135
1513
  this.maxSize = maxSize;
1136
1514
  }
1137
- execute(command) {
1138
- void command.execute();
1515
+ async execute(command) {
1516
+ await command.execute();
1139
1517
  this.pushAndTrim(command);
1140
1518
  }
1141
1519
  push(command) {
@@ -1228,295 +1606,363 @@ function detectFabric(fabricOrOptions, maybeOptions, globalScope = globalThis) {
1228
1606
  };
1229
1607
  }
1230
1608
 
1231
- function fixPrototype(self, ctor) {
1232
- Object.setPrototypeOf(self, ctor.prototype);
1609
+ function isAnnotationLocked(annotation) {
1610
+ return annotation.annotationLocked === true;
1233
1611
  }
1234
- class ImageDecodeError extends Error {
1235
- constructor(message = 'Failed to decode image data URL.', originalError = null) {
1236
- super(message);
1237
- Object.defineProperty(this, "name", {
1238
- enumerable: true,
1239
- configurable: true,
1240
- writable: true,
1241
- value: 'ImageDecodeError'
1242
- });
1243
- Object.defineProperty(this, "originalError", {
1244
- enumerable: true,
1245
- configurable: true,
1246
- writable: true,
1247
- value: void 0
1248
- });
1249
- this.originalError = originalError;
1250
- fixPrototype(this, ImageDecodeError);
1251
- }
1612
+ function isAnnotationUnlocked(annotation) {
1613
+ return !isAnnotationLocked(annotation);
1252
1614
  }
1253
- class ImageLoadTimeoutError extends Error {
1254
- constructor(label, elapsedMs) {
1255
- super(`Image load timed out after ${elapsedMs}ms during ${label}`);
1256
- Object.defineProperty(this, "name", {
1257
- enumerable: true,
1258
- configurable: true,
1259
- writable: true,
1260
- value: 'ImageLoadTimeoutError'
1261
- });
1262
- Object.defineProperty(this, "label", {
1263
- enumerable: true,
1264
- configurable: true,
1265
- writable: true,
1266
- value: void 0
1267
- });
1268
- Object.defineProperty(this, "elapsedMs", {
1269
- enumerable: true,
1270
- configurable: true,
1271
- writable: true,
1272
- value: void 0
1273
- });
1274
- this.label = label;
1275
- this.elapsedMs = elapsedMs;
1276
- fixPrototype(this, ImageLoadTimeoutError);
1615
+
1616
+ function setObjectProps(object, props) {
1617
+ try {
1618
+ object.set(props);
1277
1619
  }
1278
- }
1279
- class DownsampleError extends Error {
1280
- constructor(message = 'Failed to obtain a 2D context for downsampling.', originalError = null) {
1281
- super(message);
1282
- Object.defineProperty(this, "name", {
1283
- enumerable: true,
1284
- configurable: true,
1285
- writable: true,
1286
- value: 'DownsampleError'
1287
- });
1288
- Object.defineProperty(this, "originalError", {
1289
- enumerable: true,
1290
- configurable: true,
1291
- writable: true,
1292
- value: void 0
1293
- });
1294
- this.originalError = originalError;
1295
- fixPrototype(this, DownsampleError);
1620
+ catch {
1621
+ Object.assign(object, props);
1296
1622
  }
1297
1623
  }
1298
- class MergeMasksError extends Error {
1299
- constructor(message = 'Failed to merge masks into the image.', originalError = null) {
1300
- super(message);
1301
- Object.defineProperty(this, "name", {
1302
- enumerable: true,
1303
- configurable: true,
1304
- writable: true,
1305
- value: 'MergeMasksError'
1306
- });
1307
- Object.defineProperty(this, "originalError", {
1308
- enumerable: true,
1309
- configurable: true,
1310
- writable: true,
1311
- value: void 0
1312
- });
1313
- this.originalError = originalError;
1314
- fixPrototype(this, MergeMasksError);
1315
- }
1624
+ function syncTextEditability(annotation, editable) {
1625
+ const textObject = annotation;
1626
+ textObject.editable = editable;
1316
1627
  }
1317
- class CropApplyError extends Error {
1318
- constructor(message = 'Failed to apply crop to the image.', originalError = null) {
1319
- super(message);
1320
- Object.defineProperty(this, "name", {
1321
- enumerable: true,
1322
- configurable: true,
1323
- writable: true,
1324
- value: 'CropApplyError'
1325
- });
1326
- Object.defineProperty(this, "originalError", {
1327
- enumerable: true,
1328
- configurable: true,
1329
- writable: true,
1330
- value: void 0
1628
+ function syncAnnotationRuntimeState(annotation) {
1629
+ var _a;
1630
+ const hidden = annotation.annotationHidden === true;
1631
+ const locked = isAnnotationLocked(annotation);
1632
+ setObjectProps(annotation, {
1633
+ visible: !hidden,
1634
+ selectable: locked ? false : true,
1635
+ evented: locked ? false : true,
1636
+ hasControls: !locked,
1637
+ lockMovementX: locked,
1638
+ lockMovementY: locked,
1639
+ lockScalingX: locked,
1640
+ lockScalingY: locked,
1641
+ lockRotation: locked,
1642
+ });
1643
+ if (!locked) {
1644
+ setObjectProps(annotation, {
1645
+ selectable: true,
1646
+ evented: true,
1647
+ hasControls: true,
1648
+ lockMovementX: false,
1649
+ lockMovementY: false,
1650
+ lockScalingX: false,
1651
+ lockScalingY: false,
1652
+ lockRotation: false,
1331
1653
  });
1332
- this.originalError = originalError;
1333
- fixPrototype(this, CropApplyError);
1334
1654
  }
1335
- }
1336
- class ExportNotReadyError extends Error {
1337
- constructor(operation = 'exportImageFile') {
1338
- super(`Cannot ${operation}: no image is loaded on the canvas.`);
1339
- Object.defineProperty(this, "name", {
1340
- enumerable: true,
1341
- configurable: true,
1342
- writable: true,
1343
- value: 'ExportNotReadyError'
1344
- });
1345
- Object.defineProperty(this, "operation", {
1346
- enumerable: true,
1347
- configurable: true,
1348
- writable: true,
1349
- value: void 0
1350
- });
1351
- this.operation = operation;
1352
- fixPrototype(this, ExportNotReadyError);
1655
+ if (isTextAnnotationObject(annotation)) {
1656
+ syncTextEditability(annotation, !locked);
1353
1657
  }
1658
+ (_a = annotation.setCoords) === null || _a === void 0 ? void 0 : _a.call(annotation);
1354
1659
  }
1355
- class ExportError extends Error {
1356
- constructor(message = 'Failed to export image.', originalError = null) {
1357
- super(message);
1358
- Object.defineProperty(this, "name", {
1359
- enumerable: true,
1360
- configurable: true,
1361
- writable: true,
1362
- value: 'ExportError'
1363
- });
1364
- Object.defineProperty(this, "originalError", {
1365
- enumerable: true,
1366
- configurable: true,
1367
- writable: true,
1368
- value: void 0
1369
- });
1370
- this.originalError = originalError;
1371
- fixPrototype(this, ExportError);
1372
- }
1660
+ function syncAnnotationRuntimeStates(annotations) {
1661
+ annotations.forEach(syncAnnotationRuntimeState);
1373
1662
  }
1374
1663
 
1375
- const SELECTED_STROKE = '#ff0000';
1376
- const SELECTED_STROKE_WIDTH = 1;
1377
- const HOVER_STROKE = '#ff5500';
1378
- const HOVER_STROKE_WIDTH = 2;
1379
- const HOVER_OPACITY_BUMP = 0.2;
1380
- const DEFAULT_STROKE_FALLBACK = '#ccc';
1381
- const DEFAULT_STROKE_WIDTH_FALLBACK = 1;
1382
- const DEFAULT_ALPHA_FALLBACK = 0.5;
1383
- function getMaskNormalStyle(mask) {
1384
- var _a;
1385
- const strokeWidth = Number(mask.originalStrokeWidth);
1386
- const opacity = Number(mask.originalAlpha);
1387
- return {
1388
- stroke: (_a = mask.originalStroke) !== null && _a !== void 0 ? _a : DEFAULT_STROKE_FALLBACK,
1389
- strokeWidth: Number.isFinite(strokeWidth) ? strokeWidth : DEFAULT_STROKE_WIDTH_FALLBACK,
1390
- opacity: Number.isFinite(opacity) ? opacity : DEFAULT_ALPHA_FALLBACK,
1391
- };
1392
- }
1393
- function getMaskHoverStyle(mask) {
1394
- const opacity = Number(mask.originalAlpha);
1395
- const baseAlpha = Number.isFinite(opacity) ? opacity : DEFAULT_ALPHA_FALLBACK;
1396
- return {
1397
- stroke: HOVER_STROKE,
1398
- strokeWidth: HOVER_STROKE_WIDTH,
1399
- opacity: Math.min(baseAlpha + HOVER_OPACITY_BUMP, 1),
1400
- };
1664
+ function isActiveSelectionObject$1(object) {
1665
+ if (!object)
1666
+ return false;
1667
+ const type = typeof object.type === 'string' ? object.type.toLowerCase() : '';
1668
+ if (type === 'activeselection')
1669
+ return true;
1670
+ const isType = object.isType;
1671
+ return (typeof isType === 'function' &&
1672
+ (isType.call(object, 'ActiveSelection') || isType.call(object, 'activeSelection')));
1401
1673
  }
1402
- function applyMaskSelectedStyle(mask) {
1403
- mask.set({ stroke: SELECTED_STROKE, strokeWidth: SELECTED_STROKE_WIDTH });
1674
+ function getActiveSelectionObjects(canvas) {
1675
+ const active = canvas.getActiveObject();
1676
+ if (!active)
1677
+ return [];
1678
+ if (!isActiveSelectionObject$1(active))
1679
+ return [active];
1680
+ const getObjects = active.getObjects;
1681
+ return typeof getObjects === 'function' ? getObjects.call(active) : [];
1682
+ }
1683
+ function getAnnotations(canvas) {
1684
+ return canvas.getObjects().filter(isAnnotationObject).slice();
1685
+ }
1686
+ function getSelectedAnnotations(canvas) {
1687
+ return getActiveSelectionObjects(canvas).filter(isAnnotationObject);
1688
+ }
1689
+ function snapshotAnnotation(annotation) {
1690
+ return JSON.stringify({
1691
+ text: annotation.text,
1692
+ fontSize: annotation.fontSize,
1693
+ fontFamily: annotation.fontFamily,
1694
+ fontWeight: annotation.fontWeight,
1695
+ fill: annotation.fill,
1696
+ backgroundColor: annotation.backgroundColor,
1697
+ textAlign: annotation.textAlign,
1698
+ width: annotation.width,
1699
+ stroke: annotation.stroke,
1700
+ strokeWidth: annotation.strokeWidth,
1701
+ opacity: annotation.opacity,
1702
+ visible: annotation.visible,
1703
+ selectable: annotation.selectable,
1704
+ evented: annotation.evented,
1705
+ annotationHidden: annotation.annotationHidden,
1706
+ annotationLocked: annotation.annotationLocked,
1707
+ });
1404
1708
  }
1405
- function applyMaskUnselectedStyle(mask) {
1406
- var _a;
1407
- const strokeWidth = Number(mask.originalStrokeWidth);
1408
- mask.set({
1409
- stroke: (_a = mask.originalStroke) !== null && _a !== void 0 ? _a : DEFAULT_STROKE_FALLBACK,
1410
- strokeWidth: Number.isFinite(strokeWidth) ? strokeWidth : DEFAULT_STROKE_WIDTH_FALLBACK,
1411
- });
1709
+ function setAnnotationProps(annotation, props) {
1710
+ try {
1711
+ annotation.set(props);
1712
+ }
1713
+ catch {
1714
+ Object.assign(annotation, props);
1715
+ }
1716
+ }
1717
+ function updateTextAnnotation(annotation, config) {
1718
+ const props = {};
1719
+ const raw = config;
1720
+ if (typeof raw.text === 'string')
1721
+ props.text = raw.text;
1722
+ if (typeof raw.fontSize === 'number' && Number.isFinite(raw.fontSize) && raw.fontSize > 0) {
1723
+ props.fontSize = raw.fontSize;
1724
+ }
1725
+ if (typeof raw.fontFamily === 'string')
1726
+ props.fontFamily = raw.fontFamily;
1727
+ if (typeof raw.fontWeight === 'string' || typeof raw.fontWeight === 'number') {
1728
+ props.fontWeight = raw.fontWeight;
1729
+ }
1730
+ if (typeof raw.fill === 'string')
1731
+ props.fill = raw.fill;
1732
+ if (typeof raw.backgroundColor === 'string')
1733
+ props.backgroundColor = raw.backgroundColor;
1734
+ if (raw.textAlign === 'left' ||
1735
+ raw.textAlign === 'center' ||
1736
+ raw.textAlign === 'right' ||
1737
+ raw.textAlign === 'justify') {
1738
+ props.textAlign = raw.textAlign;
1739
+ }
1740
+ if (typeof raw.width === 'number' && Number.isFinite(raw.width) && raw.width > 0) {
1741
+ props.width = raw.width;
1742
+ }
1743
+ if (Object.keys(props).length > 0)
1744
+ setAnnotationProps(annotation, props);
1745
+ }
1746
+ function updateDrawAnnotation(annotation, config) {
1747
+ const props = {};
1748
+ const raw = config;
1749
+ if (typeof raw.stroke === 'string')
1750
+ props.stroke = raw.stroke;
1751
+ if (typeof raw.strokeWidth === 'number' &&
1752
+ Number.isFinite(raw.strokeWidth) &&
1753
+ raw.strokeWidth > 0) {
1754
+ props.strokeWidth = raw.strokeWidth;
1755
+ }
1756
+ if (typeof raw.opacity === 'number' && Number.isFinite(raw.opacity)) {
1757
+ props.opacity = Math.max(0, Math.min(1, raw.opacity));
1758
+ }
1759
+ if (Object.keys(props).length > 0)
1760
+ setAnnotationProps(annotation, props);
1761
+ }
1762
+ function updateAnnotationObject(annotation, config) {
1763
+ const before = snapshotAnnotation(annotation);
1764
+ const raw = config;
1765
+ if (typeof raw.annotationHidden === 'boolean') {
1766
+ annotation.annotationHidden = raw.annotationHidden;
1767
+ }
1768
+ if (typeof raw.annotationLocked === 'boolean') {
1769
+ annotation.annotationLocked = raw.annotationLocked;
1770
+ }
1771
+ const lockedAfter = isAnnotationLocked(annotation);
1772
+ if (!lockedAfter) {
1773
+ if (typeof raw.selectable === 'boolean')
1774
+ annotation.selectable = raw.selectable;
1775
+ if (typeof raw.evented === 'boolean')
1776
+ annotation.evented = raw.evented;
1777
+ if (isTextAnnotationObject(annotation))
1778
+ updateTextAnnotation(annotation, config);
1779
+ if (isDrawAnnotationObject(annotation))
1780
+ updateDrawAnnotation(annotation, config);
1781
+ }
1782
+ syncAnnotationRuntimeState(annotation);
1783
+ return snapshotAnnotation(annotation) !== before;
1784
+ }
1785
+ function updateAnnotation(context, annotationId, config) {
1786
+ const target = getAnnotations(context.canvas).find((annotation) => annotation.annotationId === annotationId);
1787
+ if (!target)
1788
+ return false;
1789
+ const changed = updateAnnotationObject(target, config);
1790
+ if (!changed)
1791
+ return false;
1792
+ context.canvas.requestRenderAll();
1793
+ context.saveCanvasState();
1794
+ context.updateUi();
1795
+ return true;
1412
1796
  }
1413
- function attachMaskHoverHandlers(mask) {
1414
- const tagged = mask;
1415
- const mouseover = () => {
1416
- var _a;
1417
- tagged.set(getMaskHoverStyle(tagged));
1418
- (_a = tagged.canvas) === null || _a === void 0 ? void 0 : _a.requestRenderAll();
1419
- };
1420
- const mouseout = () => {
1421
- var _a;
1422
- tagged.set(getMaskNormalStyle(tagged));
1423
- (_a = tagged.canvas) === null || _a === void 0 ? void 0 : _a.requestRenderAll();
1424
- };
1425
- tagged.on('mouseover', mouseover);
1426
- tagged.on('mouseout', mouseout);
1427
- tagged.imageEditorMaskHandlers = { mouseover, mouseout };
1797
+ function updateSelectedAnnotation(context, config) {
1798
+ const selectedAnnotations = getSelectedAnnotations(context.canvas);
1799
+ if (selectedAnnotations.length === 0)
1800
+ return false;
1801
+ const changed = selectedAnnotations
1802
+ .map((annotation) => updateAnnotationObject(annotation, config))
1803
+ .some(Boolean);
1804
+ if (!changed)
1805
+ return false;
1806
+ context.canvas.requestRenderAll();
1807
+ context.saveCanvasState();
1808
+ context.updateUi();
1809
+ return true;
1428
1810
  }
1429
- function reattachMaskHoverHandlers(mask) {
1430
- var _a;
1431
- const tagged = mask;
1432
- if (tagged.imageEditorMaskHandlers) {
1433
- try {
1434
- tagged.off('mouseover', tagged.imageEditorMaskHandlers.mouseover);
1435
- tagged.off('mouseout', tagged.imageEditorMaskHandlers.mouseout);
1436
- }
1437
- catch {
1438
- }
1439
- delete tagged.imageEditorMaskHandlers;
1440
- }
1441
- const patch = {};
1442
- if (!Number.isFinite(Number(tagged.originalAlpha))) {
1443
- const opacity = Number(tagged.opacity);
1444
- patch.originalAlpha = Number.isFinite(opacity) ? opacity : DEFAULT_ALPHA_FALLBACK;
1445
- }
1446
- if (tagged.originalStroke == null) {
1447
- patch.originalStroke = (_a = tagged.stroke) !== null && _a !== void 0 ? _a : DEFAULT_STROKE_FALLBACK;
1448
- }
1449
- if (!Number.isFinite(Number(tagged.originalStrokeWidth))) {
1450
- const sw = Number(tagged.strokeWidth);
1451
- patch.originalStrokeWidth = Number.isFinite(sw) ? sw : DEFAULT_STROKE_WIDTH_FALLBACK;
1811
+ function removeAnnotationObjects(context, objects, options = {}) {
1812
+ const force = options.force === true;
1813
+ const removable = objects.filter((annotation) => force || isAnnotationUnlocked(annotation));
1814
+ if (removable.length === 0)
1815
+ return 0;
1816
+ for (const annotation of removable) {
1817
+ context.canvas.remove(annotation);
1452
1818
  }
1453
- if (Object.keys(patch).length > 0)
1454
- tagged.set(patch);
1455
- attachMaskHoverHandlers(tagged);
1819
+ context.canvas.discardActiveObject();
1820
+ context.canvas.renderAll();
1821
+ if (options.saveHistory !== false)
1822
+ context.saveCanvasState();
1823
+ context.updateUi();
1824
+ return removable.length;
1456
1825
  }
1457
- function detachMaskHoverHandlers(mask) {
1458
- const tagged = mask;
1459
- if (!tagged.imageEditorMaskHandlers)
1826
+ function removeSelectedAnnotation(context) {
1827
+ return removeAnnotationObjects(context, getSelectedAnnotations(context.canvas));
1828
+ }
1829
+ function removeAllAnnotations(context, options = {}) {
1830
+ return removeAnnotationObjects(context, getAnnotations(context.canvas), options);
1831
+ }
1832
+ function getAnnotationListDocument(context) {
1833
+ var _a, _b, _c, _d, _e;
1834
+ const canvasLike = context.canvas;
1835
+ 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);
1836
+ }
1837
+ function renderAnnotationList(context) {
1838
+ const listId = context.getListElementId();
1839
+ if (!listId)
1460
1840
  return;
1461
- try {
1462
- tagged.off('mouseover', tagged.imageEditorMaskHandlers.mouseover);
1463
- tagged.off('mouseout', tagged.imageEditorMaskHandlers.mouseout);
1464
- }
1465
- catch {
1466
- }
1467
- delete tagged.imageEditorMaskHandlers;
1841
+ const ownerDocument = getAnnotationListDocument(context);
1842
+ const listEl = ownerDocument.getElementById(listId);
1843
+ if (!listEl || !context.canvas)
1844
+ return;
1845
+ listEl.innerHTML = '';
1846
+ const canvas = context.canvas;
1847
+ getAnnotations(canvas).forEach((annotation) => {
1848
+ const item = ownerDocument.createElement('li');
1849
+ item.className = 'list-group-item annotation-item';
1850
+ item.textContent = annotation.annotationName;
1851
+ item.dataset.annotationId = String(annotation.annotationId);
1852
+ item.onclick = () => {
1853
+ const id = Number(item.dataset.annotationId);
1854
+ if (!Number.isFinite(id))
1855
+ return;
1856
+ const target = getAnnotations(canvas).find((candidate) => candidate.annotationId === id);
1857
+ if (!target)
1858
+ return;
1859
+ canvas.setActiveObject(target);
1860
+ context.onAnnotationSelected(target);
1861
+ };
1862
+ listEl.appendChild(item);
1863
+ });
1468
1864
  }
1469
- function captureMaskStyleBackup(mask) {
1470
- var _a, _b, _c, _d, _e, _f, _g;
1471
- return {
1472
- object: mask,
1473
- opacity: (_a = mask.opacity) !== null && _a !== void 0 ? _a : 1,
1474
- fill: ((_b = mask.fill) !== null && _b !== void 0 ? _b : null),
1475
- strokeWidth: (_c = mask.strokeWidth) !== null && _c !== void 0 ? _c : 0,
1476
- stroke: ((_d = mask.stroke) !== null && _d !== void 0 ? _d : null),
1477
- selectable: (_e = mask.selectable) !== null && _e !== void 0 ? _e : true,
1478
- evented: (_f = mask.evented) !== null && _f !== void 0 ? _f : true,
1479
- lockRotation: (_g = mask.lockRotation) !== null && _g !== void 0 ? _g : false,
1480
- };
1865
+ function updateAnnotationListSelection(context, selectedAnnotation) {
1866
+ const listId = context.getListElementId();
1867
+ if (!listId)
1868
+ return;
1869
+ const listEl = getAnnotationListDocument(context).getElementById(listId);
1870
+ if (!listEl)
1871
+ return;
1872
+ const selectedId = selectedAnnotation ? String(selectedAnnotation.annotationId) : null;
1873
+ listEl.querySelectorAll('.annotation-item').forEach((item) => {
1874
+ item.classList.toggle('active', selectedId !== null && item.dataset.annotationId === selectedId);
1875
+ });
1481
1876
  }
1482
- function restoreMaskStyleBackup(backup) {
1877
+
1878
+ function isLegacySessionObject(object) {
1879
+ const candidate = object;
1880
+ return (candidate.isCropRect === true ||
1881
+ candidate.maskLabel === true ||
1882
+ candidate.isMosaicPreview === true);
1883
+ }
1884
+ function moveObjectTo(canvas, object, index) {
1885
+ const canvasWithLayerApi = canvas;
1886
+ if (typeof canvasWithLayerApi.moveObjectTo === 'function') {
1887
+ canvasWithLayerApi.moveObjectTo(object, index);
1888
+ return;
1889
+ }
1483
1890
  try {
1484
- backup.object.set({
1485
- opacity: backup.opacity,
1486
- fill: backup.fill,
1487
- strokeWidth: backup.strokeWidth,
1488
- stroke: backup.stroke,
1489
- selectable: backup.selectable,
1490
- evented: backup.evented,
1491
- lockRotation: backup.lockRotation,
1492
- });
1493
- if (typeof backup.object.setCoords === 'function') {
1494
- backup.object.setCoords();
1495
- }
1891
+ canvas.remove(object);
1892
+ canvas.insertAt(index, object);
1496
1893
  }
1497
1894
  catch {
1895
+ canvas.add(object);
1498
1896
  }
1499
1897
  }
1500
- async function withMaskStyleBackup(context, mutator, callback) {
1501
- if (!context.canvas)
1502
- return await callback();
1503
- const masks = context.canvas.getObjects().filter(isMaskObject);
1504
- const backups = masks.map(captureMaskStyleBackup);
1505
- try {
1506
- masks.forEach((mask, index) => mutator(mask, index));
1507
- return await callback();
1508
- }
1509
- finally {
1510
- for (const backup of backups)
1511
- restoreMaskStyleBackup(backup);
1898
+ function ensureOnCanvas(canvas, object) {
1899
+ if (!canvas.getObjects().includes(object)) {
1900
+ canvas.add(object);
1512
1901
  }
1513
1902
  }
1514
- function applyCropHideMaskStyle(mask) {
1515
- try {
1516
- mask.set({ opacity: 0, evented: false, selectable: false });
1517
- }
1518
- catch {
1903
+ function withoutObject(canvas, object) {
1904
+ return canvas.getObjects().filter((candidate) => candidate !== object);
1905
+ }
1906
+ function findFirstSessionIndex(objects) {
1907
+ return objects.findIndex((object) => isSessionObject(object) || isLegacySessionObject(object));
1908
+ }
1909
+ function getOrderedGroups(canvas) {
1910
+ const baseImages = [];
1911
+ const overlays = [];
1912
+ const sessions = [];
1913
+ const others = [];
1914
+ for (const object of canvas.getObjects()) {
1915
+ if (isBaseImageObject(object)) {
1916
+ baseImages.push(object);
1917
+ }
1918
+ else if (isEditableOverlayObject(object)) {
1919
+ overlays.push(object);
1920
+ }
1921
+ else if (isSessionObject(object) || isLegacySessionObject(object)) {
1922
+ sessions.push(object);
1923
+ }
1924
+ else {
1925
+ others.push(object);
1926
+ }
1519
1927
  }
1928
+ return { baseImages, overlays, sessions, others };
1929
+ }
1930
+ function normalizeLayerOrder(canvas) {
1931
+ const groups = getOrderedGroups(canvas);
1932
+ const ordered = [
1933
+ ...groups.baseImages,
1934
+ ...groups.others,
1935
+ ...groups.overlays,
1936
+ ...groups.sessions,
1937
+ ];
1938
+ ordered.forEach((object, index) => {
1939
+ moveObjectTo(canvas, object, index);
1940
+ });
1941
+ }
1942
+ function placeMaskObject(canvas, mask) {
1943
+ ensureOnCanvas(canvas, mask);
1944
+ const objects = withoutObject(canvas, mask);
1945
+ const firstSessionIndex = findFirstSessionIndex(objects);
1946
+ moveObjectTo(canvas, mask, firstSessionIndex === -1 ? objects.length : firstSessionIndex);
1947
+ }
1948
+ function placeAnnotationObject(canvas, annotation) {
1949
+ ensureOnCanvas(canvas, annotation);
1950
+ const objects = withoutObject(canvas, annotation);
1951
+ const firstSessionIndex = findFirstSessionIndex(objects);
1952
+ moveObjectTo(canvas, annotation, firstSessionIndex === -1 ? objects.length : firstSessionIndex);
1953
+ }
1954
+ function getEditableOverlayRange(canvas) {
1955
+ const objects = canvas.getObjects();
1956
+ const overlayIndexes = objects
1957
+ .map((object, index) => ({ object, index }))
1958
+ .filter(({ object }) => isEditableOverlayObject(object));
1959
+ if (overlayIndexes.length === 0)
1960
+ return { start: -1, end: -1, overlays: [] };
1961
+ return {
1962
+ start: overlayIndexes[0].index,
1963
+ end: overlayIndexes[overlayIndexes.length - 1].index,
1964
+ overlays: overlayIndexes.map(({ object }) => object),
1965
+ };
1520
1966
  }
1521
1967
 
1522
1968
  function hasMeaningfulCanvasRegion(rect, canvasWidth, canvasHeight) {
@@ -1574,37 +2020,734 @@ function getClampedCanvasRegion(rect, canvasWidth, canvasHeight, options = {}) {
1574
2020
  height: Math.max(1, bottom - top),
1575
2021
  };
1576
2022
  }
1577
- function hasFractionalCanvasEdge(value) {
1578
- const numericValue = Number(value);
1579
- if (!Number.isFinite(numericValue))
1580
- return false;
1581
- return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
2023
+ function hasFractionalCanvasEdge(value) {
2024
+ const numericValue = Number(value);
2025
+ if (!Number.isFinite(numericValue))
2026
+ return false;
2027
+ return Math.abs(numericValue - Math.round(numericValue)) > 0.01;
2028
+ }
2029
+ function getPartialExportEdges(bounds, angle = 0) {
2030
+ if (!bounds)
2031
+ return null;
2032
+ const normalizedAngle = Math.abs((Number(angle) || 0) % 90);
2033
+ const isAxisAligned = normalizedAngle < 0.01 || Math.abs(normalizedAngle - 90) < 0.01;
2034
+ if (!isAxisAligned)
2035
+ return null;
2036
+ const left = Number(bounds.left) || 0;
2037
+ const top = Number(bounds.top) || 0;
2038
+ return {
2039
+ left: hasFractionalCanvasEdge(left),
2040
+ top: hasFractionalCanvasEdge(top),
2041
+ right: hasFractionalCanvasEdge(left + (Number(bounds.width) || 0)),
2042
+ bottom: hasFractionalCanvasEdge(top + (Number(bounds.height) || 0)),
2043
+ };
2044
+ }
2045
+ function getObjectBBox(object) {
2046
+ object.setCoords();
2047
+ const boundingRect = object.getBoundingRect();
2048
+ return {
2049
+ left: boundingRect.left,
2050
+ top: boundingRect.top,
2051
+ width: boundingRect.width,
2052
+ height: boundingRect.height,
2053
+ };
2054
+ }
2055
+
2056
+ function resolveNumeric(val, axis, fallback, canvas, options) {
2057
+ if (typeof val === 'number') {
2058
+ return val;
2059
+ }
2060
+ if (typeof val === 'function') {
2061
+ return val(canvas, options);
2062
+ }
2063
+ if (typeof val === 'string' && val.endsWith('%')) {
2064
+ const pct = parseFloat(val);
2065
+ if (!Number.isFinite(pct)) {
2066
+ return fallback;
2067
+ }
2068
+ const dim = axis === 'x' ? canvas.getWidth() : canvas.getHeight();
2069
+ return Math.floor(dim * (pct / 100));
2070
+ }
2071
+ return fallback;
2072
+ }
2073
+ function coercePoint(pt) {
2074
+ if (Array.isArray(pt)) {
2075
+ return { x: Number(pt[0]), y: Number(pt[1]) };
2076
+ }
2077
+ return { x: Number(pt.x), y: Number(pt.y) };
2078
+ }
2079
+
2080
+ function isFinitePoint(value) {
2081
+ const point = value;
2082
+ return (!!point &&
2083
+ typeof point.x === 'number' &&
2084
+ Number.isFinite(point.x) &&
2085
+ typeof point.y === 'number' &&
2086
+ Number.isFinite(point.y));
2087
+ }
2088
+ function getPointerFromFabricEvent(canvas, event) {
2089
+ const fabricEvent = event && typeof event === 'object'
2090
+ ? event
2091
+ : null;
2092
+ if (!fabricEvent)
2093
+ return null;
2094
+ if (isFinitePoint(fabricEvent.scenePoint))
2095
+ return { ...fabricEvent.scenePoint };
2096
+ if (isFinitePoint(fabricEvent.pointer))
2097
+ return { ...fabricEvent.pointer };
2098
+ if (isFinitePoint(fabricEvent.absolutePointer))
2099
+ return { ...fabricEvent.absolutePointer };
2100
+ if (fabricEvent.e && typeof canvas.getPointer === 'function') {
2101
+ const pointer = canvas.getPointer(fabricEvent.e);
2102
+ if (isFinitePoint(pointer))
2103
+ return { ...pointer };
2104
+ }
2105
+ return null;
2106
+ }
2107
+
2108
+ function resolveDefaultTextPosition(context) {
2109
+ const image = context.getOriginalImage();
2110
+ if (image) {
2111
+ const bounds = getObjectBBox(image);
2112
+ return { left: Math.round(bounds.left + 10), top: Math.round(bounds.top + 10) };
2113
+ }
2114
+ return { left: 10, top: 10 };
2115
+ }
2116
+ function resolveTextCreationConfig(context, config) {
2117
+ var _a, _b;
2118
+ const base = mergeTextAnnotationConfigPatch(context.getTextConfig(), config);
2119
+ const fallback = resolveDefaultTextPosition(context);
2120
+ const leftInput = (_a = config.left) !== null && _a !== void 0 ? _a : base.left;
2121
+ const topInput = (_b = config.top) !== null && _b !== void 0 ? _b : base.top;
2122
+ return {
2123
+ ...base,
2124
+ left: resolveNumeric(leftInput, 'x', fallback.left, context.canvas, context.options),
2125
+ top: resolveNumeric(topInput, 'y', fallback.top, context.canvas, context.options),
2126
+ };
2127
+ }
2128
+ function nextAnnotationMeta(context, config) {
2129
+ const annotationId = context.getAnnotationCounter() + 1;
2130
+ context.setAnnotationCounter(annotationId);
2131
+ return {
2132
+ annotationId,
2133
+ annotationName: `${context.options.textAnnotationName}${annotationId}`,
2134
+ annotationHidden: config.annotationHidden,
2135
+ annotationLocked: config.annotationLocked,
2136
+ };
2137
+ }
2138
+ function attachTextEditingHandlers(context, annotation) {
2139
+ const textObject = annotation;
2140
+ if (textObject.imageEditorTextEditingHandlers) {
2141
+ try {
2142
+ textObject.off('editing:entered', textObject.imageEditorTextEditingHandlers.entered);
2143
+ textObject.off('editing:exited', textObject.imageEditorTextEditingHandlers.exited);
2144
+ }
2145
+ catch {
2146
+ }
2147
+ }
2148
+ const entered = () => {
2149
+ var _a;
2150
+ textObject.imageEditorTextEditingInitialText = String((_a = textObject.text) !== null && _a !== void 0 ? _a : '');
2151
+ textObject.imageEditorTextEditingCancel = false;
2152
+ };
2153
+ const exited = () => {
2154
+ var _a;
2155
+ const initial = textObject.imageEditorTextEditingInitialText;
2156
+ const finalText = String((_a = textObject.text) !== null && _a !== void 0 ? _a : '');
2157
+ const cancel = textObject.imageEditorTextEditingCancel === true;
2158
+ if (cancel && initial !== undefined) {
2159
+ textObject.set({ text: initial });
2160
+ }
2161
+ delete textObject.imageEditorTextEditingInitialText;
2162
+ delete textObject.imageEditorTextEditingCancel;
2163
+ if (!cancel && initial !== undefined && initial !== finalText) {
2164
+ context.saveCanvasState();
2165
+ const callbackContext = context.buildCallbackContext('createTextAnnotation');
2166
+ context.emitAnnotationsChanged(callbackContext);
2167
+ context.emitImageChanged(callbackContext);
2168
+ }
2169
+ };
2170
+ textObject.on('editing:entered', entered);
2171
+ textObject.on('editing:exited', exited);
2172
+ textObject.imageEditorTextEditingHandlers = { entered, exited };
2173
+ }
2174
+ function selectAllText(annotation) {
2175
+ var _a;
2176
+ const textObject = annotation;
2177
+ const textLength = String((_a = textObject.text) !== null && _a !== void 0 ? _a : '').length;
2178
+ if (textLength <= 0)
2179
+ return;
2180
+ if (typeof textObject.selectAll === 'function') {
2181
+ textObject.selectAll();
2182
+ return;
2183
+ }
2184
+ if (typeof textObject.setSelectionStart === 'function' &&
2185
+ typeof textObject.setSelectionEnd === 'function') {
2186
+ textObject.setSelectionStart(0);
2187
+ textObject.setSelectionEnd(textLength);
2188
+ return;
2189
+ }
2190
+ textObject.selectionStart = 0;
2191
+ textObject.selectionEnd = textLength;
2192
+ }
2193
+ function createTextAnnotation(context, config = {}) {
2194
+ var _a, _b;
2195
+ if (!context.isImageLoaded())
2196
+ return null;
2197
+ const resolved = resolveTextCreationConfig(context, config);
2198
+ const textbox = new context.fabric.Textbox(resolved.text, {
2199
+ left: resolved.left,
2200
+ top: resolved.top,
2201
+ width: resolved.width,
2202
+ fontSize: resolved.fontSize,
2203
+ fontFamily: resolved.fontFamily,
2204
+ fontWeight: resolved.fontWeight,
2205
+ fill: resolved.fill,
2206
+ backgroundColor: resolved.backgroundColor,
2207
+ textAlign: resolved.textAlign,
2208
+ angle: resolved.angle,
2209
+ selectable: resolved.selectable,
2210
+ evented: resolved.evented,
2211
+ editable: resolved.editable,
2212
+ originX: 'left',
2213
+ originY: 'top',
2214
+ ...resolved.styles,
2215
+ });
2216
+ const meta = nextAnnotationMeta(context, resolved);
2217
+ const annotation = markAnnotationObject(textbox, {
2218
+ annotationId: meta.annotationId,
2219
+ annotationType: 'text',
2220
+ annotationName: meta.annotationName,
2221
+ annotationHidden: meta.annotationHidden,
2222
+ annotationLocked: meta.annotationLocked,
2223
+ });
2224
+ syncAnnotationRuntimeState(annotation);
2225
+ attachTextEditingHandlers(context, annotation);
2226
+ placeAnnotationObject(context.canvas, annotation);
2227
+ if (resolved.selectable !== false && isAnnotationUnlocked(annotation)) {
2228
+ context.canvas.setActiveObject(annotation);
2229
+ }
2230
+ context.canvas.renderAll();
2231
+ context.updateAnnotationList();
2232
+ context.saveCanvasState();
2233
+ const callbackContext = context.buildCallbackContext('createTextAnnotation');
2234
+ context.emitAnnotationsChanged(callbackContext);
2235
+ context.emitImageChanged(callbackContext);
2236
+ if (resolved.enterEditing && isAnnotationUnlocked(annotation)) {
2237
+ (_b = (_a = annotation).enterEditing) === null || _b === void 0 ? void 0 : _b.call(_a);
2238
+ selectAllText(annotation);
2239
+ }
2240
+ return annotation;
2241
+ }
2242
+ function handleTextModePointer(context, event) {
2243
+ var _a, _b;
2244
+ const fabricEvent = event;
2245
+ const target = fabricEvent.target;
2246
+ if (target) {
2247
+ if (isTextAnnotationObject(target) && isAnnotationUnlocked(target)) {
2248
+ context.canvas.setActiveObject(target);
2249
+ (_b = (_a = target).enterEditing) === null || _b === void 0 ? void 0 : _b.call(_a);
2250
+ }
2251
+ else if (isEditableOverlayObject(target)) {
2252
+ context.canvas.setActiveObject(target);
2253
+ }
2254
+ return;
2255
+ }
2256
+ const pointer = getPointerFromFabricEvent(context.canvas, event);
2257
+ if (!pointer)
2258
+ return;
2259
+ createTextAnnotation(context, {
2260
+ left: pointer.x,
2261
+ top: pointer.y,
2262
+ });
2263
+ }
2264
+ function enterTextMode(context) {
2265
+ if (context.getTextSession())
2266
+ return;
2267
+ if (!context.isImageLoaded())
2268
+ return;
2269
+ const { canvas } = context;
2270
+ const previousCanvasSelection = !!canvas.selection;
2271
+ const previousDefaultCursor = canvas.defaultCursor;
2272
+ canvas.selection = true;
2273
+ canvas.defaultCursor = 'text';
2274
+ const callback = (event) => handleTextModePointer(context, event);
2275
+ canvas.on('mouse:down', callback);
2276
+ const session = {
2277
+ mode: 'text',
2278
+ previousCanvasSelection,
2279
+ previousDefaultCursor,
2280
+ handlers: [{ eventName: 'mouse:down', callback }],
2281
+ dispose: () => {
2282
+ try {
2283
+ canvas.off('mouse:down', callback);
2284
+ }
2285
+ catch {
2286
+ }
2287
+ canvas.selection = previousCanvasSelection;
2288
+ canvas.defaultCursor = previousDefaultCursor !== null && previousDefaultCursor !== void 0 ? previousDefaultCursor : 'default';
2289
+ },
2290
+ };
2291
+ const preview = new context.fabric.Rect({
2292
+ left: -1,
2293
+ top: -1,
2294
+ width: 1,
2295
+ height: 1,
2296
+ selectable: false,
2297
+ evented: false,
2298
+ visible: false,
2299
+ excludeFromExport: true,
2300
+ });
2301
+ markSessionObject(preview, 'textPreview');
2302
+ context.setTextSession(session);
2303
+ context.updateUi();
2304
+ }
2305
+ function exitTextMode(context) {
2306
+ const session = context.getTextSession();
2307
+ if (!session)
2308
+ return;
2309
+ session.dispose();
2310
+ context.setTextSession(null);
2311
+ context.canvas.requestRenderAll();
2312
+ context.updateUi();
2313
+ }
2314
+ function finalizeActiveTextEditing(context, options) {
2315
+ var _a;
2316
+ const active = context.canvas.getActiveObject();
2317
+ if (!active || !isTextAnnotationObject(active))
2318
+ return;
2319
+ const textObject = active;
2320
+ if (textObject.isEditing !== true)
2321
+ return;
2322
+ textObject.imageEditorTextEditingCancel = !options.commit;
2323
+ (_a = textObject.exitEditing) === null || _a === void 0 ? void 0 : _a.call(textObject);
2324
+ context.canvas.requestRenderAll();
2325
+ }
2326
+ function attachTextEditingHandlersToAnnotations(context, annotations) {
2327
+ annotations.filter(isTextAnnotationObject).forEach((annotation) => {
2328
+ attachTextEditingHandlers(context, annotation);
2329
+ });
2330
+ }
2331
+
2332
+ function colorWithOpacity(color, opacity) {
2333
+ const alpha = Math.max(0, Math.min(1, opacity));
2334
+ if (alpha >= 1)
2335
+ return color;
2336
+ if (/^#([0-9a-f]{6})$/i.test(color)) {
2337
+ const hex = color.slice(1);
2338
+ const r = Number.parseInt(hex.slice(0, 2), 16);
2339
+ const g = Number.parseInt(hex.slice(2, 4), 16);
2340
+ const b = Number.parseInt(hex.slice(4, 6), 16);
2341
+ return `rgba(${r},${g},${b},${alpha})`;
2342
+ }
2343
+ return color;
2344
+ }
2345
+ function configureBrush(context) {
2346
+ const config = context.getDrawConfig();
2347
+ const canvasWithBrush = context.canvas;
2348
+ canvasWithBrush.freeDrawingBrush = new context.fabric.PencilBrush(context.canvas);
2349
+ canvasWithBrush.freeDrawingBrush.width = config.brushSize;
2350
+ canvasWithBrush.freeDrawingBrush.color = colorWithOpacity(config.color, config.opacity);
2351
+ canvasWithBrush.freeDrawingBrush.strokeLineCap = config.lineCap;
2352
+ canvasWithBrush.freeDrawingBrush.strokeLineJoin = config.lineJoin;
2353
+ }
2354
+ function markPathAsDrawAnnotation(context, path) {
2355
+ const config = context.getDrawConfig();
2356
+ const annotationId = context.getAnnotationCounter() + 1;
2357
+ context.setAnnotationCounter(annotationId);
2358
+ path.set({
2359
+ selectable: config.selectable,
2360
+ evented: config.evented,
2361
+ opacity: config.opacity,
2362
+ stroke: config.color,
2363
+ strokeWidth: config.brushSize,
2364
+ });
2365
+ const annotation = markAnnotationObject(path, {
2366
+ annotationId,
2367
+ annotationType: 'draw',
2368
+ annotationName: `${context.options.drawAnnotationName}${annotationId}`,
2369
+ annotationHidden: config.annotationHidden,
2370
+ annotationLocked: config.annotationLocked,
2371
+ });
2372
+ syncAnnotationRuntimeState(annotation);
2373
+ return annotation;
2374
+ }
2375
+ function handlePathCreated(context, event) {
2376
+ const path = event.path;
2377
+ if (!path)
2378
+ return;
2379
+ const annotation = markPathAsDrawAnnotation(context, path);
2380
+ placeAnnotationObject(context.canvas, annotation);
2381
+ context.canvas.setActiveObject(annotation);
2382
+ context.canvas.renderAll();
2383
+ context.updateAnnotationList();
2384
+ context.saveCanvasState();
2385
+ const callbackContext = context.buildCallbackContext('enterDrawMode');
2386
+ context.emitAnnotationsChanged(callbackContext);
2387
+ context.emitImageChanged(callbackContext);
2388
+ }
2389
+ function enterDrawMode(context) {
2390
+ if (context.getDrawSession())
2391
+ return;
2392
+ if (!context.isImageLoaded())
2393
+ return;
2394
+ const { canvas } = context;
2395
+ const canvasWithDrawing = canvas;
2396
+ const previousDrawingMode = !!canvasWithDrawing.isDrawingMode;
2397
+ const previousBrush = canvasWithDrawing.freeDrawingBrush;
2398
+ const previousCanvasSelection = !!canvas.selection;
2399
+ const previousDefaultCursor = canvas.defaultCursor;
2400
+ canvas.selection = false;
2401
+ canvas.defaultCursor = 'crosshair';
2402
+ canvasWithDrawing.isDrawingMode = true;
2403
+ configureBrush(context);
2404
+ const callback = (event) => handlePathCreated(context, event);
2405
+ canvas.on('path:created', callback);
2406
+ const session = {
2407
+ mode: 'draw',
2408
+ previousDrawingMode,
2409
+ previousBrush,
2410
+ previousCanvasSelection,
2411
+ previousDefaultCursor,
2412
+ handlers: [{ eventName: 'path:created', callback }],
2413
+ dispose: () => {
2414
+ try {
2415
+ canvas.off('path:created', callback);
2416
+ }
2417
+ catch {
2418
+ }
2419
+ canvasWithDrawing.isDrawingMode = previousDrawingMode;
2420
+ canvasWithDrawing.freeDrawingBrush = previousBrush;
2421
+ canvas.selection = previousCanvasSelection;
2422
+ canvas.defaultCursor = previousDefaultCursor !== null && previousDefaultCursor !== void 0 ? previousDefaultCursor : 'default';
2423
+ },
2424
+ };
2425
+ context.setDrawSession(session);
2426
+ context.updateUi();
2427
+ }
2428
+ function exitDrawMode(context) {
2429
+ const session = context.getDrawSession();
2430
+ if (!session)
2431
+ return;
2432
+ session.dispose();
2433
+ context.setDrawSession(null);
2434
+ context.canvas.requestRenderAll();
2435
+ context.updateUi();
2436
+ }
2437
+ function updateDrawBrush(context) {
2438
+ if (!context.getDrawSession())
2439
+ return;
2440
+ configureBrush(context);
2441
+ }
2442
+
2443
+ function fixPrototype(self, ctor) {
2444
+ Object.setPrototypeOf(self, ctor.prototype);
2445
+ }
2446
+ class ImageDecodeError extends Error {
2447
+ constructor(message = 'Failed to decode image data URL.', originalError = null) {
2448
+ super(message);
2449
+ Object.defineProperty(this, "name", {
2450
+ enumerable: true,
2451
+ configurable: true,
2452
+ writable: true,
2453
+ value: 'ImageDecodeError'
2454
+ });
2455
+ Object.defineProperty(this, "originalError", {
2456
+ enumerable: true,
2457
+ configurable: true,
2458
+ writable: true,
2459
+ value: void 0
2460
+ });
2461
+ this.originalError = originalError;
2462
+ fixPrototype(this, ImageDecodeError);
2463
+ }
2464
+ }
2465
+ class ImageLoadTimeoutError extends Error {
2466
+ constructor(label, elapsedMs) {
2467
+ super(`Image load timed out after ${elapsedMs}ms during ${label}`);
2468
+ Object.defineProperty(this, "name", {
2469
+ enumerable: true,
2470
+ configurable: true,
2471
+ writable: true,
2472
+ value: 'ImageLoadTimeoutError'
2473
+ });
2474
+ Object.defineProperty(this, "label", {
2475
+ enumerable: true,
2476
+ configurable: true,
2477
+ writable: true,
2478
+ value: void 0
2479
+ });
2480
+ Object.defineProperty(this, "elapsedMs", {
2481
+ enumerable: true,
2482
+ configurable: true,
2483
+ writable: true,
2484
+ value: void 0
2485
+ });
2486
+ this.label = label;
2487
+ this.elapsedMs = elapsedMs;
2488
+ fixPrototype(this, ImageLoadTimeoutError);
2489
+ }
2490
+ }
2491
+ class DownsampleError extends Error {
2492
+ constructor(message = 'Failed to obtain a 2D context for downsampling.', originalError = null) {
2493
+ super(message);
2494
+ Object.defineProperty(this, "name", {
2495
+ enumerable: true,
2496
+ configurable: true,
2497
+ writable: true,
2498
+ value: 'DownsampleError'
2499
+ });
2500
+ Object.defineProperty(this, "originalError", {
2501
+ enumerable: true,
2502
+ configurable: true,
2503
+ writable: true,
2504
+ value: void 0
2505
+ });
2506
+ this.originalError = originalError;
2507
+ fixPrototype(this, DownsampleError);
2508
+ }
2509
+ }
2510
+ class MergeMasksError extends Error {
2511
+ constructor(message = 'Failed to merge masks into the image.', originalError = null) {
2512
+ super(message);
2513
+ Object.defineProperty(this, "name", {
2514
+ enumerable: true,
2515
+ configurable: true,
2516
+ writable: true,
2517
+ value: 'MergeMasksError'
2518
+ });
2519
+ Object.defineProperty(this, "originalError", {
2520
+ enumerable: true,
2521
+ configurable: true,
2522
+ writable: true,
2523
+ value: void 0
2524
+ });
2525
+ this.originalError = originalError;
2526
+ fixPrototype(this, MergeMasksError);
2527
+ }
2528
+ }
2529
+ class MergeAnnotationsError extends Error {
2530
+ constructor(message = 'Failed to merge annotations into the image.', originalError = null) {
2531
+ super(message);
2532
+ Object.defineProperty(this, "name", {
2533
+ enumerable: true,
2534
+ configurable: true,
2535
+ writable: true,
2536
+ value: 'MergeAnnotationsError'
2537
+ });
2538
+ Object.defineProperty(this, "originalError", {
2539
+ enumerable: true,
2540
+ configurable: true,
2541
+ writable: true,
2542
+ value: void 0
2543
+ });
2544
+ this.originalError = originalError;
2545
+ fixPrototype(this, MergeAnnotationsError);
2546
+ }
2547
+ }
2548
+ class CropApplyError extends Error {
2549
+ constructor(message = 'Failed to apply crop to the image.', originalError = null) {
2550
+ super(message);
2551
+ Object.defineProperty(this, "name", {
2552
+ enumerable: true,
2553
+ configurable: true,
2554
+ writable: true,
2555
+ value: 'CropApplyError'
2556
+ });
2557
+ Object.defineProperty(this, "originalError", {
2558
+ enumerable: true,
2559
+ configurable: true,
2560
+ writable: true,
2561
+ value: void 0
2562
+ });
2563
+ this.originalError = originalError;
2564
+ fixPrototype(this, CropApplyError);
2565
+ }
2566
+ }
2567
+ class ExportNotReadyError extends Error {
2568
+ constructor(operation = 'exportImageFile') {
2569
+ super(`Cannot ${operation}: no image is loaded on the canvas.`);
2570
+ Object.defineProperty(this, "name", {
2571
+ enumerable: true,
2572
+ configurable: true,
2573
+ writable: true,
2574
+ value: 'ExportNotReadyError'
2575
+ });
2576
+ Object.defineProperty(this, "operation", {
2577
+ enumerable: true,
2578
+ configurable: true,
2579
+ writable: true,
2580
+ value: void 0
2581
+ });
2582
+ this.operation = operation;
2583
+ fixPrototype(this, ExportNotReadyError);
2584
+ }
2585
+ }
2586
+ class ExportError extends Error {
2587
+ constructor(message = 'Failed to export image.', originalError = null) {
2588
+ super(message);
2589
+ Object.defineProperty(this, "name", {
2590
+ enumerable: true,
2591
+ configurable: true,
2592
+ writable: true,
2593
+ value: 'ExportError'
2594
+ });
2595
+ Object.defineProperty(this, "originalError", {
2596
+ enumerable: true,
2597
+ configurable: true,
2598
+ writable: true,
2599
+ value: void 0
2600
+ });
2601
+ this.originalError = originalError;
2602
+ fixPrototype(this, ExportError);
2603
+ }
2604
+ }
2605
+
2606
+ const SELECTED_STROKE = '#ff0000';
2607
+ const SELECTED_STROKE_WIDTH = 1;
2608
+ const HOVER_STROKE = '#ff5500';
2609
+ const HOVER_STROKE_WIDTH = 2;
2610
+ const HOVER_OPACITY_BUMP = 0.2;
2611
+ const DEFAULT_STROKE_FALLBACK = '#ccc';
2612
+ const DEFAULT_STROKE_WIDTH_FALLBACK = 1;
2613
+ const DEFAULT_ALPHA_FALLBACK = 0.5;
2614
+ function getMaskNormalStyle(mask) {
2615
+ var _a;
2616
+ const strokeWidth = Number(mask.originalStrokeWidth);
2617
+ const opacity = Number(mask.originalAlpha);
2618
+ return {
2619
+ stroke: (_a = mask.originalStroke) !== null && _a !== void 0 ? _a : DEFAULT_STROKE_FALLBACK,
2620
+ strokeWidth: Number.isFinite(strokeWidth) ? strokeWidth : DEFAULT_STROKE_WIDTH_FALLBACK,
2621
+ opacity: Number.isFinite(opacity) ? opacity : DEFAULT_ALPHA_FALLBACK,
2622
+ };
2623
+ }
2624
+ function getMaskHoverStyle(mask) {
2625
+ const opacity = Number(mask.originalAlpha);
2626
+ const baseAlpha = Number.isFinite(opacity) ? opacity : DEFAULT_ALPHA_FALLBACK;
2627
+ return {
2628
+ stroke: HOVER_STROKE,
2629
+ strokeWidth: HOVER_STROKE_WIDTH,
2630
+ opacity: Math.min(baseAlpha + HOVER_OPACITY_BUMP, 1),
2631
+ };
2632
+ }
2633
+ function applyMaskSelectedStyle(mask) {
2634
+ mask.set({ stroke: SELECTED_STROKE, strokeWidth: SELECTED_STROKE_WIDTH });
2635
+ }
2636
+ function applyMaskUnselectedStyle(mask) {
2637
+ var _a;
2638
+ const strokeWidth = Number(mask.originalStrokeWidth);
2639
+ mask.set({
2640
+ stroke: (_a = mask.originalStroke) !== null && _a !== void 0 ? _a : DEFAULT_STROKE_FALLBACK,
2641
+ strokeWidth: Number.isFinite(strokeWidth) ? strokeWidth : DEFAULT_STROKE_WIDTH_FALLBACK,
2642
+ });
2643
+ }
2644
+ function attachMaskHoverHandlers(mask) {
2645
+ const tagged = mask;
2646
+ const mouseover = () => {
2647
+ var _a;
2648
+ tagged.set(getMaskHoverStyle(tagged));
2649
+ (_a = tagged.canvas) === null || _a === void 0 ? void 0 : _a.requestRenderAll();
2650
+ };
2651
+ const mouseout = () => {
2652
+ var _a;
2653
+ tagged.set(getMaskNormalStyle(tagged));
2654
+ (_a = tagged.canvas) === null || _a === void 0 ? void 0 : _a.requestRenderAll();
2655
+ };
2656
+ tagged.on('mouseover', mouseover);
2657
+ tagged.on('mouseout', mouseout);
2658
+ tagged.imageEditorMaskHandlers = { mouseover, mouseout };
2659
+ }
2660
+ function reattachMaskHoverHandlers(mask) {
2661
+ var _a;
2662
+ const tagged = mask;
2663
+ if (tagged.imageEditorMaskHandlers) {
2664
+ try {
2665
+ tagged.off('mouseover', tagged.imageEditorMaskHandlers.mouseover);
2666
+ tagged.off('mouseout', tagged.imageEditorMaskHandlers.mouseout);
2667
+ }
2668
+ catch {
2669
+ }
2670
+ delete tagged.imageEditorMaskHandlers;
2671
+ }
2672
+ const patch = {};
2673
+ if (!Number.isFinite(Number(tagged.originalAlpha))) {
2674
+ const opacity = Number(tagged.opacity);
2675
+ patch.originalAlpha = Number.isFinite(opacity) ? opacity : DEFAULT_ALPHA_FALLBACK;
2676
+ }
2677
+ if (tagged.originalStroke == null) {
2678
+ patch.originalStroke = (_a = tagged.stroke) !== null && _a !== void 0 ? _a : DEFAULT_STROKE_FALLBACK;
2679
+ }
2680
+ if (!Number.isFinite(Number(tagged.originalStrokeWidth))) {
2681
+ const sw = Number(tagged.strokeWidth);
2682
+ patch.originalStrokeWidth = Number.isFinite(sw) ? sw : DEFAULT_STROKE_WIDTH_FALLBACK;
2683
+ }
2684
+ if (Object.keys(patch).length > 0)
2685
+ tagged.set(patch);
2686
+ attachMaskHoverHandlers(tagged);
2687
+ }
2688
+ function detachMaskHoverHandlers(mask) {
2689
+ const tagged = mask;
2690
+ if (!tagged.imageEditorMaskHandlers)
2691
+ return;
2692
+ try {
2693
+ tagged.off('mouseover', tagged.imageEditorMaskHandlers.mouseover);
2694
+ tagged.off('mouseout', tagged.imageEditorMaskHandlers.mouseout);
2695
+ }
2696
+ catch {
2697
+ }
2698
+ delete tagged.imageEditorMaskHandlers;
1582
2699
  }
1583
- function getPartialExportEdges(bounds, angle = 0) {
1584
- if (!bounds)
1585
- return null;
1586
- const normalizedAngle = Math.abs((Number(angle) || 0) % 90);
1587
- const isAxisAligned = normalizedAngle < 0.01 || Math.abs(normalizedAngle - 90) < 0.01;
1588
- if (!isAxisAligned)
1589
- return null;
1590
- const left = Number(bounds.left) || 0;
1591
- const top = Number(bounds.top) || 0;
2700
+ function captureMaskStyleBackup(mask) {
2701
+ var _a, _b, _c, _d, _e, _f, _g;
1592
2702
  return {
1593
- left: hasFractionalCanvasEdge(left),
1594
- top: hasFractionalCanvasEdge(top),
1595
- right: hasFractionalCanvasEdge(left + (Number(bounds.width) || 0)),
1596
- bottom: hasFractionalCanvasEdge(top + (Number(bounds.height) || 0)),
2703
+ object: mask,
2704
+ opacity: (_a = mask.opacity) !== null && _a !== void 0 ? _a : 1,
2705
+ fill: ((_b = mask.fill) !== null && _b !== void 0 ? _b : null),
2706
+ strokeWidth: (_c = mask.strokeWidth) !== null && _c !== void 0 ? _c : 0,
2707
+ stroke: ((_d = mask.stroke) !== null && _d !== void 0 ? _d : null),
2708
+ selectable: (_e = mask.selectable) !== null && _e !== void 0 ? _e : true,
2709
+ evented: (_f = mask.evented) !== null && _f !== void 0 ? _f : true,
2710
+ lockRotation: (_g = mask.lockRotation) !== null && _g !== void 0 ? _g : false,
1597
2711
  };
1598
2712
  }
1599
- function getObjectBBox(object) {
1600
- object.setCoords();
1601
- const boundingRect = object.getBoundingRect();
1602
- return {
1603
- left: boundingRect.left,
1604
- top: boundingRect.top,
1605
- width: boundingRect.width,
1606
- height: boundingRect.height,
1607
- };
2713
+ function restoreMaskStyleBackup(backup) {
2714
+ try {
2715
+ backup.object.set({
2716
+ opacity: backup.opacity,
2717
+ fill: backup.fill,
2718
+ strokeWidth: backup.strokeWidth,
2719
+ stroke: backup.stroke,
2720
+ selectable: backup.selectable,
2721
+ evented: backup.evented,
2722
+ lockRotation: backup.lockRotation,
2723
+ });
2724
+ if (typeof backup.object.setCoords === 'function') {
2725
+ backup.object.setCoords();
2726
+ }
2727
+ }
2728
+ catch {
2729
+ }
2730
+ }
2731
+ async function withMaskStyleBackup(context, mutator, callback) {
2732
+ if (!context.canvas)
2733
+ return await callback();
2734
+ const masks = context.canvas.getObjects().filter(isMaskObject);
2735
+ const backups = masks.map(captureMaskStyleBackup);
2736
+ try {
2737
+ masks.forEach((mask, index) => mutator(mask, index));
2738
+ return await callback();
2739
+ }
2740
+ finally {
2741
+ for (const backup of backups)
2742
+ restoreMaskStyleBackup(backup);
2743
+ }
2744
+ }
2745
+ function applyCropHideMaskStyle(mask) {
2746
+ try {
2747
+ mask.set({ opacity: 0, evented: false, selectable: false });
2748
+ }
2749
+ catch {
2750
+ }
1608
2751
  }
1609
2752
 
1610
2753
  const CROP_RECT_FILL = 'rgba(0,0,0,0.12)';
@@ -1839,6 +2982,7 @@ function enterCropMode(context) {
1839
2982
  cropRect.setControlVisible('mtr', false);
1840
2983
  }
1841
2984
  canvas.add(cropRect);
2985
+ markSessionObject(cropRect, 'cropRect');
1842
2986
  cropRect.isCropRect = true;
1843
2987
  canvas.bringObjectToFront(cropRect);
1844
2988
  canvas.setActiveObject(cropRect);
@@ -2238,29 +3382,6 @@ function getCanvasDocument$2(context) {
2238
3382
  const element = (_b = (_a = context.canvas).getElement) === null || _b === void 0 ? void 0 : _b.call(_a);
2239
3383
  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
3384
  }
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
3385
  function safeRender(canvas) {
2265
3386
  try {
2266
3387
  canvas.requestRenderAll();
@@ -2274,7 +3395,6 @@ function safeRender(canvas) {
2274
3395
  }
2275
3396
  }
2276
3397
  function createPreviewCircle(context) {
2277
- var _a;
2278
3398
  const config = context.getMosaicConfig();
2279
3399
  const circle = new context.fabric.Circle({
2280
3400
  left: 0,
@@ -2285,13 +3405,16 @@ function createPreviewCircle(context) {
2285
3405
  fill: config.previewFill,
2286
3406
  stroke: config.previewStroke,
2287
3407
  strokeWidth: config.previewStrokeWidth,
2288
- strokeDashArray: (_a = config.previewStrokeDashArray) !== null && _a !== void 0 ? _a : undefined,
3408
+ strokeDashArray: config.previewStrokeDashArray
3409
+ ? [...config.previewStrokeDashArray]
3410
+ : undefined,
2289
3411
  selectable: false,
2290
3412
  evented: false,
2291
3413
  excludeFromExport: true,
2292
3414
  objectCaching: false,
2293
3415
  visible: false,
2294
3416
  });
3417
+ markSessionObject(circle, 'mosaicPreviewCircle');
2295
3418
  circle.isMosaicPreview = true;
2296
3419
  return circle;
2297
3420
  }
@@ -2334,6 +3457,7 @@ function createPreviewImage(context, sourceImage, rasterCache) {
2334
3457
  objectCaching: false,
2335
3458
  visible: true,
2336
3459
  });
3460
+ markSessionObject(image, 'mosaicPreviewImage');
2337
3461
  image.isMosaicPreview = true;
2338
3462
  return image;
2339
3463
  }
@@ -2385,6 +3509,18 @@ function removePreviewImage(context, session) {
2385
3509
  }
2386
3510
  session.previewImage = null;
2387
3511
  }
3512
+ function releaseMosaicRasterCache(session) {
3513
+ const cache = session.rasterCache;
3514
+ if (!cache)
3515
+ return;
3516
+ try {
3517
+ cache.offscreenCanvas.width = 0;
3518
+ cache.offscreenCanvas.height = 0;
3519
+ }
3520
+ catch {
3521
+ }
3522
+ session.rasterCache = null;
3523
+ }
2388
3524
  function hidePreview(context) {
2389
3525
  var _a;
2390
3526
  const circle = (_a = context.getMosaicSession()) === null || _a === void 0 ? void 0 : _a.previewCircle;
@@ -2548,7 +3684,7 @@ function replaceBaseImage(context, oldImage, newImage, mimeType) {
2548
3684
  canvas.add(newImage);
2549
3685
  newAdded = true;
2550
3686
  canvas.sendObjectToBack(newImage);
2551
- context.setOriginalImage(newImage);
3687
+ context.setOriginalImage(markBaseImageObject(newImage));
2552
3688
  context.setCurrentImageMimeType(mimeType);
2553
3689
  canvas.renderAll();
2554
3690
  }
@@ -2868,6 +4004,7 @@ function exitMosaicMode(context) {
2868
4004
  detachCanvasHandlers(context, session);
2869
4005
  removePreviewCircle(context, session);
2870
4006
  removePreviewImage(context, session);
4007
+ releaseMosaicRasterCache(session);
2871
4008
  restoreObjectStates(session);
2872
4009
  context.canvas.selection = !!session.prevSelection;
2873
4010
  context.canvas.defaultCursor = (_a = session.prevDefaultCursor) !== null && _a !== void 0 ? _a : 'default';
@@ -2875,7 +4012,6 @@ function exitMosaicMode(context) {
2875
4012
  context.canvas.renderAll();
2876
4013
  }
2877
4014
  function updateMosaicPreview(context) {
2878
- var _a;
2879
4015
  const session = context.getMosaicSession();
2880
4016
  const circle = session === null || session === void 0 ? void 0 : session.previewCircle;
2881
4017
  if (!session || !circle)
@@ -2886,12 +4022,141 @@ function updateMosaicPreview(context) {
2886
4022
  fill: config.previewFill,
2887
4023
  stroke: config.previewStroke,
2888
4024
  strokeWidth: config.previewStrokeWidth,
2889
- strokeDashArray: (_a = config.previewStrokeDashArray) !== null && _a !== void 0 ? _a : undefined,
4025
+ strokeDashArray: config.previewStrokeDashArray
4026
+ ? [...config.previewStrokeDashArray]
4027
+ : undefined,
2890
4028
  });
2891
4029
  context.canvas.bringObjectToFront(circle);
2892
4030
  safeRender(context.canvas);
2893
4031
  }
2894
4032
 
4033
+ function startImageElementLoad(dataUrl, options) {
4034
+ const imageElement = new Image();
4035
+ if (options.crossOrigin !== undefined) {
4036
+ imageElement.crossOrigin = options.crossOrigin;
4037
+ }
4038
+ const cleanup = (clearSource = false) => {
4039
+ if (typeof imageElement.removeEventListener === 'function') {
4040
+ imageElement.removeEventListener('load', handleLoad);
4041
+ imageElement.removeEventListener('error', handleError);
4042
+ }
4043
+ else {
4044
+ imageElement.onload = null;
4045
+ imageElement.onerror = null;
4046
+ }
4047
+ if (clearSource) {
4048
+ try {
4049
+ imageElement.src = '';
4050
+ }
4051
+ catch {
4052
+ }
4053
+ }
4054
+ };
4055
+ const handleLoad = () => {
4056
+ var _a, _b;
4057
+ const validationError = (_b = (_a = options.validate) === null || _a === void 0 ? void 0 : _a.call(options, imageElement)) !== null && _b !== void 0 ? _b : null;
4058
+ if (validationError) {
4059
+ cleanup(true);
4060
+ rejectImage(validationError);
4061
+ return;
4062
+ }
4063
+ cleanup(false);
4064
+ resolveImage(imageElement);
4065
+ };
4066
+ const handleError = (event) => {
4067
+ cleanup(true);
4068
+ rejectImage(options.createError(event));
4069
+ };
4070
+ let resolveImage;
4071
+ let rejectImage;
4072
+ const promise = new Promise((resolve, reject) => {
4073
+ resolveImage = resolve;
4074
+ rejectImage = reject;
4075
+ if (typeof imageElement.addEventListener === 'function') {
4076
+ imageElement.addEventListener('load', handleLoad, { once: true });
4077
+ imageElement.addEventListener('error', handleError, { once: true });
4078
+ }
4079
+ else {
4080
+ imageElement.onload = handleLoad;
4081
+ imageElement.onerror = handleError;
4082
+ }
4083
+ imageElement.src = dataUrl;
4084
+ });
4085
+ return { promise, cleanup };
4086
+ }
4087
+
4088
+ function createMergeError(operation, error) {
4089
+ if (operation === 'mergeAnnotations') {
4090
+ if (error instanceof MergeAnnotationsError)
4091
+ return error;
4092
+ const message = error instanceof Error
4093
+ ? `mergeAnnotations failed: ${error.message}`
4094
+ : 'mergeAnnotations failed';
4095
+ return new MergeAnnotationsError(message, error);
4096
+ }
4097
+ if (error instanceof MergeMasksError)
4098
+ return error;
4099
+ const message = error instanceof Error ? `mergeMasks failed: ${error.message}` : 'mergeMasks failed';
4100
+ return new MergeMasksError(message, error);
4101
+ }
4102
+ function detachObjects(canvas, objects) {
4103
+ for (const object of objects) {
4104
+ if (!canvas.getObjects().includes(object))
4105
+ continue;
4106
+ canvas.remove(object);
4107
+ }
4108
+ canvas.discardActiveObject();
4109
+ canvas.renderAll();
4110
+ }
4111
+ async function flattenOverlayGroupToBaseImage(context, options) {
4112
+ if (!context.isImageLoaded())
4113
+ return;
4114
+ if (options.getTargets().length === 0)
4115
+ return;
4116
+ const beforeSnapshot = context.captureSnapshot();
4117
+ const preservedObjects = options.getPreservedObjects();
4118
+ const preScrollTop = context.containerElement ? context.containerElement.scrollTop : null;
4119
+ const preScrollLeft = context.containerElement ? context.containerElement.scrollLeft : null;
4120
+ try {
4121
+ detachObjects(context.canvas, preservedObjects);
4122
+ const exportedDataUrl = await context.exportImageBase64(options.exportOptions);
4123
+ if (!exportedDataUrl) {
4124
+ throw createMergeError(options.operation, `${options.operation}: exportImageBase64 returned an empty data URL.`);
4125
+ }
4126
+ options.removeTargetsNoHistory();
4127
+ await context.loadImage(exportedDataUrl, { preserveScroll: true });
4128
+ await options.restorePreservedObjects(preservedObjects);
4129
+ normalizeLayerOrder(context.canvas);
4130
+ context.canvas.renderAll();
4131
+ context.updateInputs();
4132
+ context.updateUi();
4133
+ if (context.containerElement) {
4134
+ try {
4135
+ if (preScrollTop !== null)
4136
+ context.containerElement.scrollTop = preScrollTop;
4137
+ if (preScrollLeft !== null)
4138
+ context.containerElement.scrollLeft = preScrollLeft;
4139
+ }
4140
+ catch (scrollError) {
4141
+ console.warn(`[ImageEditor] ${options.operation}: scroll restore failed`, scrollError);
4142
+ }
4143
+ }
4144
+ const afterSnapshot = context.captureSnapshot();
4145
+ if (beforeSnapshot && afterSnapshot && beforeSnapshot !== afterSnapshot) {
4146
+ context.historyManager.push(new Command(() => context.loadFromState(afterSnapshot), () => context.loadFromState(beforeSnapshot)));
4147
+ }
4148
+ }
4149
+ catch (error) {
4150
+ try {
4151
+ await context.loadFromState(beforeSnapshot);
4152
+ }
4153
+ catch (rollbackError) {
4154
+ console.warn(`[ImageEditor] ${options.operation}: rollback failed`, rollbackError);
4155
+ }
4156
+ throw createMergeError(options.operation, error);
4157
+ }
4158
+ }
4159
+
2895
4160
  function resolveMultiplier(requested, fallback) {
2896
4161
  const num = Number(requested);
2897
4162
  if (Number.isFinite(num) && num > 0)
@@ -2908,13 +4173,31 @@ function resolveExportOptions(context, options) {
2908
4173
  const providedOptions = options !== null && options !== void 0 ? options : {};
2909
4174
  return {
2910
4175
  exportArea: resolveExportArea(providedOptions.exportArea, context.options.exportAreaByDefault),
2911
- mergeMask: typeof providedOptions.mergeMask === 'boolean'
2912
- ? providedOptions.mergeMask
2913
- : context.options.mergeMaskByDefault,
4176
+ mergeMasks: typeof providedOptions.mergeMasks === 'boolean'
4177
+ ? providedOptions.mergeMasks
4178
+ : context.options.mergeMasksByDefault,
4179
+ mergeAnnotations: typeof providedOptions.mergeAnnotations === 'boolean'
4180
+ ? providedOptions.mergeAnnotations
4181
+ : context.options.mergeAnnotationsByDefault,
2914
4182
  multiplier: resolveMultiplier(providedOptions.multiplier, context.options.exportMultiplier),
2915
4183
  format: resolveExportFormat(providedOptions, context.options.downsampleQuality),
2916
4184
  };
2917
4185
  }
4186
+ function resolveDownloadOptions(context, options) {
4187
+ var _a;
4188
+ const providedOptions = typeof options === 'string'
4189
+ ? { fileName: options }
4190
+ : (options !== null && options !== void 0 ? options : {});
4191
+ return {
4192
+ fileName: (_a = providedOptions.fileName) !== null && _a !== void 0 ? _a : context.options.defaultDownloadFileName,
4193
+ exportOptions: {
4194
+ exportArea: context.options.exportAreaByDefault,
4195
+ mergeMasks: providedOptions.mergeMasks,
4196
+ mergeAnnotations: providedOptions.mergeAnnotations,
4197
+ multiplier: context.options.exportMultiplier,
4198
+ },
4199
+ };
4200
+ }
2918
4201
  function readCanvasDimension(canvas, getterName, propertyName) {
2919
4202
  const canvasLike = canvas;
2920
4203
  const getter = canvasLike[getterName];
@@ -2954,25 +4237,26 @@ function computeExportRegion(context, exportArea) {
2954
4237
  partialEdges: getPartialExportEdges(bounds, Number(originalImage.angle) || 0),
2955
4238
  };
2956
4239
  }
2957
- async function withMaskExportState(context, mergeMask, callback) {
2958
- if (!mergeMask)
2959
- return withMasksHidden(context, callback);
4240
+ async function withMaskExportState(context, mergeMasks, callback) {
4241
+ if (!mergeMasks) {
4242
+ return withObjectsHidden(context.canvas, isMaskObject, callback);
4243
+ }
2960
4244
  return withMaskStyleBackup({ canvas: context.canvas, options: context.options }, applyExportBakeInStyle, callback);
2961
4245
  }
2962
- async function withMasksHidden(context, callback) {
2963
- const backups = getCanvasObjects(context.canvas)
2964
- .filter(isMaskObject)
2965
- .map((mask) => ({
2966
- mask,
2967
- visible: mask.visible,
4246
+ async function withObjectsHidden(canvas, predicate, callback) {
4247
+ const backups = getCanvasObjects(canvas)
4248
+ .filter(predicate)
4249
+ .map((object) => ({
4250
+ object,
4251
+ visible: object.visible,
2968
4252
  }));
2969
4253
  for (const backup of backups) {
2970
4254
  try {
2971
- if (typeof backup.mask.set === 'function') {
2972
- backup.mask.set({ visible: false });
4255
+ if (typeof backup.object.set === 'function') {
4256
+ backup.object.set({ visible: false });
2973
4257
  }
2974
4258
  else {
2975
- backup.mask.visible = false;
4259
+ backup.object.visible = false;
2976
4260
  }
2977
4261
  }
2978
4262
  catch {
@@ -2984,18 +4268,31 @@ async function withMasksHidden(context, callback) {
2984
4268
  finally {
2985
4269
  for (const backup of backups) {
2986
4270
  try {
2987
- if (typeof backup.mask.set === 'function') {
2988
- backup.mask.set({ visible: backup.visible });
4271
+ if (typeof backup.object.set === 'function') {
4272
+ backup.object.set({ visible: backup.visible });
2989
4273
  }
2990
4274
  else {
2991
- backup.mask.visible = backup.visible;
4275
+ backup.object.visible = backup.visible;
2992
4276
  }
2993
4277
  }
2994
4278
  catch {
2995
4279
  }
2996
4280
  }
4281
+ requestRender(canvas);
2997
4282
  }
2998
4283
  }
4284
+ async function withSessionObjectsHidden(context, callback) {
4285
+ return withObjectsHidden(context.canvas, (object) => isSessionObject(object) ||
4286
+ object.isCropRect === true ||
4287
+ object.maskLabel === true ||
4288
+ object.isMosaicPreview === true, callback);
4289
+ }
4290
+ async function withAnnotationsExportState(context, mergeAnnotations, callback) {
4291
+ if (!mergeAnnotations) {
4292
+ return withObjectsHidden(context.canvas, isAnnotationObject, callback);
4293
+ }
4294
+ return withObjectsHidden(context.canvas, (object) => isAnnotationObject(object) && object.annotationHidden === true, callback);
4295
+ }
2999
4296
  function getCanvasObjects(canvas) {
3000
4297
  try {
3001
4298
  return canvas.getObjects();
@@ -3105,67 +4402,40 @@ function applyExportBakeInStyle(mask) {
3105
4402
  }
3106
4403
  function renderCanvasToDataUrl(canvas, format, quality, multiplier, region) {
3107
4404
  const fabricOptions = {
3108
- format,
3109
- multiplier,
3110
- };
3111
- if (quality !== undefined)
3112
- fabricOptions.quality = quality;
3113
- if (region) {
3114
- fabricOptions.left = region.left;
3115
- fabricOptions.top = region.top;
3116
- fabricOptions.width = region.width;
3117
- fabricOptions.height = region.height;
3118
- }
3119
- return canvas.toDataURL(fabricOptions);
3120
- }
3121
- function hasPartialEdges(edges) {
3122
- return !!edges && (edges.left || edges.top || edges.right || edges.bottom);
3123
- }
3124
- function getImageDimensions(imageElement) {
3125
- return {
3126
- width: Math.max(1, imageElement.naturalWidth || imageElement.width || 1),
3127
- height: Math.max(1, imageElement.naturalHeight || imageElement.height || 1),
3128
- };
3129
- }
3130
- function loadImageElement(dataUrl) {
3131
- return new Promise((resolve, reject) => {
3132
- const imageElement = new Image();
3133
- imageElement.crossOrigin = 'anonymous';
3134
- const cleanup = () => {
3135
- if (typeof imageElement.removeEventListener === 'function') {
3136
- imageElement.removeEventListener('load', handleLoad);
3137
- imageElement.removeEventListener('error', handleError);
3138
- }
3139
- else {
3140
- imageElement.onload = null;
3141
- imageElement.onerror = null;
3142
- }
3143
- };
3144
- const handleLoad = () => {
3145
- cleanup();
3146
- resolve(imageElement);
3147
- };
3148
- const handleError = () => {
3149
- cleanup();
3150
- reject(new Error('Failed to decode export data URL'));
3151
- };
3152
- if (typeof imageElement.addEventListener === 'function') {
3153
- imageElement.addEventListener('load', handleLoad, { once: true });
3154
- imageElement.addEventListener('error', handleError, { once: true });
3155
- }
3156
- else {
3157
- imageElement.onload = handleLoad;
3158
- imageElement.onerror = handleError;
3159
- }
3160
- imageElement.src = dataUrl;
3161
- });
4405
+ format,
4406
+ multiplier,
4407
+ };
4408
+ if (quality !== undefined)
4409
+ fabricOptions.quality = quality;
4410
+ if (region) {
4411
+ fabricOptions.left = region.left;
4412
+ fabricOptions.top = region.top;
4413
+ fabricOptions.width = region.width;
4414
+ fabricOptions.height = region.height;
4415
+ }
4416
+ return canvas.toDataURL(fabricOptions);
4417
+ }
4418
+ function hasPartialEdges(edges) {
4419
+ return !!edges && (edges.left || edges.top || edges.right || edges.bottom);
4420
+ }
4421
+ function getImageDimensions(imageElement) {
4422
+ return {
4423
+ width: Math.max(1, imageElement.naturalWidth || imageElement.width || 1),
4424
+ height: Math.max(1, imageElement.naturalHeight || imageElement.height || 1),
4425
+ };
4426
+ }
4427
+ function loadImageElement(dataUrl) {
4428
+ return startImageElementLoad(dataUrl, {
4429
+ crossOrigin: 'anonymous',
4430
+ createError: () => new Error('Failed to decode export data URL'),
4431
+ }).promise;
3162
4432
  }
3163
- async function sealPartialTransparentEdges(dataUrl, edges, target) {
4433
+ async function sealPartialTransparentEdges(dataUrl, edges, target, ownerDocument) {
3164
4434
  if (!hasPartialEdges(edges))
3165
4435
  return dataUrl;
3166
4436
  const imageElement = await loadImageElement(dataUrl);
3167
4437
  const { width, height } = getImageDimensions(imageElement);
3168
- const offscreenCanvas = document.createElement('canvas');
4438
+ const offscreenCanvas = ownerDocument.createElement('canvas');
3169
4439
  offscreenCanvas.width = width;
3170
4440
  offscreenCanvas.height = height;
3171
4441
  const canvasContext = offscreenCanvas.getContext('2d');
@@ -3212,14 +4482,14 @@ async function sealPartialTransparentEdges(dataUrl, edges, target) {
3212
4482
  ? offscreenCanvas.toDataURL(target.mimeType)
3213
4483
  : offscreenCanvas.toDataURL(target.mimeType, target.quality);
3214
4484
  }
3215
- function getJpegBackgroundColor(backgroundColor) {
3216
- return resolveCanvasFillStyle(backgroundColor);
4485
+ function getJpegBackgroundColor(backgroundColor, ownerDocument) {
4486
+ return resolveCanvasFillStyle(backgroundColor, ownerDocument);
3217
4487
  }
3218
- function resolveCanvasFillStyle(backgroundColor, fallback = '#ffffff') {
4488
+ function resolveCanvasFillStyle(backgroundColor, ownerDocument, fallback = '#ffffff') {
3219
4489
  const value = String(backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : '').trim();
3220
4490
  if (!value || isTransparentCssColor(value))
3221
4491
  return '#ffffff';
3222
- const context = createColorValidationContext();
4492
+ const context = createColorValidationContext(ownerDocument);
3223
4493
  if (!context)
3224
4494
  return fallback;
3225
4495
  context.fillStyle = '#000001';
@@ -3236,21 +4506,23 @@ function resolveCanvasFillStyle(backgroundColor, fallback = '#ffffff') {
3236
4506
  return secondResolved;
3237
4507
  return fallback;
3238
4508
  }
3239
- function createColorValidationContext() {
4509
+ function createColorValidationContext(ownerDocument) {
3240
4510
  try {
3241
- if (typeof document === 'undefined' || typeof document.createElement !== 'function') {
3242
- return null;
3243
- }
3244
- return document.createElement('canvas').getContext('2d');
4511
+ return ownerDocument.createElement('canvas').getContext('2d');
3245
4512
  }
3246
4513
  catch {
3247
4514
  return null;
3248
4515
  }
3249
4516
  }
3250
4517
  function getCanvasDocument$1(canvas) {
3251
- var _a, _b, _c, _d, _e;
4518
+ var _a, _b, _c, _d;
3252
4519
  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);
4520
+ const ownerDocument = (_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;
4521
+ if (ownerDocument)
4522
+ return ownerDocument;
4523
+ if (typeof document !== 'undefined')
4524
+ return document;
4525
+ throw new Error('Document is unavailable for export canvas creation.');
3254
4526
  }
3255
4527
  function isTransparentCssColor(value) {
3256
4528
  const normalized = value.trim().toLowerCase();
@@ -3279,16 +4551,16 @@ function isZeroCssAlpha(value) {
3279
4551
  const numericAlpha = Number.parseFloat(alpha);
3280
4552
  return Number.isFinite(numericAlpha) && numericAlpha === 0;
3281
4553
  }
3282
- async function convertDataUrlToOpaqueJpeg(dataUrl, backgroundColor, quality) {
4554
+ async function convertDataUrlToOpaqueJpeg(dataUrl, backgroundColor, quality, ownerDocument) {
3283
4555
  const imageElement = await loadImageElement(dataUrl);
3284
4556
  const { width, height } = getImageDimensions(imageElement);
3285
- const offscreenCanvas = document.createElement('canvas');
4557
+ const offscreenCanvas = ownerDocument.createElement('canvas');
3286
4558
  offscreenCanvas.width = width;
3287
4559
  offscreenCanvas.height = height;
3288
4560
  const canvasContext = offscreenCanvas.getContext('2d');
3289
4561
  if (!canvasContext)
3290
4562
  throw new Error('2D canvas context is unavailable');
3291
- canvasContext.fillStyle = getJpegBackgroundColor(backgroundColor);
4563
+ canvasContext.fillStyle = getJpegBackgroundColor(backgroundColor, ownerDocument);
3292
4564
  canvasContext.fillRect(0, 0, width, height);
3293
4565
  canvasContext.drawImage(imageElement, 0, 0, width, height);
3294
4566
  return offscreenCanvas.toDataURL('image/jpeg', quality);
@@ -3319,13 +4591,14 @@ function dataUrlToBytes(dataUrl) {
3319
4591
  }
3320
4592
  throw new Error('No base64 decoder is available for exportImageFile.');
3321
4593
  }
3322
- async function reencodeDataUrlAs(sourceDataUrl, target, backgroundColor) {
4594
+ async function reencodeDataUrlAs(sourceDataUrl, target, backgroundColor, canvas) {
3323
4595
  if (sourceDataUrl.startsWith(`data:${target.mimeType}`)) {
3324
4596
  return sourceDataUrl;
3325
4597
  }
3326
4598
  const imageElement = await loadImageElement(sourceDataUrl);
3327
4599
  const { width, height } = getImageDimensions(imageElement);
3328
- const offscreenCanvas = document.createElement('canvas');
4600
+ const ownerDocument = getCanvasDocument$1(canvas);
4601
+ const offscreenCanvas = ownerDocument.createElement('canvas');
3329
4602
  offscreenCanvas.width = width;
3330
4603
  offscreenCanvas.height = height;
3331
4604
  const canvasContext = offscreenCanvas.getContext('2d');
@@ -3333,7 +4606,7 @@ async function reencodeDataUrlAs(sourceDataUrl, target, backgroundColor) {
3333
4606
  throw new Error('Unable to acquire 2D context for export conversion');
3334
4607
  }
3335
4608
  if (target.format === 'jpeg') {
3336
- canvasContext.fillStyle = getJpegBackgroundColor(backgroundColor);
4609
+ canvasContext.fillStyle = getJpegBackgroundColor(backgroundColor, ownerDocument);
3337
4610
  canvasContext.fillRect(0, 0, width, height);
3338
4611
  }
3339
4612
  canvasContext.drawImage(imageElement, 0, 0, width, height);
@@ -3356,14 +4629,16 @@ async function exportImageBase64(context, options) {
3356
4629
  assertExportPixelBudget(context, resolved.multiplier, region);
3357
4630
  const renderFormat = region && resolved.format.format === 'jpeg' ? 'png' : resolved.format.format;
3358
4631
  const renderQuality = renderFormat === 'png' ? undefined : resolved.format.quality;
3359
- let dataUrl = await withMaskExportState(context, resolved.mergeMask, async () => renderCanvasToDataUrl(context.canvas, renderFormat, renderQuality, resolved.multiplier, region));
4632
+ let dataUrl = await withSessionObjectsHidden(context, async () => withMaskExportState(context, resolved.mergeMasks, async () => withAnnotationsExportState(context, resolved.mergeAnnotations, async () => renderCanvasToDataUrl(context.canvas, renderFormat, renderQuality, resolved.multiplier, region))));
3360
4633
  if (region) {
3361
4634
  const sealedFormat = resolved.format.format === 'jpeg'
3362
4635
  ? { format: 'png', mimeType: 'image/png', quality: undefined }
3363
4636
  : resolved.format;
3364
- dataUrl = await sealPartialTransparentEdges(dataUrl, partialEdges, sealedFormat);
4637
+ if (hasPartialEdges(partialEdges)) {
4638
+ dataUrl = await sealPartialTransparentEdges(dataUrl, partialEdges, sealedFormat, getCanvasDocument$1(context.canvas));
4639
+ }
3365
4640
  if (resolved.format.format === 'jpeg') {
3366
- dataUrl = await convertDataUrlToOpaqueJpeg(dataUrl, context.options.backgroundColor, resolved.format.quality);
4641
+ dataUrl = await convertDataUrlToOpaqueJpeg(dataUrl, context.options.backgroundColor, resolved.format.quality, getCanvasDocument$1(context.canvas));
3367
4642
  }
3368
4643
  }
3369
4644
  return dataUrl;
@@ -3385,7 +4660,8 @@ async function exportImageFile(context, options) {
3385
4660
  const resolved = resolveExportFormat(providedOptions, context.options.downsampleQuality);
3386
4661
  const base64 = await exportImageBase64(context, {
3387
4662
  exportArea: providedOptions.exportArea,
3388
- mergeMask: providedOptions.mergeMask,
4663
+ mergeMasks: providedOptions.mergeMasks,
4664
+ mergeAnnotations: providedOptions.mergeAnnotations,
3389
4665
  multiplier: providedOptions.multiplier,
3390
4666
  quality: providedOptions.quality,
3391
4667
  fileType: providedOptions.fileType,
@@ -3393,7 +4669,7 @@ async function exportImageFile(context, options) {
3393
4669
  if (!base64) {
3394
4670
  throw new ExportNotReadyError('exportImageFile');
3395
4671
  }
3396
- const finalDataUrl = await reencodeDataUrlAs(base64, resolved, context.options.backgroundColor);
4672
+ const finalDataUrl = await reencodeDataUrlAs(base64, resolved, context.options.backgroundColor, context.canvas);
3397
4673
  let bytes;
3398
4674
  try {
3399
4675
  bytes = dataUrlToBytes(finalDataUrl);
@@ -3403,23 +4679,19 @@ async function exportImageFile(context, options) {
3403
4679
  }
3404
4680
  return new File([bytes], fileName, { type: resolved.mimeType });
3405
4681
  }
3406
- function downloadImage(context, fileName) {
4682
+ function downloadImage(context, options) {
3407
4683
  if (!context.isImageLoaded()) {
3408
4684
  warnNoImageLoaded('downloadImage');
3409
4685
  return;
3410
4686
  }
3411
- const resolvedFileName = fileName !== null && fileName !== void 0 ? fileName : context.options.defaultDownloadFileName;
3412
- void exportImageBase64(context, {
3413
- exportArea: context.options.exportAreaByDefault,
3414
- mergeMask: context.options.mergeMaskByDefault,
3415
- multiplier: context.options.exportMultiplier,
3416
- })
4687
+ const resolved = resolveDownloadOptions(context, options);
4688
+ void exportImageBase64(context, resolved.exportOptions)
3417
4689
  .then((dataUrl) => {
3418
4690
  if (!dataUrl)
3419
4691
  return;
3420
4692
  const ownerDocument = getCanvasDocument$1(context.canvas);
3421
4693
  const link = ownerDocument.createElement('a');
3422
- link.download = resolvedFileName;
4694
+ link.download = resolved.fileName;
3423
4695
  link.href = dataUrl;
3424
4696
  const body = ownerDocument.body;
3425
4697
  body.appendChild(link);
@@ -3436,60 +4708,40 @@ function downloadImage(context, fileName) {
3436
4708
  });
3437
4709
  }
3438
4710
  async function mergeMasks(context) {
3439
- if (!context.isImageLoaded())
3440
- return;
3441
- const masks = context.canvas
3442
- .getObjects()
3443
- .filter((o) => 'maskId' in o && typeof o.maskId === 'number');
3444
- if (masks.length === 0)
3445
- return;
3446
- const beforeSnapshot = context.saveState();
3447
- context.canvas.discardActiveObject();
3448
- context.canvas.renderAll();
3449
- const preScrollTop = context.containerElement ? context.containerElement.scrollTop : null;
3450
- const preScrollLeft = context.containerElement ? context.containerElement.scrollLeft : null;
3451
- try {
3452
- const merged = await exportImageBase64(context, {
4711
+ await flattenOverlayGroupToBaseImage(context, {
4712
+ operation: 'mergeMasks',
4713
+ exportOptions: {
3453
4714
  exportArea: 'image',
3454
- mergeMask: true,
4715
+ mergeMasks: true,
4716
+ mergeAnnotations: false,
3455
4717
  multiplier: context.options.exportMultiplier,
3456
4718
  fileType: 'png',
3457
- });
3458
- if (!merged) {
3459
- throw new MergeMasksError('mergeMasks: exportImageBase64 returned an empty data URL.');
3460
- }
3461
- context.removeAllMasksNoHistory();
3462
- await context.loadImage(merged, { preserveScroll: true });
3463
- const afterSnapshot = context.saveState();
3464
- if (context.containerElement) {
3465
- try {
3466
- if (preScrollTop !== null) {
3467
- context.containerElement.scrollTop = preScrollTop;
3468
- }
3469
- if (preScrollLeft !== null) {
3470
- context.containerElement.scrollLeft = preScrollLeft;
3471
- }
3472
- }
3473
- catch (scrollError) {
3474
- console.warn('[ImageEditor] mergeMasks: scroll restore failed', scrollError);
3475
- }
3476
- }
3477
- if (beforeSnapshot && afterSnapshot && beforeSnapshot !== afterSnapshot) {
3478
- context.historyManager.push(new Command(() => context.loadFromState(afterSnapshot), () => context.loadFromState(beforeSnapshot)));
3479
- }
3480
- }
3481
- catch (error) {
3482
- try {
3483
- await context.loadFromState(beforeSnapshot);
3484
- }
3485
- catch (rollbackError) {
3486
- console.warn('[ImageEditor] mergeMasks: rollback failed', rollbackError);
3487
- }
3488
- if (error instanceof MergeMasksError)
3489
- throw error;
3490
- const message = error instanceof Error ? `mergeMasks failed: ${error.message}` : 'mergeMasks failed';
3491
- throw new MergeMasksError(message, error);
3492
- }
4719
+ },
4720
+ getTargets: () => context.canvas.getObjects().filter(isMaskObject),
4721
+ getPreservedObjects: () => context.getAnnotations(),
4722
+ removeTargetsNoHistory: () => {
4723
+ context.removeAllMasksNoHistory();
4724
+ },
4725
+ restorePreservedObjects: (objects) => context.restoreAnnotations(objects),
4726
+ });
4727
+ }
4728
+ async function mergeAnnotations(context) {
4729
+ await flattenOverlayGroupToBaseImage(context, {
4730
+ operation: 'mergeAnnotations',
4731
+ exportOptions: {
4732
+ exportArea: 'image',
4733
+ mergeMasks: false,
4734
+ mergeAnnotations: true,
4735
+ multiplier: context.options.exportMultiplier,
4736
+ fileType: 'png',
4737
+ },
4738
+ getTargets: () => context.canvas.getObjects().filter(isAnnotationObject),
4739
+ getPreservedObjects: () => context.getMasks(),
4740
+ removeTargetsNoHistory: () => {
4741
+ context.removeAllAnnotationsNoHistory();
4742
+ },
4743
+ restorePreservedObjects: (objects) => context.restoreMasks(objects),
4744
+ });
3493
4745
  }
3494
4746
 
3495
4747
  function forceReflow(element) {
@@ -3723,6 +4975,7 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
3723
4975
  lastSnapshot: context.getLastSnapshot(),
3724
4976
  canvasJson: serializeCanvas(context.canvas),
3725
4977
  maskCounter: context.getMaskCounter(),
4978
+ annotationCounter: context.getAnnotationCounter(),
3726
4979
  currentScale: context.getCurrentScale(),
3727
4980
  currentRotation: context.getCurrentRotation(),
3728
4981
  baseImageScale: context.getBaseImageScale(),
@@ -3744,23 +4997,25 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
3744
4997
  context.canvas.discardActiveObject();
3745
4998
  context.canvas.clear();
3746
4999
  context.canvas.backgroundColor = context.options.backgroundColor;
3747
- fabricImage.set({
5000
+ const baseImage = markBaseImageObject(fabricImage);
5001
+ baseImage.set({
3748
5002
  originX: 'left',
3749
5003
  originY: 'top',
3750
5004
  selectable: false,
3751
5005
  evented: false,
3752
5006
  });
3753
- const layout = computeLayout(context, fabricImage);
5007
+ const layout = computeLayout(context, baseImage);
3754
5008
  applyCanvasDimensions(context.canvas, layout.canvasWidth, layout.canvasHeight, context.containerElement);
3755
- fabricImage.set({ left: layout.imageLeft, top: layout.imageTop });
3756
- fabricImage.scale(layout.imageScale);
3757
- context.canvas.add(fabricImage);
3758
- context.canvas.sendObjectToBack(fabricImage);
3759
- context.setOriginalImage(fabricImage);
5009
+ baseImage.set({ left: layout.imageLeft, top: layout.imageTop });
5010
+ baseImage.scale(layout.imageScale);
5011
+ context.canvas.add(baseImage);
5012
+ context.canvas.sendObjectToBack(baseImage);
5013
+ context.setOriginalImage(baseImage);
3760
5014
  context.setBaseImageScale(layout.baseImageScale);
3761
5015
  context.setCurrentScale(1);
3762
5016
  context.setCurrentRotation(0);
3763
5017
  context.setMaskCounter(0);
5018
+ context.setAnnotationCounter(0);
3764
5019
  context.setIsImageLoadedToCanvas(true);
3765
5020
  context.setCurrentImageMimeType(loadSource.mimeType);
3766
5021
  context.canvas.renderAll();
@@ -3793,49 +5048,12 @@ async function loadImage(context, imageBase64, loadOptions = {}) {
3793
5048
  }
3794
5049
  }
3795
5050
  function startImageDecode(dataUrl) {
3796
- const imageElement = new Image();
3797
- const cleanup = (clearSource = false) => {
3798
- if (typeof imageElement.removeEventListener === 'function') {
3799
- imageElement.removeEventListener('load', handleLoad);
3800
- imageElement.removeEventListener('error', handleError);
3801
- }
3802
- else {
3803
- imageElement.onload = null;
3804
- imageElement.onerror = null;
3805
- }
3806
- if (clearSource) {
3807
- imageElement.src = '';
3808
- }
3809
- };
3810
- const handleLoad = () => {
3811
- if (!hasNaturalImageDimensions(imageElement)) {
3812
- cleanup(true);
3813
- rejectImage(new ImageDecodeError('Failed to decode image data URL: image has no natural dimensions.', null));
3814
- return;
3815
- }
3816
- cleanup(false);
3817
- resolveImage(imageElement);
3818
- };
3819
- const handleError = (e) => {
3820
- cleanup(true);
3821
- rejectImage(new ImageDecodeError('Failed to decode image data URL.', e));
3822
- };
3823
- let resolveImage;
3824
- let rejectImage;
3825
- const promise = new Promise((resolve, reject) => {
3826
- resolveImage = resolve;
3827
- rejectImage = reject;
3828
- if (typeof imageElement.addEventListener === 'function') {
3829
- imageElement.addEventListener('load', handleLoad, { once: true });
3830
- imageElement.addEventListener('error', handleError, { once: true });
3831
- }
3832
- else {
3833
- imageElement.onload = handleLoad;
3834
- imageElement.onerror = handleError;
3835
- }
3836
- imageElement.src = dataUrl;
5051
+ return startImageElementLoad(dataUrl, {
5052
+ validate: (imageElement) => hasNaturalImageDimensions(imageElement)
5053
+ ? null
5054
+ : new ImageDecodeError('Failed to decode image data URL: image has no natural dimensions.', null),
5055
+ createError: (event) => new ImageDecodeError('Failed to decode image data URL.', event),
3837
5056
  });
3838
- return { promise, cleanup };
3839
5057
  }
3840
5058
  function hasNaturalImageDimensions(imageElement) {
3841
5059
  return (Number.isFinite(imageElement.naturalWidth) &&
@@ -3913,6 +5131,7 @@ async function replayRollback(context, bundle) {
3913
5131
  context.setIsImageLoadedToCanvas(bundle.isImageLoadedToCanvas);
3914
5132
  context.setLastSnapshot(bundle.lastSnapshot);
3915
5133
  context.setMaskCounter(bundle.maskCounter);
5134
+ context.setAnnotationCounter(bundle.annotationCounter);
3916
5135
  context.setCurrentScale(bundle.currentScale);
3917
5136
  context.setCurrentRotation(bundle.currentRotation);
3918
5137
  context.setBaseImageScale(bundle.baseImageScale);
@@ -4155,30 +5374,6 @@ function computeTopLeftPoint(object) {
4155
5374
  return { x: boundingRect.left, y: boundingRect.top };
4156
5375
  }
4157
5376
 
4158
- function resolveNumeric(val, axis, fallback, canvas, options) {
4159
- if (typeof val === 'number') {
4160
- return val;
4161
- }
4162
- if (typeof val === 'function') {
4163
- return val(canvas, options);
4164
- }
4165
- if (typeof val === 'string' && val.endsWith('%')) {
4166
- const pct = parseFloat(val);
4167
- if (!Number.isFinite(pct)) {
4168
- return fallback;
4169
- }
4170
- const dim = axis === 'x' ? canvas.getWidth() : canvas.getHeight();
4171
- return Math.floor(dim * (pct / 100));
4172
- }
4173
- return fallback;
4174
- }
4175
- function coercePoint(pt) {
4176
- if (Array.isArray(pt)) {
4177
- return { x: Number(pt[0]), y: Number(pt[1]) };
4178
- }
4179
- return { x: Number(pt.x), y: Number(pt.y) };
4180
- }
4181
-
4182
5377
  const POLYGON_AREA_EPSILON = 1e-6;
4183
5378
  function createMaskUid(maskId) {
4184
5379
  return `mask-${maskId}`;
@@ -4473,18 +5668,19 @@ function createMask(context, config = {}) {
4473
5668
  if ('strokeDashArray' in styles) {
4474
5669
  maskObject.strokeDashArray = styles.strokeDashArray;
4475
5670
  }
4476
- maskObject.originalAlpha = resolvedConfig.alpha;
4477
- maskObject.originalStroke = maskObject.stroke;
4478
- maskObject.originalStrokeWidth = maskObject.strokeWidth;
4479
- attachMaskHoverHandlers(maskObject);
4480
5671
  const nextId = context.getMaskCounter() + 1;
4481
5672
  context.setMaskCounter(nextId);
4482
- maskObject.maskId = nextId;
4483
- maskObject.maskUid = createMaskUid(nextId);
4484
- maskObject.maskName = `${options.maskName}${nextId}`;
5673
+ markMaskObject(maskObject, {
5674
+ maskId: nextId,
5675
+ maskUid: createMaskUid(nextId),
5676
+ maskName: `${options.maskName}${nextId}`,
5677
+ originalAlpha: resolvedConfig.alpha,
5678
+ originalStroke: maskObject.stroke,
5679
+ originalStrokeWidth: maskObject.strokeWidth,
5680
+ });
5681
+ attachMaskHoverHandlers(maskObject);
4485
5682
  context.setLastMask(maskObject);
4486
- canvas.add(maskObject);
4487
- canvas.bringObjectToFront(maskObject);
5683
+ placeMaskObject(canvas, maskObject);
4488
5684
  context.updateMaskList();
4489
5685
  if (resolvedConfig.selectable !== false) {
4490
5686
  canvas.setActiveObject(maskObject);
@@ -4501,13 +5697,35 @@ function createMask(context, config = {}) {
4501
5697
  }
4502
5698
  return maskObject;
4503
5699
  }
5700
+ function isActiveSelectionObject(object) {
5701
+ if (!object)
5702
+ return false;
5703
+ const type = typeof object.type === 'string' ? object.type.toLowerCase() : '';
5704
+ if (type === 'activeselection')
5705
+ return true;
5706
+ const isType = object.isType;
5707
+ return (typeof isType === 'function' &&
5708
+ (isType.call(object, 'ActiveSelection') || isType.call(object, 'activeSelection')));
5709
+ }
5710
+ function getSelectedMaskObjects(canvas) {
5711
+ const active = canvas.getActiveObject();
5712
+ if (!active)
5713
+ return [];
5714
+ if (!isActiveSelectionObject(active))
5715
+ return isMaskObject(active) ? [active] : [];
5716
+ const getObjects = active.getObjects;
5717
+ const objects = typeof getObjects === 'function' ? getObjects.call(active) : [];
5718
+ return objects.filter(isMaskObject);
5719
+ }
4504
5720
  function removeSelectedMask(context) {
4505
- const active = context.canvas.getActiveObject();
4506
- if (!active || !isMaskObject(active))
5721
+ const selectedMasks = getSelectedMaskObjects(context.canvas);
5722
+ if (selectedMasks.length === 0)
4507
5723
  return;
4508
- context.removeLabelForMask(active);
4509
- detachMaskHoverHandlers(active);
4510
- context.canvas.remove(active);
5724
+ for (const mask of selectedMasks) {
5725
+ context.removeLabelForMask(mask);
5726
+ detachMaskHoverHandlers(mask);
5727
+ context.canvas.remove(mask);
5728
+ }
4511
5729
  context.canvas.discardActiveObject();
4512
5730
  context.updateMaskList();
4513
5731
  context.canvas.renderAll();
@@ -4584,6 +5802,7 @@ function createLabelForMask(context, mask) {
4584
5802
  };
4585
5803
  labelTextObject = new fabricModule.FabricText(labelText, textOptions);
4586
5804
  }
5805
+ markSessionObject(labelTextObject, 'maskLabel');
4587
5806
  labelTextObject.maskLabel = true;
4588
5807
  mask.labelObject = labelTextObject;
4589
5808
  canvas.add(labelTextObject);
@@ -4845,6 +6064,22 @@ const CROP_MODE_CONTROL_KEYS = [
4845
6064
  'removeSelectedMaskButton',
4846
6065
  'removeAllMasksButton',
4847
6066
  'mergeMasksButton',
6067
+ 'mergeAnnotationsButton',
6068
+ 'enterTextModeButton',
6069
+ 'exitTextModeButton',
6070
+ 'textColorInput',
6071
+ 'textFontSizeInput',
6072
+ 'enterDrawModeButton',
6073
+ 'exitDrawModeButton',
6074
+ 'drawColorInput',
6075
+ 'drawBrushSizeInput',
6076
+ 'removeSelectedAnnotationButton',
6077
+ 'removeAllAnnotationsButton',
6078
+ 'deleteSelectedObjectButton',
6079
+ 'bringSelectedObjectForwardButton',
6080
+ 'sendSelectedObjectBackwardButton',
6081
+ 'bringSelectedObjectToFrontButton',
6082
+ 'sendSelectedObjectToBackButton',
4848
6083
  'downloadImageButton',
4849
6084
  'zoomInButton',
4850
6085
  'zoomOutButton',
@@ -4862,6 +6097,16 @@ const CROP_MODE_CONTROL_KEYS = [
4862
6097
  ];
4863
6098
  const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
4864
6099
  const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
6100
+ const TEXT_MODE_ENABLED_KEYS = [
6101
+ 'exitTextModeButton',
6102
+ 'textColorInput',
6103
+ 'textFontSizeInput',
6104
+ ];
6105
+ const DRAW_MODE_ENABLED_KEYS = [
6106
+ 'exitDrawModeButton',
6107
+ 'drawColorInput',
6108
+ 'drawBrushSizeInput',
6109
+ ];
4865
6110
  const MOSAIC_MODE_CONTROL_KEYS = [
4866
6111
  'scalePercentageInput',
4867
6112
  'rotateLeftDegreesInput',
@@ -4872,6 +6117,22 @@ const MOSAIC_MODE_CONTROL_KEYS = [
4872
6117
  'removeSelectedMaskButton',
4873
6118
  'removeAllMasksButton',
4874
6119
  'mergeMasksButton',
6120
+ 'mergeAnnotationsButton',
6121
+ 'enterTextModeButton',
6122
+ 'exitTextModeButton',
6123
+ 'textColorInput',
6124
+ 'textFontSizeInput',
6125
+ 'enterDrawModeButton',
6126
+ 'exitDrawModeButton',
6127
+ 'drawColorInput',
6128
+ 'drawBrushSizeInput',
6129
+ 'removeSelectedAnnotationButton',
6130
+ 'removeAllAnnotationsButton',
6131
+ 'deleteSelectedObjectButton',
6132
+ 'bringSelectedObjectForwardButton',
6133
+ 'sendSelectedObjectBackwardButton',
6134
+ 'bringSelectedObjectToFrontButton',
6135
+ 'sendSelectedObjectToBackButton',
4875
6136
  'downloadImageButton',
4876
6137
  'zoomInButton',
4877
6138
  'zoomOutButton',
@@ -4914,6 +6175,29 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
4914
6175
  'removeSelectedMask',
4915
6176
  'removeAllMasks',
4916
6177
  'mergeMasks',
6178
+ 'createTextAnnotation',
6179
+ 'enterTextMode',
6180
+ 'exitTextMode',
6181
+ 'setTextConfig',
6182
+ 'resetTextConfig',
6183
+ 'setTextColor',
6184
+ 'setTextFontSize',
6185
+ 'enterDrawMode',
6186
+ 'exitDrawMode',
6187
+ 'setDrawConfig',
6188
+ 'resetDrawConfig',
6189
+ 'setDrawColor',
6190
+ 'setDrawBrushSize',
6191
+ 'updateSelectedAnnotation',
6192
+ 'updateAnnotation',
6193
+ 'removeSelectedAnnotation',
6194
+ 'removeAllAnnotations',
6195
+ 'deleteSelectedObject',
6196
+ 'mergeAnnotations',
6197
+ 'bringSelectedObjectForward',
6198
+ 'sendSelectedObjectBackward',
6199
+ 'bringSelectedObjectToFront',
6200
+ 'sendSelectedObjectToBack',
4917
6201
  'enterCropMode',
4918
6202
  'applyCrop',
4919
6203
  'cancelCrop',
@@ -4931,6 +6215,27 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
4931
6215
  'downloadImage',
4932
6216
  'dispose',
4933
6217
  ]);
6218
+ const TOOL_MODE_ALLOWED_OPERATIONS = {
6219
+ crop: CROP_SESSION_ALLOWED_OPERATIONS,
6220
+ mosaic: MOSAIC_SESSION_ALLOWED_OPERATIONS,
6221
+ text: new Set([
6222
+ 'exitTextMode',
6223
+ 'createTextAnnotation',
6224
+ 'setTextConfig',
6225
+ 'resetTextConfig',
6226
+ 'setTextColor',
6227
+ 'setTextFontSize',
6228
+ 'saveState',
6229
+ ]),
6230
+ draw: new Set([
6231
+ 'exitDrawMode',
6232
+ 'setDrawConfig',
6233
+ 'resetDrawConfig',
6234
+ 'setDrawColor',
6235
+ 'setDrawBrushSize',
6236
+ 'saveState',
6237
+ ]),
6238
+ };
4934
6239
  function isImageEditorOperation(value) {
4935
6240
  return value !== null && IMAGE_EDITOR_OPERATIONS.has(value);
4936
6241
  }
@@ -4973,6 +6278,30 @@ class ImageEditor {
4973
6278
  writable: true,
4974
6279
  value: void 0
4975
6280
  });
6281
+ Object.defineProperty(this, "defaultTextConfig", {
6282
+ enumerable: true,
6283
+ configurable: true,
6284
+ writable: true,
6285
+ value: void 0
6286
+ });
6287
+ Object.defineProperty(this, "currentTextConfig", {
6288
+ enumerable: true,
6289
+ configurable: true,
6290
+ writable: true,
6291
+ value: void 0
6292
+ });
6293
+ Object.defineProperty(this, "defaultDrawConfig", {
6294
+ enumerable: true,
6295
+ configurable: true,
6296
+ writable: true,
6297
+ value: void 0
6298
+ });
6299
+ Object.defineProperty(this, "currentDrawConfig", {
6300
+ enumerable: true,
6301
+ configurable: true,
6302
+ writable: true,
6303
+ value: void 0
6304
+ });
4976
6305
  Object.defineProperty(this, "canvas", {
4977
6306
  enumerable: true,
4978
6307
  configurable: true,
@@ -5069,6 +6398,12 @@ class ImageEditor {
5069
6398
  writable: true,
5070
6399
  value: null
5071
6400
  });
6401
+ Object.defineProperty(this, "annotationCounter", {
6402
+ enumerable: true,
6403
+ configurable: true,
6404
+ writable: true,
6405
+ value: 0
6406
+ });
5072
6407
  Object.defineProperty(this, "lastSnapshot", {
5073
6408
  enumerable: true,
5074
6409
  configurable: true,
@@ -5117,12 +6452,36 @@ class ImageEditor {
5117
6452
  writable: true,
5118
6453
  value: null
5119
6454
  });
6455
+ Object.defineProperty(this, "textSession", {
6456
+ enumerable: true,
6457
+ configurable: true,
6458
+ writable: true,
6459
+ value: null
6460
+ });
6461
+ Object.defineProperty(this, "drawSession", {
6462
+ enumerable: true,
6463
+ configurable: true,
6464
+ writable: true,
6465
+ value: null
6466
+ });
5120
6467
  Object.defineProperty(this, "domBindings", {
5121
6468
  enumerable: true,
5122
6469
  configurable: true,
5123
6470
  writable: true,
5124
6471
  value: null
5125
6472
  });
6473
+ Object.defineProperty(this, "keyboardDocument", {
6474
+ enumerable: true,
6475
+ configurable: true,
6476
+ writable: true,
6477
+ value: null
6478
+ });
6479
+ Object.defineProperty(this, "keyboardHandler", {
6480
+ enumerable: true,
6481
+ configurable: true,
6482
+ writable: true,
6483
+ value: null
6484
+ });
5126
6485
  Object.defineProperty(this, "isDisposed", {
5127
6486
  enumerable: true,
5128
6487
  configurable: true,
@@ -5160,6 +6519,10 @@ class ImageEditor {
5160
6519
  this.currentLayoutMode = this.options.layoutMode;
5161
6520
  this.defaultMosaicConfig = this.options.defaultMosaicConfig;
5162
6521
  this.currentMosaicConfig = cloneResolvedMosaicConfig(this.defaultMosaicConfig);
6522
+ this.defaultTextConfig = this.options.defaultTextConfig;
6523
+ this.currentTextConfig = cloneResolvedTextAnnotationConfig(this.defaultTextConfig);
6524
+ this.defaultDrawConfig = this.options.defaultDrawConfig;
6525
+ this.currentDrawConfig = cloneResolvedDrawConfig(this.defaultDrawConfig);
5163
6526
  const rawDefaultLayoutMode = detected.options
5164
6527
  .defaultLayoutMode;
5165
6528
  if (rawDefaultLayoutMode !== undefined && !isLayoutMode(rawDefaultLayoutMode)) {
@@ -5196,6 +6559,23 @@ class ImageEditor {
5196
6559
  removeSelectedMaskButton: 'removeSelectedMaskButton',
5197
6560
  removeAllMasksButton: 'removeAllMasksButton',
5198
6561
  mergeMasksButton: 'mergeMasksButton',
6562
+ annotationList: 'annotationList',
6563
+ enterTextModeButton: 'enterTextModeButton',
6564
+ exitTextModeButton: 'exitTextModeButton',
6565
+ textColorInput: 'textColorInput',
6566
+ textFontSizeInput: 'textFontSizeInput',
6567
+ enterDrawModeButton: 'enterDrawModeButton',
6568
+ exitDrawModeButton: 'exitDrawModeButton',
6569
+ drawColorInput: 'drawColorInput',
6570
+ drawBrushSizeInput: 'drawBrushSizeInput',
6571
+ removeSelectedAnnotationButton: 'removeSelectedAnnotationButton',
6572
+ removeAllAnnotationsButton: 'removeAllAnnotationsButton',
6573
+ deleteSelectedObjectButton: 'deleteSelectedObjectButton',
6574
+ mergeAnnotationsButton: 'mergeAnnotationsButton',
6575
+ bringSelectedObjectForwardButton: 'bringSelectedObjectForwardButton',
6576
+ sendSelectedObjectBackwardButton: 'sendSelectedObjectBackwardButton',
6577
+ bringSelectedObjectToFrontButton: 'bringSelectedObjectToFrontButton',
6578
+ sendSelectedObjectToBackButton: 'sendSelectedObjectToBackButton',
5199
6579
  downloadImageButton: 'downloadImageButton',
5200
6580
  maskList: 'maskList',
5201
6581
  zoomInButton: 'zoomInButton',
@@ -5214,12 +6594,13 @@ class ImageEditor {
5214
6594
  uploadArea: 'uploadArea',
5215
6595
  };
5216
6596
  this.elements = { ...defaults, ...idMap };
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; });
5218
6597
  this.initCanvas();
6598
+ 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; });
5219
6599
  this.transformController = new TransformController(this.buildTransformContext());
5220
6600
  this.bindDomEvents();
5221
6601
  this.updateInputs();
5222
6602
  this.updateMaskList();
6603
+ this.updateAnnotationList();
5223
6604
  this.updateUi();
5224
6605
  if (this.options.initialImageBase64) {
5225
6606
  void this.loadImage(this.options.initialImageBase64).catch(() => {
@@ -5271,20 +6652,24 @@ class ImageEditor {
5271
6652
  });
5272
6653
  this.canvas.on('selection:cleared', () => this.handleSelectionChanged([]));
5273
6654
  const onObjectEvent = (e) => {
5274
- if (e.target && isMaskObject(e.target))
5275
- this.syncMaskLabel(e.target);
6655
+ if (e.target)
6656
+ this.handleObjectMovingScalingRotating(e.target);
5276
6657
  };
5277
6658
  const onObjectModified = (e) => {
5278
- if (!e.target || !isMaskObject(e.target))
5279
- return;
5280
- this.syncMaskLabel(e.target);
5281
- this.saveState();
6659
+ if (e.target)
6660
+ this.handleObjectModified(e.target);
5282
6661
  };
5283
6662
  this.canvas.on('object:moving', onObjectEvent);
5284
6663
  this.canvas.on('object:scaling', onObjectEvent);
5285
6664
  this.canvas.on('object:rotating', onObjectEvent);
5286
6665
  this.canvas.on('object:modified', onObjectModified);
5287
6666
  }
6667
+ getLiveCanvasOrThrow(operationName) {
6668
+ if (this.isDisposed || !this.canvas) {
6669
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" after dispose.`);
6670
+ }
6671
+ return this.canvas;
6672
+ }
5288
6673
  bindDomEvents() {
5289
6674
  this.bindElementIfExists('uploadArea', 'click', () => {
5290
6675
  var _a;
@@ -5319,6 +6704,42 @@ class ImageEditor {
5319
6704
  this.bindElementIfExists('mergeMasksButton', 'click', () => {
5320
6705
  void this.mergeMasks();
5321
6706
  });
6707
+ this.bindElementIfExists('mergeAnnotationsButton', 'click', () => {
6708
+ void this.mergeAnnotations();
6709
+ });
6710
+ this.bindElementIfExists('enterTextModeButton', 'click', () => {
6711
+ this.enterTextMode();
6712
+ });
6713
+ this.bindElementIfExists('exitTextModeButton', 'click', () => {
6714
+ this.exitTextMode();
6715
+ });
6716
+ this.bindElementIfExists('enterDrawModeButton', 'click', () => {
6717
+ this.enterDrawMode();
6718
+ });
6719
+ this.bindElementIfExists('exitDrawModeButton', 'click', () => {
6720
+ this.exitDrawMode();
6721
+ });
6722
+ this.bindElementIfExists('removeSelectedAnnotationButton', 'click', () => {
6723
+ this.removeSelectedAnnotation();
6724
+ });
6725
+ this.bindElementIfExists('removeAllAnnotationsButton', 'click', () => {
6726
+ this.removeAllAnnotations();
6727
+ });
6728
+ this.bindElementIfExists('deleteSelectedObjectButton', 'click', () => {
6729
+ this.deleteSelectedObject();
6730
+ });
6731
+ this.bindElementIfExists('bringSelectedObjectForwardButton', 'click', () => {
6732
+ this.bringSelectedObjectForward();
6733
+ });
6734
+ this.bindElementIfExists('sendSelectedObjectBackwardButton', 'click', () => {
6735
+ this.sendSelectedObjectBackward();
6736
+ });
6737
+ this.bindElementIfExists('bringSelectedObjectToFrontButton', 'click', () => {
6738
+ this.bringSelectedObjectToFront();
6739
+ });
6740
+ this.bindElementIfExists('sendSelectedObjectToBackButton', 'click', () => {
6741
+ this.sendSelectedObjectToBack();
6742
+ });
5322
6743
  this.bindElementIfExists('downloadImageButton', 'click', () => {
5323
6744
  this.downloadImage();
5324
6745
  });
@@ -5385,10 +6806,91 @@ class ImageEditor {
5385
6806
  bindMosaicSizeInput('mosaicBlockSizeInput', (value) => {
5386
6807
  this.setMosaicBlockSize(value);
5387
6808
  });
6809
+ const bindStringInput = (key, applyValue) => {
6810
+ const handler = (event) => {
6811
+ applyValue(event.target.value);
6812
+ };
6813
+ this.bindElementIfExists(key, 'input', handler);
6814
+ this.bindElementIfExists(key, 'change', handler);
6815
+ };
6816
+ const bindNumberInput = (key, applyValue) => {
6817
+ const handler = (event) => {
6818
+ applyValue(parseFloat(event.target.value));
6819
+ };
6820
+ this.bindElementIfExists(key, 'input', handler);
6821
+ this.bindElementIfExists(key, 'change', handler);
6822
+ };
6823
+ bindStringInput('textColorInput', (value) => this.applyTextColorInput(value));
6824
+ bindNumberInput('textFontSizeInput', (value) => this.applyTextFontSizeInput(value));
6825
+ bindStringInput('drawColorInput', (value) => this.applyDrawColorInput(value));
6826
+ bindNumberInput('drawBrushSizeInput', (value) => this.applyDrawBrushSizeInput(value));
6827
+ this.bindKeyboardEvents();
6828
+ }
6829
+ bindElementIfExists(key, event, handler) {
6830
+ var _a;
6831
+ (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.bindIfExists(key, event, handler);
6832
+ }
6833
+ bindKeyboardEvents() {
6834
+ var _a, _b;
6835
+ const ownerDocument = (_b = (_a = this.canvasElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document;
6836
+ if (this.keyboardHandler && this.keyboardDocument) {
6837
+ this.keyboardDocument.removeEventListener('keydown', this.keyboardHandler);
6838
+ }
6839
+ this.keyboardDocument = ownerDocument;
6840
+ this.keyboardHandler = (event) => this.handleKeyboardEvent(event);
6841
+ ownerDocument.addEventListener('keydown', this.keyboardHandler);
6842
+ }
6843
+ isNativeTextInputActive() {
6844
+ var _a;
6845
+ const activeElement = (_a = this.keyboardDocument) === null || _a === void 0 ? void 0 : _a.activeElement;
6846
+ if (!activeElement)
6847
+ return false;
6848
+ const tagName = activeElement.tagName.toLowerCase();
6849
+ return (tagName === 'input' ||
6850
+ tagName === 'textarea' ||
6851
+ tagName === 'select' ||
6852
+ activeElement.isContentEditable === true);
6853
+ }
6854
+ isFabricTextEditingActive() {
6855
+ var _a;
6856
+ const activeObject = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
6857
+ return !!(activeObject &&
6858
+ isTextAnnotationObject(activeObject) &&
6859
+ activeObject.isEditing === true);
6860
+ }
6861
+ handleKeyboardEvent(event) {
6862
+ if (this.isDisposed)
6863
+ return;
6864
+ if (event.key === 'Delete' || event.key === 'Backspace') {
6865
+ if (this.isNativeTextInputActive() || this.isFabricTextEditingActive())
6866
+ return;
6867
+ this.deleteSelectedObject();
6868
+ return;
6869
+ }
6870
+ if (event.key !== 'Escape')
6871
+ return;
6872
+ if (this.isFabricTextEditingActive() && this.canvas) {
6873
+ finalizeActiveTextEditing(this.buildTextControllerContext(), { commit: false });
6874
+ event.preventDefault();
6875
+ return;
6876
+ }
6877
+ if (this.textSession) {
6878
+ this.exitTextMode();
6879
+ }
6880
+ else if (this.drawSession) {
6881
+ this.exitDrawMode();
6882
+ }
6883
+ else if (this.mosaicSession) {
6884
+ this.exitMosaicMode();
6885
+ }
6886
+ else if (this.cropSession) {
6887
+ this.cancelCrop();
6888
+ }
5388
6889
  }
5389
- bindElementIfExists(key, event, handler) {
5390
- var _a;
5391
- (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.bindIfExists(key, event, handler);
6890
+ finalizeActiveTextEditingIfNeeded() {
6891
+ if (!this.canvas || !this.isFabricTextEditingActive())
6892
+ return;
6893
+ finalizeActiveTextEditing(this.buildTextControllerContext(), { commit: true });
5392
6894
  }
5393
6895
  async loadImageFile(file) {
5394
6896
  const inputId = this.elements.imageInput;
@@ -5431,9 +6933,11 @@ class ImageEditor {
5431
6933
  return;
5432
6934
  if (!this.canRunIdleOperation('loadImage', options))
5433
6935
  return;
6936
+ this.finalizeActiveTextEditingIfNeeded();
5434
6937
  const callbackContext = this.getOperationContext('loadImage', options);
5435
6938
  const previousImage = this.originalImage;
5436
6939
  const hadMasks = this.getMasks().length > 0;
6940
+ const hadAnnotations = this.getAnnotations().length > 0;
5437
6941
  this.emitOptionCallback('onImageLoadStart', [callbackContext]);
5438
6942
  this.operationGuard.beginLoading();
5439
6943
  this.emitBusyChangeIfChanged(callbackContext);
@@ -5462,6 +6966,10 @@ class ImageEditor {
5462
6966
  setMaskCounter: (v) => {
5463
6967
  this.maskCounter = v;
5464
6968
  },
6969
+ getAnnotationCounter: () => this.annotationCounter,
6970
+ setAnnotationCounter: (v) => {
6971
+ this.annotationCounter = v;
6972
+ },
5465
6973
  getCurrentScale: () => this.currentScale,
5466
6974
  setCurrentScale: (v) => {
5467
6975
  this.currentScale = v;
@@ -5494,6 +7002,7 @@ class ImageEditor {
5494
7002
  this.lastMask = null;
5495
7003
  this.updateInputs();
5496
7004
  this.updateMaskList();
7005
+ this.updateAnnotationList();
5497
7006
  this.updateUi();
5498
7007
  if (previousImage && previousImage !== this.originalImage) {
5499
7008
  this.emitOptionCallback('onImageCleared', [previousImage, callbackContext]);
@@ -5505,6 +7014,9 @@ class ImageEditor {
5505
7014
  if (hadMasks) {
5506
7015
  this.emitMasksChanged(callbackContext);
5507
7016
  }
7017
+ if (hadAnnotations) {
7018
+ this.emitAnnotationsChanged(callbackContext);
7019
+ }
5508
7020
  this.emitImageChanged(callbackContext);
5509
7021
  }
5510
7022
  getInternalOperationToken(options) {
@@ -5529,15 +7041,11 @@ class ImageEditor {
5529
7041
  assertIdleForOperation(operationName, options) {
5530
7042
  const token = this.getInternalOperationToken(options);
5531
7043
  this.operationGuard.assertIdleForOperation(operationName, token);
5532
- if (this.cropSession &&
5533
- !this.operationGuard.isOwnOperation(token) &&
5534
- !CROP_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
5535
- throw new Error(`[ImageEditor] Cannot run "${operationName}" while crop mode is active.`);
5536
- }
5537
- if (this.mosaicSession &&
7044
+ const activeToolMode = this.getActiveToolMode();
7045
+ if (activeToolMode &&
5538
7046
  !this.operationGuard.isOwnOperation(token) &&
5539
- !MOSAIC_SESSION_ALLOWED_OPERATIONS.has(operationName)) {
5540
- throw new Error(`[ImageEditor] Cannot run "${operationName}" while mosaic mode is active.`);
7047
+ !TOOL_MODE_ALLOWED_OPERATIONS[activeToolMode].has(operationName)) {
7048
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while ${activeToolMode} mode is active.`);
5541
7049
  }
5542
7050
  if (this.animQueue.isBusy() && !this.canRunDuringAnimationQueue(options)) {
5543
7051
  throw new Error(`[ImageEditor] Cannot run "${operationName}" while an animation is queued.`);
@@ -5570,10 +7078,7 @@ class ImageEditor {
5570
7078
  ((_b = this.originalImage.height) !== null && _b !== void 0 ? _b : 0) > 0);
5571
7079
  }
5572
7080
  isBusy() {
5573
- return (this.operationGuard.isBusy() ||
5574
- this.animQueue.isBusy() ||
5575
- this.cropSession !== null ||
5576
- this.mosaicSession !== null);
7081
+ return this.operationGuard.isBusy() || this.animQueue.isBusy() || this.isToolModeActive();
5577
7082
  }
5578
7083
  setLayoutMode(mode) {
5579
7084
  if (!isLayoutMode(mode)) {
@@ -5648,11 +7153,35 @@ class ImageEditor {
5648
7153
  return [];
5649
7154
  return this.canvas.getObjects().filter(isMaskObject).slice();
5650
7155
  }
7156
+ getAnnotations() {
7157
+ if (!this.canvas)
7158
+ return [];
7159
+ return getAnnotations(this.canvas);
7160
+ }
5651
7161
  getMaskCollectionSignature() {
5652
7162
  return this.getMasks()
5653
7163
  .map((mask) => `${mask.maskId}:${mask.maskName}`)
5654
7164
  .join('|');
5655
7165
  }
7166
+ getAnnotationCollectionSignature() {
7167
+ return this.getAnnotations()
7168
+ .map((annotation) => `${annotation.annotationId}:${annotation.annotationName}`)
7169
+ .join('|');
7170
+ }
7171
+ getActiveToolMode() {
7172
+ if (this.cropSession)
7173
+ return 'crop';
7174
+ if (this.mosaicSession)
7175
+ return 'mosaic';
7176
+ if (this.textSession)
7177
+ return 'text';
7178
+ if (this.drawSession)
7179
+ return 'draw';
7180
+ return null;
7181
+ }
7182
+ isToolModeActive() {
7183
+ return this.getActiveToolMode() !== null;
7184
+ }
5656
7185
  getEditorState() {
5657
7186
  const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
5658
7187
  const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
@@ -5661,11 +7190,15 @@ class ImageEditor {
5661
7190
  hasImage: image !== null,
5662
7191
  image,
5663
7192
  maskCount: this.getMasks().length,
7193
+ annotationCount: this.getAnnotations().length,
5664
7194
  currentScale: this.currentScale,
5665
7195
  currentRotation: this.currentRotation,
5666
7196
  isBusy: this.isBusy(),
7197
+ activeToolMode: this.getActiveToolMode(),
5667
7198
  isCropMode: this.cropSession !== null,
5668
7199
  isMosaicMode: this.mosaicSession !== null,
7200
+ isTextMode: this.textSession !== null,
7201
+ isDrawMode: this.drawSession !== null,
5669
7202
  canUndo: this.historyManager.canUndo(),
5670
7203
  canRedo: this.historyManager.canRedo(),
5671
7204
  canvasWidth,
@@ -5678,6 +7211,9 @@ class ImageEditor {
5678
7211
  emitMasksChanged(context) {
5679
7212
  this.emitOptionCallback('onMasksChanged', [this.getMasks(), context]);
5680
7213
  }
7214
+ emitAnnotationsChanged(context) {
7215
+ this.emitOptionCallback('onAnnotationsChanged', [this.getAnnotations(), context]);
7216
+ }
5681
7217
  emitBusyChangeIfChanged(context) {
5682
7218
  const isBusy = this.isBusy();
5683
7219
  if (this.lastEmittedIsBusy === isBusy)
@@ -5686,11 +7222,20 @@ class ImageEditor {
5686
7222
  this.emitOptionCallback('onBusyChange', [isBusy, context]);
5687
7223
  }
5688
7224
  buildSelection(selected) {
5689
- var _a;
7225
+ var _a, _b;
5690
7226
  const selectedMasks = selected.filter(isMaskObject);
7227
+ const selectedAnnotations = selected.filter(isAnnotationObject);
7228
+ const selectedObjectKind = selectedMasks.length === 1 && selectedAnnotations.length === 0
7229
+ ? 'mask'
7230
+ : selectedAnnotations.length === 1 && selectedMasks.length === 0
7231
+ ? 'annotation'
7232
+ : null;
5691
7233
  return {
5692
7234
  selectedMask: (_a = selectedMasks[0]) !== null && _a !== void 0 ? _a : null,
5693
7235
  selectedMasks,
7236
+ selectedAnnotation: (_b = selectedAnnotations[0]) !== null && _b !== void 0 ? _b : null,
7237
+ selectedAnnotations,
7238
+ selectedObjectKind,
5694
7239
  };
5695
7240
  }
5696
7241
  withSelectionChangeContext(context, callback) {
@@ -5729,7 +7274,7 @@ class ImageEditor {
5729
7274
  applyCanvasDimensions(this.canvas, widthPx, heightPx, this.containerElement);
5730
7275
  }
5731
7276
  alignObjectBoundingBoxToCanvasTopLeft(object) {
5732
- var _a, _b;
7277
+ var _a, _b, _c;
5733
7278
  object.setCoords();
5734
7279
  const boundingRect = object.getBoundingRect();
5735
7280
  object.set({
@@ -5737,7 +7282,7 @@ class ImageEditor {
5737
7282
  top: ((_b = object.top) !== null && _b !== void 0 ? _b : 0) - boundingRect.top,
5738
7283
  });
5739
7284
  object.setCoords();
5740
- this.canvas.renderAll();
7285
+ (_c = this.canvas) === null || _c === void 0 ? void 0 : _c.renderAll();
5741
7286
  }
5742
7287
  measureLayoutViewport(scrollbarSize) {
5743
7288
  return this.viewportCache.measure(this.containerElement, {
@@ -5745,7 +7290,13 @@ class ImageEditor {
5745
7290
  height: this.options.canvasHeight,
5746
7291
  }, scrollbarSize);
5747
7292
  }
5748
- updateCanvasSizeToImageBounds() {
7293
+ getScrollbarStableViewportCanvasSize(viewport) {
7294
+ return {
7295
+ width: Math.max(1, viewport.width - 1),
7296
+ height: Math.max(1, viewport.height - 1),
7297
+ };
7298
+ }
7299
+ updateCanvasSizeToImageBounds(options = {}) {
5749
7300
  var _a, _b;
5750
7301
  if (!this.originalImage)
5751
7302
  return;
@@ -5753,13 +7304,26 @@ class ImageEditor {
5753
7304
  const boundingRect = this.originalImage.getBoundingRect();
5754
7305
  const scrollbarSize = measureScrollbarSize((_b = (_a = this.containerElement) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : null);
5755
7306
  const viewport = this.measureLayoutViewport(scrollbarSize);
7307
+ const shouldStabilizeContainedViewport = options.stabilizeContainedViewport !== false;
7308
+ const imageFitsViewport = boundingRect.width <= viewport.width + LAYOUT_EPSILON &&
7309
+ boundingRect.height <= viewport.height + LAYOUT_EPSILON;
5756
7310
  if (this.currentLayoutMode === 'fit' || this.currentLayoutMode === 'cover') {
7311
+ if (imageFitsViewport) {
7312
+ const canvasSize = shouldStabilizeContainedViewport
7313
+ ? this.getScrollbarStableViewportCanvasSize(viewport)
7314
+ : viewport;
7315
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
7316
+ return;
7317
+ }
5757
7318
  const canvasSize = computeScrollableCanvasSize(boundingRect.width, boundingRect.height, viewport, scrollbarSize);
5758
7319
  this.setCanvasSizePx(canvasSize.width, canvasSize.height);
5759
7320
  return;
5760
7321
  }
5761
- if (boundingRect.width <= viewport.width && boundingRect.height <= viewport.height) {
5762
- this.setCanvasSizePx(viewport.width, viewport.height);
7322
+ if (imageFitsViewport) {
7323
+ const canvasSize = shouldStabilizeContainedViewport
7324
+ ? this.getScrollbarStableViewportCanvasSize(viewport)
7325
+ : viewport;
7326
+ this.setCanvasSizePx(canvasSize.width, canvasSize.height);
5763
7327
  return;
5764
7328
  }
5765
7329
  this.setCanvasSizePx(Math.max(viewport.width, Math.ceil(boundingRect.width)), Math.max(viewport.height, Math.ceil(boundingRect.height)));
@@ -5859,7 +7423,7 @@ class ImageEditor {
5859
7423
  }
5860
7424
  buildTransformContext() {
5861
7425
  return {
5862
- canvas: this.canvas,
7426
+ canvas: this.getLiveCanvasOrThrow('buildTransformContext'),
5863
7427
  options: this.options,
5864
7428
  guard: this.operationGuard,
5865
7429
  getOriginalImage: () => this.originalImage,
@@ -6011,6 +7575,7 @@ class ImageEditor {
6011
7575
  const context = this.buildCallbackContext(activeRestoreOperation !== null && activeRestoreOperation !== void 0 ? activeRestoreOperation : 'loadFromState', activeRestoreOperation === 'undo' || activeRestoreOperation === 'redo');
6012
7576
  const previousImage = this.originalImage;
6013
7577
  const previousMaskSignature = this.getMaskCollectionSignature();
7578
+ const previousAnnotationSignature = this.getAnnotationCollectionSignature();
6014
7579
  try {
6015
7580
  const restoredState = await loadFromState({
6016
7581
  canvas: this.canvas,
@@ -6033,6 +7598,7 @@ class ImageEditor {
6033
7598
  this.canvas.sendObjectToBack(this.originalImage);
6034
7599
  }
6035
7600
  this.maskCounter = restoredState.maxMaskId;
7601
+ this.annotationCounter = restoredState.maxAnnotationId;
6036
7602
  const editorState = restoredState.editorState;
6037
7603
  if (editorState) {
6038
7604
  this.currentScale = editorState.currentScale;
@@ -6050,22 +7616,25 @@ class ImageEditor {
6050
7616
  }
6051
7617
  this.isImageLoadedToCanvas = !!this.originalImage;
6052
7618
  if (this.originalImage && this.shouldNormalizeCanvasSizeAfterStateRestore()) {
6053
- this.updateCanvasSizeToImageBounds();
7619
+ this.updateCanvasSizeToImageBounds({ stabilizeContainedViewport: false });
6054
7620
  this.alignObjectBoundingBoxToCanvasTopLeft(this.originalImage);
6055
7621
  }
6056
7622
  if (this.originalImage) {
6057
7623
  this.settleFitCoverScrollbarsAfterStateRestore();
6058
7624
  }
6059
- const restoredMasks = restoredState.objects.filter(isMaskObject);
7625
+ const restoredMasks = restoredState.masks;
6060
7626
  this.lastMask = restoredMasks.reduce((lastMask, maskObject) => !lastMask || maskObject.maskId > lastMask.maskId ? maskObject : lastMask, null);
6061
7627
  restoredMasks.forEach((maskObject) => {
6062
7628
  applyMaskUnselectedStyle(maskObject);
6063
7629
  reattachMaskHoverHandlers(maskObject);
6064
7630
  });
7631
+ syncAnnotationRuntimeStates(restoredState.annotations);
7632
+ attachTextEditingHandlersToAnnotations(this.buildTextControllerContext(), restoredState.annotations);
6065
7633
  this.lastSnapshot = this.captureSnapshotInternal();
6066
7634
  this.canvas.renderAll();
6067
7635
  this.updateInputs();
6068
7636
  this.updateMaskList();
7637
+ this.updateAnnotationList();
6069
7638
  this.updateUi();
6070
7639
  if (previousImage && previousImage !== this.originalImage) {
6071
7640
  this.emitOptionCallback('onImageCleared', [previousImage, context]);
@@ -6073,17 +7642,32 @@ class ImageEditor {
6073
7642
  if (previousMaskSignature !== this.getMaskCollectionSignature()) {
6074
7643
  this.emitMasksChanged(context);
6075
7644
  }
7645
+ if (previousAnnotationSignature !== this.getAnnotationCollectionSignature()) {
7646
+ this.emitAnnotationsChanged(context);
7647
+ }
6076
7648
  this.emitImageChanged(context);
7649
+ const canvas = this.getLiveCanvasOrThrow('loadFromState');
6077
7650
  const activeMaskId = editorState === null || editorState === void 0 ? void 0 : editorState.activeMaskId;
6078
- if (typeof activeMaskId === 'number') {
7651
+ const activeAnnotationId = editorState === null || editorState === void 0 ? void 0 : editorState.activeAnnotationId;
7652
+ if ((editorState === null || editorState === void 0 ? void 0 : editorState.activeObjectKind) === 'mask' && typeof activeMaskId === 'number') {
6079
7653
  const activeMask = restoredMasks.find((maskObject) => maskObject.maskId === activeMaskId);
6080
7654
  if (activeMask) {
6081
7655
  this.withSelectionChangeContext(context, () => {
6082
- this.canvas.setActiveObject(activeMask);
7656
+ canvas.setActiveObject(activeMask);
6083
7657
  this.handleSelectionChanged([activeMask]);
6084
7658
  });
6085
7659
  }
6086
7660
  }
7661
+ else if ((editorState === null || editorState === void 0 ? void 0 : editorState.activeObjectKind) === 'annotation' &&
7662
+ typeof activeAnnotationId === 'number') {
7663
+ const activeAnnotation = restoredState.annotations.find((annotation) => annotation.annotationId === activeAnnotationId);
7664
+ if (activeAnnotation) {
7665
+ this.withSelectionChangeContext(context, () => {
7666
+ canvas.setActiveObject(activeAnnotation);
7667
+ this.handleSelectionChanged([activeAnnotation]);
7668
+ });
7669
+ }
7670
+ }
6087
7671
  }
6088
7672
  catch (error) {
6089
7673
  reportError(this.options, error, 'Failed to restore canvas state.');
@@ -6094,24 +7678,26 @@ class ImageEditor {
6094
7678
  this.saveStateInternal();
6095
7679
  }
6096
7680
  saveStateInternal(options) {
6097
- var _a, _b;
7681
+ var _a, _b, _c;
6098
7682
  if (!this.canvas || this.shouldSuppressSaveState)
6099
7683
  return;
6100
7684
  if (!this.canRunIdleOperation('saveState', options))
6101
7685
  return;
6102
7686
  const activeObj = this.canvas.getActiveObject();
6103
7687
  const activeMask = this.getActiveMaskForSnapshot();
7688
+ const activeAnnotation = this.getActiveAnnotationForSnapshot();
6104
7689
  this.hideAllMaskLabels();
6105
7690
  try {
6106
7691
  const after = saveState({
6107
7692
  canvas: this.canvas,
6108
7693
  activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
7694
+ activeAnnotationId: (_b = activeAnnotation === null || activeAnnotation === void 0 ? void 0 : activeAnnotation.annotationId) !== null && _b !== void 0 ? _b : null,
6109
7695
  currentScale: this.currentScale,
6110
7696
  currentRotation: this.currentRotation,
6111
7697
  baseImageScale: this.baseImageScale,
6112
7698
  currentImageMimeType: this.currentImageMimeType,
6113
7699
  });
6114
- const before = (_b = this.lastSnapshot) !== null && _b !== void 0 ? _b : after;
7700
+ const before = (_c = this.lastSnapshot) !== null && _c !== void 0 ? _c : after;
6115
7701
  if (after === before) {
6116
7702
  return;
6117
7703
  }
@@ -6127,25 +7713,32 @@ class ImageEditor {
6127
7713
  reportWarning(this.options, error, 'Failed to capture canvas snapshot.');
6128
7714
  }
6129
7715
  finally {
6130
- this.restoreActiveMaskAfterSnapshot(activeObj, activeMask);
7716
+ this.restoreActiveObjectAfterSnapshot(activeObj, activeMask, activeAnnotation);
6131
7717
  this.updateUi();
6132
7718
  }
6133
7719
  }
6134
- restoreActiveMaskAfterSnapshot(activeObj, activeMask) {
7720
+ restoreActiveObjectAfterSnapshot(activeObj, activeMask, activeAnnotation) {
6135
7721
  if (!this.canvas)
6136
7722
  return;
6137
7723
  const maskToRestore = activeObj && isMaskObject(activeObj) ? activeObj : activeMask;
6138
- if (!maskToRestore || !this.canvas.getObjects().includes(maskToRestore))
7724
+ const annotationToRestore = activeObj && isAnnotationObject(activeObj) ? activeObj : activeAnnotation;
7725
+ if (maskToRestore && this.canvas.getObjects().includes(maskToRestore)) {
7726
+ this.canvas.setActiveObject(maskToRestore);
7727
+ this.showLabelForMask(maskToRestore);
7728
+ this.updateMaskListSelection(maskToRestore);
6139
7729
  return;
6140
- this.canvas.setActiveObject(maskToRestore);
6141
- this.showLabelForMask(maskToRestore);
6142
- this.updateMaskListSelection(maskToRestore);
7730
+ }
7731
+ if (annotationToRestore && this.canvas.getObjects().includes(annotationToRestore)) {
7732
+ this.canvas.setActiveObject(annotationToRestore);
7733
+ this.updateAnnotationListSelection(annotationToRestore);
7734
+ }
6143
7735
  }
6144
7736
  undo() {
6145
7737
  if (this.isDisposed)
6146
7738
  return Promise.resolve();
6147
7739
  if (!this.canRunIdleOperation('undo'))
6148
7740
  return Promise.resolve();
7741
+ this.finalizeActiveTextEditingIfNeeded();
6149
7742
  const context = this.buildCallbackContext('undo', true);
6150
7743
  const job = this.animQueue.add(async () => {
6151
7744
  if (this.isDisposed)
@@ -6169,6 +7762,7 @@ class ImageEditor {
6169
7762
  return Promise.resolve();
6170
7763
  if (!this.canRunIdleOperation('redo'))
6171
7764
  return Promise.resolve();
7765
+ this.finalizeActiveTextEditingIfNeeded();
6172
7766
  const context = this.buildCallbackContext('redo', true);
6173
7767
  const job = this.animQueue.add(async () => {
6174
7768
  if (this.isDisposed)
@@ -6204,153 +7798,593 @@ class ImageEditor {
6204
7798
  removeSelectedMask() {
6205
7799
  if (!this.canvas)
6206
7800
  return;
6207
- if (!this.canRunIdleOperation('removeSelectedMask'))
7801
+ if (!this.canRunIdleOperation('removeSelectedMask'))
7802
+ return;
7803
+ const before = this.getMasks().length;
7804
+ const callbackContext = this.buildCallbackContext('removeSelectedMask', false);
7805
+ const removeMaskContext = this.buildRemoveMaskContext();
7806
+ this.withSelectionChangeContext(callbackContext, () => removeSelectedMask(removeMaskContext));
7807
+ this.updateUi();
7808
+ if (this.getMasks().length !== before) {
7809
+ this.emitMasksChanged(callbackContext);
7810
+ this.emitImageChanged(callbackContext);
7811
+ }
7812
+ }
7813
+ removeAllMasks(options = {}) {
7814
+ if (!this.canvas)
7815
+ return;
7816
+ if (!this.canRunIdleOperation('removeAllMasks', options))
7817
+ return;
7818
+ const before = this.getMasks().length;
7819
+ const callbackContext = this.buildCallbackContext('removeAllMasks', false);
7820
+ const removeMaskContext = this.buildRemoveMaskContext();
7821
+ this.withSelectionChangeContext(callbackContext, () => removeAllMasks(removeMaskContext, options));
7822
+ this.updateUi();
7823
+ if (this.getMasks().length !== before) {
7824
+ this.emitMasksChanged(callbackContext);
7825
+ this.emitImageChanged(callbackContext);
7826
+ }
7827
+ }
7828
+ buildCreateMaskContext() {
7829
+ return {
7830
+ fabric: this.fabricModule,
7831
+ canvas: this.getLiveCanvasOrThrow('createMask'),
7832
+ options: this.getRuntimeOptions(),
7833
+ getLastMask: () => this.lastMask,
7834
+ setLastMask: (maskObject) => {
7835
+ this.lastMask = maskObject;
7836
+ },
7837
+ getMaskCounter: () => this.maskCounter,
7838
+ setMaskCounter: (n) => {
7839
+ this.maskCounter = n;
7840
+ },
7841
+ updateMaskList: () => {
7842
+ this.updateMaskList();
7843
+ },
7844
+ saveCanvasState: () => {
7845
+ this.saveState();
7846
+ },
7847
+ expandCanvasIfNeeded: (widthPx, heightPx) => {
7848
+ this.setCanvasSizePx(widthPx, heightPx);
7849
+ },
7850
+ };
7851
+ }
7852
+ buildRemoveMaskContext() {
7853
+ return {
7854
+ canvas: this.getLiveCanvasOrThrow('removeMask'),
7855
+ removeLabelForMask: (mask) => {
7856
+ this.removeLabelForMask(mask);
7857
+ },
7858
+ updateMaskList: () => {
7859
+ this.updateMaskList();
7860
+ },
7861
+ saveCanvasState: () => {
7862
+ this.saveState();
7863
+ },
7864
+ setLastMask: (maskObject) => {
7865
+ this.lastMask = maskObject;
7866
+ },
7867
+ };
7868
+ }
7869
+ buildMaskLabelContext() {
7870
+ if (!this.canvas)
7871
+ return null;
7872
+ return { fabric: this.fabricModule, canvas: this.canvas, options: this.options };
7873
+ }
7874
+ removeLabelForMask(mask) {
7875
+ const context = this.buildMaskLabelContext();
7876
+ if (!context)
7877
+ return;
7878
+ removeLabelForMask(context, mask);
7879
+ }
7880
+ createLabelForMask(mask) {
7881
+ const context = this.buildMaskLabelContext();
7882
+ if (!context)
7883
+ return;
7884
+ createLabelForMask(context, mask);
7885
+ }
7886
+ hideAllMaskLabels() {
7887
+ const context = this.buildMaskLabelContext();
7888
+ if (!context)
7889
+ return;
7890
+ hideAllMaskLabels(context);
7891
+ }
7892
+ syncMaskLabel(mask) {
7893
+ const context = this.buildMaskLabelContext();
7894
+ if (!context)
7895
+ return;
7896
+ syncMaskLabel(context, mask);
7897
+ }
7898
+ showLabelForMask(mask) {
7899
+ const context = this.buildMaskLabelContext();
7900
+ if (!context)
7901
+ return;
7902
+ showLabelForMask(context, mask);
7903
+ }
7904
+ handleObjectMovingScalingRotating(target) {
7905
+ if (isMaskObject(target)) {
7906
+ this.syncMaskLabel(target);
7907
+ }
7908
+ }
7909
+ handleObjectModified(target) {
7910
+ if (isMaskObject(target)) {
7911
+ this.syncMaskLabel(target);
7912
+ const context = this.buildCallbackContext('saveState', false);
7913
+ this.saveState();
7914
+ this.emitMasksChanged(context);
7915
+ this.emitImageChanged(context);
7916
+ return;
7917
+ }
7918
+ if (isAnnotationObject(target)) {
7919
+ if (isAnnotationLocked(target))
7920
+ return;
7921
+ const context = this.buildCallbackContext('updateAnnotation', false);
7922
+ this.saveState();
7923
+ this.emitAnnotationsChanged(context);
7924
+ this.emitImageChanged(context);
7925
+ }
7926
+ }
7927
+ handleSelectionChanged(selected) {
7928
+ var _a, _b, _c, _d;
7929
+ if (!this.canvas)
7930
+ return;
7931
+ const selectedMask = (_a = selected.find(isMaskObject)) !== null && _a !== void 0 ? _a : null;
7932
+ const selectedAnnotation = (_b = selected.find(isAnnotationObject)) !== null && _b !== void 0 ? _b : null;
7933
+ const masks = this.canvas.getObjects().filter(isMaskObject);
7934
+ masks.forEach((maskObject) => {
7935
+ if (maskObject !== selectedMask) {
7936
+ if (maskObject.labelObject) {
7937
+ this.removeLabelForMask(maskObject);
7938
+ }
7939
+ applyMaskUnselectedStyle(maskObject);
7940
+ }
7941
+ else {
7942
+ applyMaskSelectedStyle(maskObject);
7943
+ }
7944
+ });
7945
+ if (selectedMask)
7946
+ this.showLabelForMask(selectedMask);
7947
+ this.updateMaskListSelection(selectedMask);
7948
+ this.updateAnnotationListSelection(selectedAnnotation);
7949
+ this.canvas.requestRenderAll();
7950
+ this.updateUi();
7951
+ const context = (_c = this.nextSelectionChangeContext) !== null && _c !== void 0 ? _c : this.buildCallbackContext((_d = this.activeStateRestoreOperation) !== null && _d !== void 0 ? _d : 'createMask', this.activeStateRestoreOperation === 'undo' ||
7952
+ this.activeStateRestoreOperation === 'redo');
7953
+ this.emitOptionCallback('onSelectionChange', [this.buildSelection(selected), context]);
7954
+ }
7955
+ buildMaskListContext() {
7956
+ return {
7957
+ canvas: this.canvas,
7958
+ getListElementId: () => this.elements.maskList,
7959
+ onMaskSelected: (mask) => this.handleSelectionChanged([mask]),
7960
+ };
7961
+ }
7962
+ updateMaskList() {
7963
+ renderMaskList(this.buildMaskListContext());
7964
+ }
7965
+ updateMaskListSelection(selectedMask) {
7966
+ updateMaskListSelection(this.buildMaskListContext(), selectedMask);
7967
+ }
7968
+ enterTextMode() {
7969
+ if (!this.canvas)
7970
+ return;
7971
+ if (!this.canRunIdleOperation('enterTextMode'))
7972
+ return;
7973
+ if (this.isToolModeActive())
7974
+ return;
7975
+ enterTextMode(this.buildTextControllerContext());
7976
+ const callbackContext = this.buildCallbackContext('enterTextMode', false);
7977
+ this.emitBusyChangeIfChanged(callbackContext);
7978
+ this.emitImageChanged(callbackContext);
7979
+ }
7980
+ exitTextMode() {
7981
+ if (!this.canvas || !this.textSession)
7982
+ return;
7983
+ if (!this.canRunIdleOperation('exitTextMode'))
7984
+ return;
7985
+ exitTextMode(this.buildTextControllerContext());
7986
+ const callbackContext = this.buildCallbackContext('exitTextMode', false);
7987
+ this.emitBusyChangeIfChanged(callbackContext);
7988
+ this.emitImageChanged(callbackContext);
7989
+ }
7990
+ isTextMode() {
7991
+ return this.textSession !== null;
7992
+ }
7993
+ createTextAnnotation(config = {}) {
7994
+ if (!this.canvas)
7995
+ return null;
7996
+ if (!this.canRunIdleOperation('createTextAnnotation'))
7997
+ return null;
7998
+ return createTextAnnotation(this.buildTextControllerContext(), config);
7999
+ }
8000
+ enterDrawMode() {
8001
+ if (!this.canvas)
8002
+ return;
8003
+ if (!this.canRunIdleOperation('enterDrawMode'))
8004
+ return;
8005
+ if (this.isToolModeActive())
8006
+ return;
8007
+ enterDrawMode(this.buildDrawControllerContext());
8008
+ const callbackContext = this.buildCallbackContext('enterDrawMode', false);
8009
+ this.emitBusyChangeIfChanged(callbackContext);
8010
+ this.emitImageChanged(callbackContext);
8011
+ }
8012
+ exitDrawMode() {
8013
+ if (!this.canvas || !this.drawSession)
8014
+ return;
8015
+ if (!this.canRunIdleOperation('exitDrawMode'))
8016
+ return;
8017
+ exitDrawMode(this.buildDrawControllerContext());
8018
+ const callbackContext = this.buildCallbackContext('exitDrawMode', false);
8019
+ this.emitBusyChangeIfChanged(callbackContext);
8020
+ this.emitImageChanged(callbackContext);
8021
+ }
8022
+ isDrawMode() {
8023
+ return this.drawSession !== null;
8024
+ }
8025
+ getTextConfig() {
8026
+ return cloneResolvedTextAnnotationConfig(this.currentTextConfig);
8027
+ }
8028
+ setTextConfig(config) {
8029
+ this.applyTextConfigPatch(config, 'setTextConfig');
8030
+ }
8031
+ resetTextConfig() {
8032
+ this.applyTextConfigPatch(this.defaultTextConfig, 'resetTextConfig');
8033
+ }
8034
+ setTextColor(color) {
8035
+ this.applyTextConfigPatch({ fill: color }, 'setTextColor');
8036
+ }
8037
+ setTextFontSize(size) {
8038
+ this.applyTextConfigPatch({ fontSize: size }, 'setTextFontSize');
8039
+ }
8040
+ getDrawConfig() {
8041
+ return cloneResolvedDrawConfig(this.currentDrawConfig);
8042
+ }
8043
+ setDrawConfig(config) {
8044
+ this.applyDrawConfigPatch(config, 'setDrawConfig');
8045
+ }
8046
+ resetDrawConfig() {
8047
+ this.applyDrawConfigPatch(this.defaultDrawConfig, 'resetDrawConfig');
8048
+ }
8049
+ setDrawColor(color) {
8050
+ this.applyDrawConfigPatch({ color }, 'setDrawColor');
8051
+ }
8052
+ setDrawBrushSize(size) {
8053
+ this.applyDrawConfigPatch({ brushSize: size }, 'setDrawBrushSize');
8054
+ }
8055
+ removeSelectedAnnotation() {
8056
+ if (!this.canvas)
8057
+ return;
8058
+ if (!this.canRunIdleOperation('removeSelectedAnnotation'))
8059
+ return;
8060
+ const before = this.getAnnotations().length;
8061
+ const callbackContext = this.buildCallbackContext('removeSelectedAnnotation', false);
8062
+ this.withSelectionChangeContext(callbackContext, () => {
8063
+ removeSelectedAnnotation(this.buildAnnotationManagerContext());
8064
+ });
8065
+ this.updateAnnotationList();
8066
+ this.updateUi();
8067
+ if (this.getAnnotations().length !== before) {
8068
+ this.emitAnnotationsChanged(callbackContext);
8069
+ this.emitImageChanged(callbackContext);
8070
+ }
8071
+ }
8072
+ removeAllAnnotations(options = {}) {
8073
+ if (!this.canvas)
8074
+ return;
8075
+ if (!this.canRunIdleOperation('removeAllAnnotations', options))
8076
+ return;
8077
+ const before = this.getAnnotations().length;
8078
+ const callbackContext = this.buildCallbackContext('removeAllAnnotations', false);
8079
+ this.withSelectionChangeContext(callbackContext, () => {
8080
+ removeAllAnnotations(this.buildAnnotationManagerContext(), options);
8081
+ });
8082
+ this.updateAnnotationList();
8083
+ this.updateUi();
8084
+ if (this.getAnnotations().length !== before) {
8085
+ this.emitAnnotationsChanged(callbackContext);
8086
+ this.emitImageChanged(callbackContext);
8087
+ }
8088
+ }
8089
+ updateAnnotation(annotationId, config) {
8090
+ if (!this.canvas)
8091
+ return;
8092
+ if (!this.canRunIdleOperation('updateAnnotation'))
8093
+ return;
8094
+ const callbackContext = this.buildCallbackContext('updateAnnotation', false);
8095
+ const changed = updateAnnotation(this.buildAnnotationManagerContext(), annotationId, config);
8096
+ if (changed) {
8097
+ this.updateAnnotationList();
8098
+ this.emitAnnotationsChanged(callbackContext);
8099
+ this.emitImageChanged(callbackContext);
8100
+ }
8101
+ }
8102
+ updateSelectedAnnotation(config) {
8103
+ if (!this.canvas)
8104
+ return;
8105
+ if (!this.canRunIdleOperation('updateSelectedAnnotation'))
6208
8106
  return;
6209
- const before = this.getMasks().length;
6210
- const callbackContext = this.buildCallbackContext('removeSelectedMask', false);
6211
- const removeMaskContext = this.buildRemoveMaskContext();
6212
- this.withSelectionChangeContext(callbackContext, () => removeSelectedMask(removeMaskContext));
6213
- this.updateUi();
6214
- if (this.getMasks().length !== before) {
6215
- this.emitMasksChanged(callbackContext);
8107
+ const callbackContext = this.buildCallbackContext('updateSelectedAnnotation', false);
8108
+ const changed = updateSelectedAnnotation(this.buildAnnotationManagerContext(), config);
8109
+ if (changed) {
8110
+ this.updateAnnotationList();
8111
+ this.emitAnnotationsChanged(callbackContext);
6216
8112
  this.emitImageChanged(callbackContext);
6217
8113
  }
6218
8114
  }
6219
- removeAllMasks(options = {}) {
8115
+ deleteSelectedObject() {
6220
8116
  if (!this.canvas)
6221
8117
  return;
6222
- if (!this.canRunIdleOperation('removeAllMasks', options))
8118
+ if (!this.canRunIdleOperation('deleteSelectedObject'))
6223
8119
  return;
6224
- const before = this.getMasks().length;
6225
- const callbackContext = this.buildCallbackContext('removeAllMasks', false);
6226
- const removeMaskContext = this.buildRemoveMaskContext();
6227
- this.withSelectionChangeContext(callbackContext, () => removeAllMasks(removeMaskContext, options));
8120
+ this.finalizeActiveTextEditingIfNeeded();
8121
+ const selectedObjects = this.getSelectedCanvasObjects();
8122
+ const selectedMasks = selectedObjects.filter(isMaskObject);
8123
+ const selectedAnnotations = selectedObjects.filter((object) => isAnnotationObject(object) && isAnnotationUnlocked(object));
8124
+ if (selectedMasks.length === 0 && selectedAnnotations.length === 0)
8125
+ return;
8126
+ const canvas = this.getLiveCanvasOrThrow('deleteSelectedObject');
8127
+ const callbackContext = this.buildCallbackContext('deleteSelectedObject', false);
8128
+ this.withSelectionChangeContext(callbackContext, () => {
8129
+ for (const mask of selectedMasks) {
8130
+ this.removeLabelForMask(mask);
8131
+ canvas.remove(mask);
8132
+ }
8133
+ removeAnnotationObjects(this.buildAnnotationManagerContext(), selectedAnnotations, {
8134
+ saveHistory: false,
8135
+ force: true,
8136
+ });
8137
+ canvas.discardActiveObject();
8138
+ canvas.renderAll();
8139
+ this.saveState();
8140
+ });
8141
+ this.updateMaskList();
8142
+ this.updateAnnotationList();
6228
8143
  this.updateUi();
6229
- if (this.getMasks().length !== before) {
8144
+ if (selectedMasks.length > 0)
6230
8145
  this.emitMasksChanged(callbackContext);
6231
- this.emitImageChanged(callbackContext);
6232
- }
8146
+ if (selectedAnnotations.length > 0)
8147
+ this.emitAnnotationsChanged(callbackContext);
8148
+ this.emitImageChanged(callbackContext);
6233
8149
  }
6234
- buildCreateMaskContext() {
8150
+ bringSelectedObjectForward() {
8151
+ this.moveSelectedEditableObject('bringSelectedObjectForward');
8152
+ }
8153
+ sendSelectedObjectBackward() {
8154
+ this.moveSelectedEditableObject('sendSelectedObjectBackward');
8155
+ }
8156
+ bringSelectedObjectToFront() {
8157
+ this.moveSelectedEditableObject('bringSelectedObjectToFront');
8158
+ }
8159
+ sendSelectedObjectToBack() {
8160
+ this.moveSelectedEditableObject('sendSelectedObjectToBack');
8161
+ }
8162
+ buildAnnotationManagerContext() {
6235
8163
  return {
6236
- fabric: this.fabricModule,
6237
- canvas: this.canvas,
6238
- options: this.getRuntimeOptions(),
6239
- getLastMask: () => this.lastMask,
6240
- setLastMask: (maskObject) => {
6241
- this.lastMask = maskObject;
6242
- },
6243
- getMaskCounter: () => this.maskCounter,
6244
- setMaskCounter: (n) => {
6245
- this.maskCounter = n;
6246
- },
6247
- updateMaskList: () => {
6248
- this.updateMaskList();
6249
- },
6250
- saveCanvasState: () => {
6251
- this.saveState();
6252
- },
6253
- expandCanvasIfNeeded: (widthPx, heightPx) => {
6254
- this.setCanvasSizePx(widthPx, heightPx);
6255
- },
8164
+ canvas: this.getLiveCanvasOrThrow('annotationManager'),
8165
+ saveCanvasState: () => this.saveState(),
8166
+ updateUi: () => this.updateUi(),
6256
8167
  };
6257
8168
  }
6258
- buildRemoveMaskContext() {
8169
+ buildAnnotationListContext() {
6259
8170
  return {
6260
8171
  canvas: this.canvas,
6261
- removeLabelForMask: (mask) => {
6262
- this.removeLabelForMask(mask);
8172
+ getListElementId: () => this.elements.annotationList,
8173
+ onAnnotationSelected: (annotation) => this.handleSelectionChanged([annotation]),
8174
+ };
8175
+ }
8176
+ updateAnnotationList() {
8177
+ renderAnnotationList(this.buildAnnotationListContext());
8178
+ }
8179
+ updateAnnotationListSelection(selectedAnnotation) {
8180
+ updateAnnotationListSelection(this.buildAnnotationListContext(), selectedAnnotation);
8181
+ }
8182
+ buildTextControllerContext() {
8183
+ return {
8184
+ fabric: this.fabricModule,
8185
+ canvas: this.getLiveCanvasOrThrow('textController'),
8186
+ options: this.options,
8187
+ getOriginalImage: () => this.originalImage,
8188
+ getTextConfig: () => this.currentTextConfig,
8189
+ isImageLoaded: () => this.isImageLoaded(),
8190
+ getAnnotationCounter: () => this.annotationCounter,
8191
+ setAnnotationCounter: (value) => {
8192
+ this.annotationCounter = value;
6263
8193
  },
6264
- updateMaskList: () => {
6265
- this.updateMaskList();
8194
+ getTextSession: () => this.textSession,
8195
+ setTextSession: (session) => {
8196
+ this.textSession = session;
6266
8197
  },
6267
- saveCanvasState: () => {
6268
- this.saveState();
8198
+ saveCanvasState: () => this.saveState(),
8199
+ updateAnnotationList: () => this.updateAnnotationList(),
8200
+ updateUi: () => this.updateUi(),
8201
+ emitAnnotationsChanged: (context) => this.emitAnnotationsChanged(context),
8202
+ emitImageChanged: (context) => this.emitImageChanged(context),
8203
+ buildCallbackContext: (operation) => this.buildCallbackContext(operation, false),
8204
+ };
8205
+ }
8206
+ buildDrawControllerContext() {
8207
+ return {
8208
+ fabric: this.fabricModule,
8209
+ canvas: this.getLiveCanvasOrThrow('drawController'),
8210
+ options: this.options,
8211
+ getDrawConfig: () => this.currentDrawConfig,
8212
+ isImageLoaded: () => this.isImageLoaded(),
8213
+ getAnnotationCounter: () => this.annotationCounter,
8214
+ setAnnotationCounter: (value) => {
8215
+ this.annotationCounter = value;
6269
8216
  },
6270
- setLastMask: (maskObject) => {
6271
- this.lastMask = maskObject;
8217
+ getDrawSession: () => this.drawSession,
8218
+ setDrawSession: (session) => {
8219
+ this.drawSession = session;
6272
8220
  },
8221
+ saveCanvasState: () => this.saveState(),
8222
+ updateAnnotationList: () => this.updateAnnotationList(),
8223
+ updateUi: () => this.updateUi(),
8224
+ emitAnnotationsChanged: (context) => this.emitAnnotationsChanged(context),
8225
+ emitImageChanged: (context) => this.emitImageChanged(context),
8226
+ buildCallbackContext: (operation) => this.buildCallbackContext(operation, false),
6273
8227
  };
6274
8228
  }
6275
- buildMaskLabelContext() {
6276
- if (!this.canvas)
6277
- return null;
6278
- return { fabric: this.fabricModule, canvas: this.canvas, options: this.options };
8229
+ applyTextConfigPatch(config, operation) {
8230
+ if (!this.canRunIdleOperation(operation))
8231
+ return;
8232
+ const invalidFields = getInvalidTextAnnotationConfigFields(config);
8233
+ if (invalidFields.length > 0) {
8234
+ reportWarning(this.options, null, `${operation} ignored invalid Text config fields: ${invalidFields.join(', ')}.`);
8235
+ }
8236
+ const next = mergeTextAnnotationConfigPatch(this.currentTextConfig, config, this.defaultTextConfig);
8237
+ if (areResolvedTextAnnotationConfigsEqual(this.currentTextConfig, next))
8238
+ return;
8239
+ this.currentTextConfig = next;
8240
+ this.updateInputs();
8241
+ this.updateUi();
8242
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
6279
8243
  }
6280
- removeLabelForMask(mask) {
6281
- const context = this.buildMaskLabelContext();
6282
- if (!context)
8244
+ applyDrawConfigPatch(config, operation) {
8245
+ if (!this.canRunIdleOperation(operation))
6283
8246
  return;
6284
- removeLabelForMask(context, mask);
8247
+ const invalidFields = getInvalidDrawConfigFields(config);
8248
+ if (invalidFields.length > 0) {
8249
+ reportWarning(this.options, null, `${operation} ignored invalid Draw config fields: ${invalidFields.join(', ')}.`);
8250
+ }
8251
+ const next = mergeDrawConfigPatch(this.currentDrawConfig, config, this.defaultDrawConfig);
8252
+ if (areResolvedDrawConfigsEqual(this.currentDrawConfig, next))
8253
+ return;
8254
+ this.currentDrawConfig = next;
8255
+ updateDrawBrush(this.buildDrawControllerContext());
8256
+ this.updateInputs();
8257
+ this.updateUi();
8258
+ this.emitImageChanged(this.buildCallbackContext(operation, false));
6285
8259
  }
6286
- createLabelForMask(mask) {
6287
- const context = this.buildMaskLabelContext();
6288
- if (!context)
8260
+ applyTextColorInput(color) {
8261
+ var _a;
8262
+ if (this.isTextMode()) {
8263
+ this.setTextColor(color);
6289
8264
  return;
6290
- createLabelForMask(context, mask);
8265
+ }
8266
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
8267
+ if (selected && isTextAnnotationObject(selected)) {
8268
+ this.updateSelectedAnnotation({ fill: color });
8269
+ return;
8270
+ }
8271
+ this.setTextColor(color);
6291
8272
  }
6292
- hideAllMaskLabels() {
6293
- const context = this.buildMaskLabelContext();
6294
- if (!context)
8273
+ applyTextFontSizeInput(size) {
8274
+ var _a;
8275
+ if (this.isTextMode()) {
8276
+ this.setTextFontSize(size);
6295
8277
  return;
6296
- hideAllMaskLabels(context);
8278
+ }
8279
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
8280
+ if (selected && isTextAnnotationObject(selected)) {
8281
+ this.updateSelectedAnnotation({ fontSize: size });
8282
+ return;
8283
+ }
8284
+ this.setTextFontSize(size);
6297
8285
  }
6298
- syncMaskLabel(mask) {
6299
- const context = this.buildMaskLabelContext();
6300
- if (!context)
8286
+ applyDrawColorInput(color) {
8287
+ var _a;
8288
+ if (this.isDrawMode()) {
8289
+ this.setDrawColor(color);
6301
8290
  return;
6302
- syncMaskLabel(context, mask);
8291
+ }
8292
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
8293
+ if (selected && isDrawAnnotationObject(selected)) {
8294
+ this.updateSelectedAnnotation({ stroke: color });
8295
+ return;
8296
+ }
8297
+ this.setDrawColor(color);
6303
8298
  }
6304
- showLabelForMask(mask) {
6305
- const context = this.buildMaskLabelContext();
6306
- if (!context)
8299
+ applyDrawBrushSizeInput(size) {
8300
+ var _a;
8301
+ if (this.isDrawMode()) {
8302
+ this.setDrawBrushSize(size);
6307
8303
  return;
6308
- showLabelForMask(context, mask);
8304
+ }
8305
+ const selected = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.getActiveObject();
8306
+ if (selected && isDrawAnnotationObject(selected)) {
8307
+ this.updateSelectedAnnotation({ strokeWidth: size });
8308
+ return;
8309
+ }
8310
+ this.setDrawBrushSize(size);
6309
8311
  }
6310
- handleSelectionChanged(selected) {
8312
+ getSelectedCanvasObjects() {
6311
8313
  var _a, _b, _c;
8314
+ if (!this.canvas)
8315
+ return [];
8316
+ const activeObject = this.canvas.getActiveObject();
8317
+ if (!activeObject)
8318
+ return [];
8319
+ const type = typeof activeObject.type === 'string' ? activeObject.type.toLowerCase() : '';
8320
+ const isActiveSelection = type === 'activeselection' ||
8321
+ ((_c = (_b = (_a = activeObject).isType) === null || _b === void 0 ? void 0 : _b.call(_a, 'ActiveSelection')) !== null && _c !== void 0 ? _c : false);
8322
+ if (!isActiveSelection)
8323
+ return [activeObject];
8324
+ const getObjects = activeObject
8325
+ .getObjects;
8326
+ return typeof getObjects === 'function' ? getObjects.call(activeObject) : [];
8327
+ }
8328
+ moveSelectedEditableObject(operation) {
6312
8329
  if (!this.canvas)
6313
8330
  return;
6314
- const selectedMask = (_a = selected.find(isMaskObject)) !== null && _a !== void 0 ? _a : null;
6315
- const masks = this.canvas.getObjects().filter(isMaskObject);
6316
- masks.forEach((maskObject) => {
6317
- if (maskObject !== selectedMask) {
6318
- if (maskObject.labelObject) {
6319
- this.removeLabelForMask(maskObject);
6320
- }
6321
- applyMaskUnselectedStyle(maskObject);
6322
- }
6323
- else {
6324
- applyMaskSelectedStyle(maskObject);
8331
+ if (!this.canRunIdleOperation(operation))
8332
+ return;
8333
+ const selected = this.getSelectedCanvasObjects().filter(isEditableOverlayObject);
8334
+ if (selected.length !== 1) {
8335
+ if (selected.length > 1) {
8336
+ reportWarning(this.options, null, `${operation} skipped: ActiveSelection layer moves are not supported.`);
6325
8337
  }
8338
+ return;
8339
+ }
8340
+ const object = selected[0];
8341
+ const range = getEditableOverlayRange(this.canvas);
8342
+ const overlays = range.overlays;
8343
+ const currentOverlayIndex = overlays.indexOf(object);
8344
+ if (currentOverlayIndex < 0)
8345
+ return;
8346
+ let nextOverlayIndex = currentOverlayIndex;
8347
+ if (operation === 'bringSelectedObjectForward') {
8348
+ nextOverlayIndex = Math.min(overlays.length - 1, currentOverlayIndex + 1);
8349
+ }
8350
+ else if (operation === 'sendSelectedObjectBackward') {
8351
+ nextOverlayIndex = Math.max(0, currentOverlayIndex - 1);
8352
+ }
8353
+ else if (operation === 'bringSelectedObjectToFront') {
8354
+ nextOverlayIndex = overlays.length - 1;
8355
+ }
8356
+ else if (operation === 'sendSelectedObjectToBack') {
8357
+ nextOverlayIndex = 0;
8358
+ }
8359
+ if (nextOverlayIndex === currentOverlayIndex)
8360
+ return;
8361
+ const reordered = overlays.slice();
8362
+ reordered.splice(currentOverlayIndex, 1);
8363
+ reordered.splice(nextOverlayIndex, 0, object);
8364
+ reordered.forEach((overlay, index) => {
8365
+ var _a, _b;
8366
+ (_b = (_a = this.canvas).moveObjectTo) === null || _b === void 0 ? void 0 : _b.call(_a, overlay, range.start + index);
6326
8367
  });
6327
- if (selectedMask)
6328
- this.showLabelForMask(selectedMask);
6329
- this.updateMaskListSelection(selectedMask);
6330
- this.canvas.requestRenderAll();
8368
+ normalizeLayerOrder(this.canvas);
8369
+ this.canvas.setActiveObject(object);
8370
+ this.canvas.renderAll();
8371
+ this.saveState();
8372
+ this.updateMaskList();
8373
+ this.updateAnnotationList();
6331
8374
  this.updateUi();
6332
- const context = (_b = this.nextSelectionChangeContext) !== null && _b !== void 0 ? _b : this.buildCallbackContext((_c = this.activeStateRestoreOperation) !== null && _c !== void 0 ? _c : 'createMask', this.activeStateRestoreOperation === 'undo' ||
6333
- this.activeStateRestoreOperation === 'redo');
6334
- this.emitOptionCallback('onSelectionChange', [this.buildSelection(selected), context]);
6335
- }
6336
- buildMaskListContext() {
6337
- return {
6338
- canvas: this.canvas,
6339
- getListElementId: () => this.elements.maskList,
6340
- onMaskSelected: (mask) => this.handleSelectionChanged([mask]),
6341
- };
6342
- }
6343
- updateMaskList() {
6344
- renderMaskList(this.buildMaskListContext());
6345
- }
6346
- updateMaskListSelection(selectedMask) {
6347
- updateMaskListSelection(this.buildMaskListContext(), selectedMask);
8375
+ const context = this.buildCallbackContext(operation, false);
8376
+ if (isMaskObject(object))
8377
+ this.emitMasksChanged(context);
8378
+ if (isAnnotationObject(object))
8379
+ this.emitAnnotationsChanged(context);
8380
+ this.emitImageChanged(context);
6348
8381
  }
6349
8382
  async mergeMasks() {
6350
8383
  if (!this.canvas)
6351
8384
  return;
6352
8385
  if (!this.canRunIdleOperation('mergeMasks'))
6353
8386
  return;
8387
+ this.finalizeActiveTextEditingIfNeeded();
6354
8388
  const hasMasks = this.canvas.getObjects().some(isMaskObject);
6355
8389
  if (!hasMasks)
6356
8390
  return;
@@ -6363,7 +8397,11 @@ class ImageEditor {
6363
8397
  await mergeMasks(mergeMasksContext);
6364
8398
  this.updateInputs();
6365
8399
  this.updateMaskList();
8400
+ this.updateAnnotationList();
6366
8401
  this.emitMasksChanged(callbackContext);
8402
+ if (this.getAnnotations().length > 0) {
8403
+ this.emitAnnotationsChanged(callbackContext);
8404
+ }
6367
8405
  this.emitImageChanged(callbackContext);
6368
8406
  }
6369
8407
  finally {
@@ -6372,17 +8410,18 @@ class ImageEditor {
6372
8410
  this.updateUi();
6373
8411
  }
6374
8412
  }
6375
- downloadImage(fileName) {
8413
+ downloadImage(options) {
6376
8414
  if (!this.canvas)
6377
8415
  return;
6378
8416
  if (!this.canRunIdleOperation('downloadImage'))
6379
8417
  return;
8418
+ this.finalizeActiveTextEditingIfNeeded();
6380
8419
  const callbackContext = this.buildCallbackContext('downloadImage', false);
6381
8420
  const operationToken = this.operationGuard.beginBusyOperation('downloadImage');
6382
8421
  this.emitBusyChangeIfChanged(callbackContext);
6383
8422
  const exportContext = this.buildExportServiceContext();
6384
8423
  try {
6385
- downloadImage(exportContext, fileName);
8424
+ downloadImage(exportContext, options);
6386
8425
  }
6387
8426
  finally {
6388
8427
  this.operationGuard.endBusyOperation(operationToken);
@@ -6394,6 +8433,7 @@ class ImageEditor {
6394
8433
  return '';
6395
8434
  if (!this.canRunIdleOperation('exportImageBase64', options))
6396
8435
  return '';
8436
+ this.finalizeActiveTextEditingIfNeeded();
6397
8437
  const callbackContext = this.buildCallbackContext('exportImageBase64', false);
6398
8438
  const operationToken = this.operationGuard.beginBusyOperation('exportImageBase64');
6399
8439
  this.emitBusyChangeIfChanged(callbackContext);
@@ -6408,6 +8448,7 @@ class ImageEditor {
6408
8448
  }
6409
8449
  async exportImageFile(options) {
6410
8450
  this.assertIdleForOperation('exportImageFile', options);
8451
+ this.finalizeActiveTextEditingIfNeeded();
6411
8452
  const callbackContext = this.buildCallbackContext('exportImageFile', false);
6412
8453
  const operationToken = this.operationGuard.beginBusyOperation('exportImageFile');
6413
8454
  this.emitBusyChangeIfChanged(callbackContext);
@@ -6423,7 +8464,7 @@ class ImageEditor {
6423
8464
  buildExportServiceContext() {
6424
8465
  return {
6425
8466
  fabric: this.fabricModule,
6426
- canvas: this.canvas,
8467
+ canvas: this.getLiveCanvasOrThrow('export'),
6427
8468
  options: this.options,
6428
8469
  isImageLoaded: () => this.isImageLoaded(),
6429
8470
  getOriginalImage: () => this.originalImage,
@@ -6439,24 +8480,74 @@ class ImageEditor {
6439
8480
  await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
6440
8481
  this.restoreMergedImageDisplayGeometry(geometry);
6441
8482
  },
6442
- saveState: () => this.captureSnapshotInternal(),
8483
+ captureSnapshot: () => this.captureSnapshotInternal(),
6443
8484
  loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
8485
+ exportImageBase64: (options) => exportImageBase64(this.buildExportServiceContext(), options),
8486
+ updateUi: () => this.updateUi(),
8487
+ updateInputs: () => this.updateInputs(),
6444
8488
  removeAllMasksNoHistory: () => {
6445
8489
  const context = this.buildRemoveMaskContext();
6446
8490
  removeAllMasks(context, { saveHistory: false });
6447
8491
  },
8492
+ getAnnotations: () => this.getAnnotations(),
8493
+ restoreAnnotations: (objects) => {
8494
+ const canvas = this.getLiveCanvasOrThrow('restoreAnnotations');
8495
+ objects.forEach((annotation) => {
8496
+ canvas.add(annotation);
8497
+ });
8498
+ syncAnnotationRuntimeStates(objects);
8499
+ attachTextEditingHandlersToAnnotations(this.buildTextControllerContext(), objects);
8500
+ this.annotationCounter = Math.max(this.annotationCounter, ...objects.map((annotation) => annotation.annotationId), 0);
8501
+ this.updateAnnotationList();
8502
+ },
8503
+ };
8504
+ }
8505
+ buildMergeAnnotationsContext(operationToken) {
8506
+ return {
8507
+ ...this.buildExportServiceContext(),
8508
+ historyManager: this.historyManager,
8509
+ containerElement: this.containerElement,
8510
+ loadImage: async (base64, providedOptions) => {
8511
+ const geometry = this.captureImageDisplayGeometry();
8512
+ await this.loadImageInternal(base64, this.withInternalOperationOptions(operationToken, providedOptions !== null && providedOptions !== void 0 ? providedOptions : {}));
8513
+ this.restoreMergedImageDisplayGeometry(geometry);
8514
+ },
8515
+ captureSnapshot: () => this.captureSnapshotInternal(),
8516
+ loadFromState: (snapshot) => this.loadFromStateInternal(snapshot, this.withInternalOperationOptions(operationToken, this.withAnimationQueueBypass())),
8517
+ exportImageBase64: (options) => exportImageBase64(this.buildExportServiceContext(), options),
8518
+ updateUi: () => this.updateUi(),
8519
+ updateInputs: () => this.updateInputs(),
8520
+ removeAllAnnotationsNoHistory: () => {
8521
+ removeAllAnnotations(this.buildAnnotationManagerContext(), {
8522
+ saveHistory: false,
8523
+ force: true,
8524
+ });
8525
+ },
8526
+ getMasks: () => this.getMasks(),
8527
+ restoreMasks: (objects) => {
8528
+ const canvas = this.getLiveCanvasOrThrow('restoreMasks');
8529
+ objects.forEach((mask) => {
8530
+ canvas.add(mask);
8531
+ reattachMaskHoverHandlers(mask);
8532
+ });
8533
+ this.lastMask = objects.reduce((lastMask, mask) => !lastMask || mask.maskId > lastMask.maskId ? mask : lastMask, null);
8534
+ this.maskCounter = Math.max(this.maskCounter, ...objects.map((mask) => mask.maskId), 0);
8535
+ this.updateMaskList();
8536
+ },
6448
8537
  };
6449
8538
  }
6450
8539
  captureSnapshotInternal() {
6451
- var _a;
8540
+ var _a, _b;
6452
8541
  if (!this.canvas) {
6453
8542
  throw new Error('[ImageEditor] Cannot capture canvas snapshot before init or after dispose.');
6454
8543
  }
6455
8544
  const activeMask = this.getActiveMaskForSnapshot();
8545
+ const activeAnnotation = this.getActiveAnnotationForSnapshot();
6456
8546
  this.hideAllMaskLabels();
6457
8547
  return saveState({
6458
8548
  canvas: this.canvas,
6459
8549
  activeMaskId: (_a = activeMask === null || activeMask === void 0 ? void 0 : activeMask.maskId) !== null && _a !== void 0 ? _a : null,
8550
+ activeAnnotationId: (_b = activeAnnotation === null || activeAnnotation === void 0 ? void 0 : activeAnnotation.annotationId) !== null && _b !== void 0 ? _b : null,
6460
8551
  currentScale: this.currentScale,
6461
8552
  currentRotation: this.currentRotation,
6462
8553
  baseImageScale: this.baseImageScale,
@@ -6475,6 +8566,12 @@ class ImageEditor {
6475
8566
  .filter((object) => isMaskObject(object) && !!object.labelObject);
6476
8567
  return labeledMasks.length === 1 ? ((_a = labeledMasks[0]) !== null && _a !== void 0 ? _a : null) : null;
6477
8568
  }
8569
+ getActiveAnnotationForSnapshot() {
8570
+ if (!this.canvas)
8571
+ return null;
8572
+ const activeObject = this.canvas.getActiveObject();
8573
+ return activeObject && isAnnotationObject(activeObject) ? activeObject : null;
8574
+ }
6478
8575
  enterMosaicMode() {
6479
8576
  if (!this.canvas || !this.originalImage)
6480
8577
  return;
@@ -6558,7 +8655,7 @@ class ImageEditor {
6558
8655
  buildMosaicControllerContext() {
6559
8656
  return {
6560
8657
  fabric: this.fabricModule,
6561
- canvas: this.canvas,
8658
+ canvas: this.getLiveCanvasOrThrow('mosaicController'),
6562
8659
  options: this.options,
6563
8660
  historyManager: this.historyManager,
6564
8661
  getMosaicConfig: () => cloneResolvedMosaicConfig(this.currentMosaicConfig),
@@ -6658,7 +8755,7 @@ class ImageEditor {
6658
8755
  buildCropControllerContext(operationToken) {
6659
8756
  return {
6660
8757
  fabric: this.fabricModule,
6661
- canvas: this.canvas,
8758
+ canvas: this.getLiveCanvasOrThrow('cropController'),
6662
8759
  options: this.options,
6663
8760
  historyManager: this.historyManager,
6664
8761
  isImageLoaded: () => this.isImageLoaded(),
@@ -6680,26 +8777,82 @@ class ImageEditor {
6680
8777
  },
6681
8778
  };
6682
8779
  }
8780
+ syncInputValue(inputElement, value) {
8781
+ if (!inputElement)
8782
+ return;
8783
+ const ownerDocument = inputElement.ownerDocument;
8784
+ if (ownerDocument.activeElement === inputElement && !inputElement.readOnly)
8785
+ return;
8786
+ if (inputElement.value !== value)
8787
+ inputElement.value = value;
8788
+ }
6683
8789
  updateInputs() {
6684
8790
  const scaleId = this.elements.scalePercentageInput;
6685
8791
  if (scaleId) {
6686
8792
  const scaleInputElement = document.getElementById(scaleId);
6687
- if (scaleInputElement) {
6688
- scaleInputElement.value = String(Math.round(this.currentScale * 100));
6689
- }
8793
+ this.syncInputValue(scaleInputElement, String(Math.round(this.currentScale * 100)));
6690
8794
  }
6691
8795
  const mosaicConfig = this.getMosaicConfig();
6692
8796
  const mosaicBrushSizeInputId = this.elements.mosaicBrushSizeInput;
6693
8797
  if (mosaicBrushSizeInputId) {
6694
8798
  const brushInput = document.getElementById(mosaicBrushSizeInputId);
6695
- if (brushInput)
6696
- brushInput.value = String(mosaicConfig.brushSize);
8799
+ this.syncInputValue(brushInput, String(mosaicConfig.brushSize));
6697
8800
  }
6698
8801
  const mosaicBlockSizeInputId = this.elements.mosaicBlockSizeInput;
6699
8802
  if (mosaicBlockSizeInputId) {
6700
8803
  const blockInput = document.getElementById(mosaicBlockSizeInputId);
6701
- if (blockInput)
6702
- blockInput.value = String(mosaicConfig.blockSize);
8804
+ this.syncInputValue(blockInput, String(mosaicConfig.blockSize));
8805
+ }
8806
+ const textConfig = this.getTextConfig();
8807
+ const textColorInputId = this.elements.textColorInput;
8808
+ if (textColorInputId) {
8809
+ const textColorInput = document.getElementById(textColorInputId);
8810
+ this.syncInputValue(textColorInput, textConfig.fill);
8811
+ }
8812
+ const textFontSizeInputId = this.elements.textFontSizeInput;
8813
+ if (textFontSizeInputId) {
8814
+ const fontInput = document.getElementById(textFontSizeInputId);
8815
+ this.syncInputValue(fontInput, String(textConfig.fontSize));
8816
+ }
8817
+ const drawConfig = this.getDrawConfig();
8818
+ const drawColorInputId = this.elements.drawColorInput;
8819
+ if (drawColorInputId) {
8820
+ const drawColorInput = document.getElementById(drawColorInputId);
8821
+ this.syncInputValue(drawColorInput, drawConfig.color);
8822
+ }
8823
+ const drawBrushSizeInputId = this.elements.drawBrushSizeInput;
8824
+ if (drawBrushSizeInputId) {
8825
+ const brushInput = document.getElementById(drawBrushSizeInputId);
8826
+ this.syncInputValue(brushInput, String(drawConfig.brushSize));
8827
+ }
8828
+ }
8829
+ async mergeAnnotations() {
8830
+ if (!this.canvas)
8831
+ return;
8832
+ if (!this.canRunIdleOperation('mergeAnnotations'))
8833
+ return;
8834
+ this.finalizeActiveTextEditingIfNeeded();
8835
+ const hasAnnotations = this.canvas.getObjects().some(isAnnotationObject);
8836
+ if (!hasAnnotations)
8837
+ return;
8838
+ const callbackContext = this.buildCallbackContext('mergeAnnotations', false);
8839
+ const operationToken = this.operationGuard.beginBusyOperation('mergeAnnotations');
8840
+ this.emitBusyChangeIfChanged(callbackContext);
8841
+ this.updateUi();
8842
+ try {
8843
+ await mergeAnnotations(this.buildMergeAnnotationsContext(operationToken));
8844
+ this.updateInputs();
8845
+ this.updateMaskList();
8846
+ this.updateAnnotationList();
8847
+ this.emitAnnotationsChanged(callbackContext);
8848
+ if (this.getMasks().length > 0)
8849
+ this.emitMasksChanged(callbackContext);
8850
+ this.emitImageChanged(callbackContext);
8851
+ }
8852
+ finally {
8853
+ this.operationGuard.endBusyOperation(operationToken);
8854
+ this.emitBusyChangeIfChanged(callbackContext);
8855
+ this.updateUi();
6703
8856
  }
6704
8857
  }
6705
8858
  updateUi() {
@@ -6708,14 +8861,20 @@ class ImageEditor {
6708
8861
  return;
6709
8862
  const hasImage = !!this.originalImage;
6710
8863
  const masks = hasImage ? this.canvas.getObjects().filter(isMaskObject) : [];
8864
+ const annotations = hasImage ? this.canvas.getObjects().filter(isAnnotationObject) : [];
6711
8865
  const hasMasks = masks.length > 0;
8866
+ const hasAnnotations = annotations.length > 0;
6712
8867
  const activeObject = this.canvas.getActiveObject();
6713
8868
  const hasSelectedMask = !!(activeObject && isMaskObject(activeObject));
8869
+ const hasSelectedAnnotation = !!(activeObject && isAnnotationObject(activeObject));
8870
+ const hasSelectedEditableObject = !!activeObject && isEditableOverlayObject(activeObject);
6714
8871
  const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
6715
8872
  const canUndo = this.historyManager.canUndo();
6716
8873
  const canRedo = this.historyManager.canRedo();
6717
8874
  const isInCropMode = this.cropSession !== null;
6718
8875
  const isInMosaicMode = this.mosaicSession !== null;
8876
+ const isInTextMode = this.textSession !== null;
8877
+ const isInDrawMode = this.drawSession !== null;
6719
8878
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
6720
8879
  const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
6721
8880
  if (isInCropMode) {
@@ -6724,6 +8883,18 @@ class ImageEditor {
6724
8883
  });
6725
8884
  return;
6726
8885
  }
8886
+ if (isInTextMode) {
8887
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
8888
+ this.setControlEnabled(key, !isBusy && TEXT_MODE_ENABLED_KEYS.includes(key));
8889
+ });
8890
+ return;
8891
+ }
8892
+ if (isInDrawMode) {
8893
+ CROP_MODE_CONTROL_KEYS.forEach((key) => {
8894
+ this.setControlEnabled(key, !isBusy && DRAW_MODE_ENABLED_KEYS.includes(key));
8895
+ });
8896
+ return;
8897
+ }
6727
8898
  if (isInMosaicMode) {
6728
8899
  MOSAIC_MODE_CONTROL_KEYS.forEach((key) => {
6729
8900
  this.setControlEnabled(key, !isBusy && !isMosaicApplying && MOSAIC_MODE_ENABLED_KEYS.includes(key));
@@ -6742,15 +8913,31 @@ class ImageEditor {
6742
8913
  this.setControlEnabled('removeSelectedMaskButton', hasSelectedMask && !isBusy);
6743
8914
  this.setControlEnabled('removeAllMasksButton', hasMasks && !isBusy);
6744
8915
  this.setControlEnabled('mergeMasksButton', hasImage && hasMasks && !isBusy);
8916
+ this.setControlEnabled('removeSelectedAnnotationButton', hasSelectedAnnotation && !isBusy);
8917
+ this.setControlEnabled('removeAllAnnotationsButton', hasAnnotations && !isBusy);
8918
+ this.setControlEnabled('deleteSelectedObjectButton', hasSelectedEditableObject && !isBusy);
8919
+ this.setControlEnabled('mergeAnnotationsButton', hasImage && hasAnnotations && !isBusy);
8920
+ this.setControlEnabled('bringSelectedObjectForwardButton', hasSelectedEditableObject && !isBusy);
8921
+ this.setControlEnabled('sendSelectedObjectBackwardButton', hasSelectedEditableObject && !isBusy);
8922
+ this.setControlEnabled('bringSelectedObjectToFrontButton', hasSelectedEditableObject && !isBusy);
8923
+ this.setControlEnabled('sendSelectedObjectToBackButton', hasSelectedEditableObject && !isBusy);
6745
8924
  this.setControlEnabled('downloadImageButton', hasImage && !isBusy);
6746
8925
  this.setControlEnabled('resetImageTransformButton', hasImage && !isDefaultTransform && !isBusy);
6747
8926
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
6748
8927
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
6749
8928
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
6750
8929
  this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
8930
+ this.setControlEnabled('enterTextModeButton', hasImage && !isBusy);
8931
+ this.setControlEnabled('enterDrawModeButton', hasImage && !isBusy);
6751
8932
  this.setControlEnabled('exitMosaicModeButton', false);
8933
+ this.setControlEnabled('exitTextModeButton', false);
8934
+ this.setControlEnabled('exitDrawModeButton', false);
6752
8935
  this.setControlEnabled('mosaicBrushSizeInput', !this.isDisposed);
6753
8936
  this.setControlEnabled('mosaicBlockSizeInput', !this.isDisposed);
8937
+ this.setControlEnabled('textColorInput', !this.isDisposed);
8938
+ this.setControlEnabled('textFontSizeInput', !this.isDisposed);
8939
+ this.setControlEnabled('drawColorInput', !this.isDisposed);
8940
+ this.setControlEnabled('drawBrushSizeInput', !this.isDisposed);
6754
8941
  this.setControlEnabled('imageInput', !isBusy);
6755
8942
  this.setControlEnabled('applyCropButton', false);
6756
8943
  this.setControlEnabled('cancelCropButton', false);
@@ -6765,7 +8952,10 @@ class ImageEditor {
6765
8952
  return;
6766
8953
  this.recordElementOriginalState(key, controlElement);
6767
8954
  if ('disabled' in controlElement) {
6768
- controlElement.disabled = !isEnabled;
8955
+ const formControl = controlElement;
8956
+ const nextDisabled = !isEnabled;
8957
+ if (formControl.disabled !== nextDisabled)
8958
+ formControl.disabled = nextDisabled;
6769
8959
  return;
6770
8960
  }
6771
8961
  if (!isEnabled) {
@@ -6838,6 +9028,15 @@ class ImageEditor {
6838
9028
  this.operationGuard.markDisposed();
6839
9029
  this.animQueue.clear();
6840
9030
  (_a = this.domBindings) === null || _a === void 0 ? void 0 : _a.removeAll();
9031
+ if (this.keyboardHandler && this.keyboardDocument) {
9032
+ try {
9033
+ this.keyboardDocument.removeEventListener('keydown', this.keyboardHandler);
9034
+ }
9035
+ catch {
9036
+ }
9037
+ }
9038
+ this.keyboardHandler = null;
9039
+ this.keyboardDocument = null;
6841
9040
  this.restoreElementOriginalStates();
6842
9041
  if (this.cropSession && this.canvas) {
6843
9042
  try {
@@ -6856,6 +9055,22 @@ class ImageEditor {
6856
9055
  }
6857
9056
  this.mosaicSession = null;
6858
9057
  }
9058
+ if (this.textSession && this.canvas) {
9059
+ try {
9060
+ exitTextMode(this.buildTextControllerContext());
9061
+ }
9062
+ catch {
9063
+ }
9064
+ this.textSession = null;
9065
+ }
9066
+ if (this.drawSession && this.canvas) {
9067
+ try {
9068
+ exitDrawMode(this.buildDrawControllerContext());
9069
+ }
9070
+ catch {
9071
+ }
9072
+ this.drawSession = null;
9073
+ }
6859
9074
  if (this.canvas) {
6860
9075
  try {
6861
9076
  void Promise.resolve(this.canvas.dispose()).catch(() => {
@@ -6871,6 +9086,7 @@ class ImageEditor {
6871
9086
  this.currentImageMimeType = null;
6872
9087
  this.lastMask = null;
6873
9088
  this.maskCounter = 0;
9089
+ this.annotationCounter = 0;
6874
9090
  this.currentScale = 1;
6875
9091
  this.currentRotation = 0;
6876
9092
  this.baseImageScale = 1;
@@ -6888,5 +9104,11 @@ class ImageEditor {
6888
9104
 
6889
9105
  exports.ImageEditor = ImageEditor;
6890
9106
  exports.default = ImageEditor;
9107
+ exports.isAnnotationObject = isAnnotationObject;
9108
+ exports.isBaseImageObject = isBaseImageObject;
9109
+ exports.isDrawAnnotationObject = isDrawAnnotationObject;
9110
+ exports.isEditableOverlayObject = isEditableOverlayObject;
6891
9111
  exports.isMaskObject = isMaskObject;
9112
+ exports.isSessionObject = isSessionObject;
9113
+ exports.isTextAnnotationObject = isTextAnnotationObject;
6892
9114
  //# sourceMappingURL=index.cjs.map