@bensitu/image-editor 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +152 -96
  2. package/dist/cjs/index.cjs +534 -139
  3. package/dist/cjs/index.cjs.map +1 -1
  4. package/dist/esm/core/default-options.js +8 -6
  5. package/dist/esm/core/default-options.js.map +1 -1
  6. package/dist/esm/core/public-types.js.map +1 -1
  7. package/dist/esm/core/state-serializer.js +8 -0
  8. package/dist/esm/core/state-serializer.js.map +1 -1
  9. package/dist/esm/crop/crop-controller.js +218 -10
  10. package/dist/esm/crop/crop-controller.js.map +1 -1
  11. package/dist/esm/export/export-format.js.map +1 -1
  12. package/dist/esm/export/export-service.js +57 -56
  13. package/dist/esm/export/export-service.js.map +1 -1
  14. package/dist/esm/image/image-loader.js +2 -2
  15. package/dist/esm/image/image-loader.js.map +1 -1
  16. package/dist/esm/image/transform-controller.js +42 -0
  17. package/dist/esm/image/transform-controller.js.map +1 -1
  18. package/dist/esm/image-editor.js +139 -14
  19. package/dist/esm/image-editor.js.map +1 -1
  20. package/dist/esm/utils/file.js +10 -0
  21. package/dist/esm/utils/file.js.map +1 -1
  22. package/dist/types/core/default-options.d.ts.map +1 -1
  23. package/dist/types/core/public-types.d.ts +68 -50
  24. package/dist/types/core/public-types.d.ts.map +1 -1
  25. package/dist/types/core/state-serializer.d.ts +5 -1
  26. package/dist/types/core/state-serializer.d.ts.map +1 -1
  27. package/dist/types/crop/crop-controller.d.ts +12 -7
  28. package/dist/types/crop/crop-controller.d.ts.map +1 -1
  29. package/dist/types/export/export-format.d.ts +7 -10
  30. package/dist/types/export/export-format.d.ts.map +1 -1
  31. package/dist/types/export/export-service.d.ts +27 -32
  32. package/dist/types/export/export-service.d.ts.map +1 -1
  33. package/dist/types/export/overlay-merge-service.d.ts +3 -3
  34. package/dist/types/export/overlay-merge-service.d.ts.map +1 -1
  35. package/dist/types/image/image-loader.d.ts +5 -5
  36. package/dist/types/image/image-loader.d.ts.map +1 -1
  37. package/dist/types/image/transform-controller.d.ts +14 -7
  38. package/dist/types/image/transform-controller.d.ts.map +1 -1
  39. package/dist/types/image-editor.d.ts +22 -12
  40. package/dist/types/image-editor.d.ts.map +1 -1
  41. package/dist/types/index.d.cts +1 -1
  42. package/dist/types/index.d.cts.map +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.d.ts.map +1 -1
  45. package/dist/types/utils/file.d.ts +13 -0
  46. package/dist/types/utils/file.d.ts.map +1 -1
  47. package/dist/umd/image-editor.umd.js +1 -1
  48. package/dist/umd/image-editor.umd.js.map +1 -1
  49. package/package.json +1 -1
@@ -12,7 +12,7 @@ import { enterDrawMode as enterDrawModeImpl, exitDrawMode as exitDrawModeImpl, u
12
12
  import { isAnnotationLocked, isAnnotationUnlocked } from './annotation/annotation-lock.js';
13
13
  import { syncAnnotationRuntimeStates } from './annotation/annotation-style.js';
14
14
  import { normalizeLayerOrder, getEditableOverlayRange } from './core/layer-order.js';
15
- import { applyCrop as applyCropImpl, cancelCrop as cancelCropImpl, enterCropMode as enterCropModeImpl, } from './crop/crop-controller.js';
15
+ import { applyCrop as applyCropImpl, cancelCrop as cancelCropImpl, enterCropMode as enterCropModeImpl, setCropAspectRatio as setCropAspectRatioImpl, } from './crop/crop-controller.js';
16
16
  import { enterMosaicMode as enterMosaicModeImpl, exitMosaicMode as exitMosaicModeImpl, updateMosaicPreview, } from './mosaic/mosaic-controller.js';
17
17
  import { downloadImage as downloadImageImpl, exportImageBase64 as exportImageBase64Impl, exportImageFile as exportImageFileImpl, mergeAnnotations as mergeAnnotationsImpl, mergeMasks as mergeMasksImpl, } from './export/export-service.js';
18
18
  import { loadImage as loadImageImpl } from './image/image-loader.js';
@@ -24,7 +24,7 @@ import { renderMaskList, updateMaskListSelection } from './mask/mask-list.js';
24
24
  import { applyMaskSelectedStyle, applyMaskUnselectedStyle, reattachMaskHoverHandlers, } from './mask/mask-style.js';
25
25
  import { DomBindings } from './ui/dom-bindings.js';
26
26
  import { setPlaceholderVisible as setPlaceholderVisibleImpl } from './ui/visibility-state.js';
27
- import { inferImageMimeType, readFileAsDataUrl, resetFileInput } from './utils/file.js';
27
+ import { inferImageMimeType, isSupportedImageDataUrl, readFileAsDataUrl, resetFileInput, } from './utils/file.js';
28
28
  import { detectSourceMimeType } from './image/image-resampler.js';
29
29
  const LAYOUT_EPSILON = 0.5;
30
30
  const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
@@ -35,6 +35,8 @@ const CROP_MODE_CONTROL_KEYS = [
35
35
  'rotateRightDegreesInput',
36
36
  'rotateLeftButton',
37
37
  'rotateRightButton',
38
+ 'flipHorizontalButton',
39
+ 'flipVerticalButton',
38
40
  'createMaskButton',
39
41
  'removeSelectedMaskButton',
40
42
  'removeAllMasksButton',
@@ -63,6 +65,7 @@ const CROP_MODE_CONTROL_KEYS = [
63
65
  'redoButton',
64
66
  'imageInput',
65
67
  'enterCropModeButton',
68
+ 'cropAspectRatioSelect',
66
69
  'applyCropButton',
67
70
  'cancelCropButton',
68
71
  'enterMosaicModeButton',
@@ -70,8 +73,12 @@ const CROP_MODE_CONTROL_KEYS = [
70
73
  'mosaicBrushSizeInput',
71
74
  'mosaicBlockSizeInput',
72
75
  ];
73
- const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
74
- const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
76
+ const CROP_MODE_ENABLED_KEYS = [
77
+ 'cropAspectRatioSelect',
78
+ 'applyCropButton',
79
+ 'cancelCropButton',
80
+ ];
81
+ const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['setCropAspectRatio', 'applyCrop', 'cancelCrop']);
75
82
  const TEXT_MODE_ENABLED_KEYS = [
76
83
  'exitTextModeButton',
77
84
  'textColorInput',
@@ -88,6 +95,8 @@ const MOSAIC_MODE_CONTROL_KEYS = [
88
95
  'rotateRightDegreesInput',
89
96
  'rotateLeftButton',
90
97
  'rotateRightButton',
98
+ 'flipHorizontalButton',
99
+ 'flipVerticalButton',
91
100
  'createMaskButton',
92
101
  'removeSelectedMaskButton',
93
102
  'removeAllMasksButton',
@@ -116,6 +125,7 @@ const MOSAIC_MODE_CONTROL_KEYS = [
116
125
  'redoButton',
117
126
  'imageInput',
118
127
  'enterCropModeButton',
128
+ 'cropAspectRatioSelect',
119
129
  'applyCropButton',
120
130
  'cancelCropButton',
121
131
  'enterMosaicModeButton',
@@ -145,6 +155,8 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
145
155
  'saveState',
146
156
  'scaleImage',
147
157
  'rotateImage',
158
+ 'flipHorizontal',
159
+ 'flipVertical',
148
160
  'resetImageTransform',
149
161
  'createMask',
150
162
  'removeSelectedMask',
@@ -174,6 +186,7 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
174
186
  'bringSelectedObjectToFront',
175
187
  'sendSelectedObjectToBack',
176
188
  'enterCropMode',
189
+ 'setCropAspectRatio',
177
190
  'applyCrop',
178
191
  'cancelCrop',
179
192
  'enterMosaicMode',
@@ -530,6 +543,8 @@ export class ImageEditor {
530
543
  rotateRightDegreesInput: 'rotateRightDegreesInput',
531
544
  rotateLeftButton: 'rotateLeftButton',
532
545
  rotateRightButton: 'rotateRightButton',
546
+ flipHorizontalButton: 'flipHorizontalButton',
547
+ flipVerticalButton: 'flipVerticalButton',
533
548
  createMaskButton: 'createMaskButton',
534
549
  removeSelectedMaskButton: 'removeSelectedMaskButton',
535
550
  removeAllMasksButton: 'removeAllMasksButton',
@@ -560,6 +575,7 @@ export class ImageEditor {
560
575
  redoButton: 'redoButton',
561
576
  imageInput: 'imageInput',
562
577
  enterCropModeButton: 'enterCropModeButton',
578
+ cropAspectRatioSelect: 'cropAspectRatioSelect',
563
579
  applyCropButton: 'applyCropButton',
564
580
  cancelCropButton: 'cancelCropButton',
565
581
  enterMosaicModeButton: 'enterMosaicModeButton',
@@ -667,6 +683,12 @@ export class ImageEditor {
667
683
  this.bindElementIfExists('resetImageTransformButton', 'click', () => {
668
684
  void this.resetImageTransform();
669
685
  });
686
+ this.bindElementIfExists('flipHorizontalButton', 'click', () => {
687
+ void this.flipHorizontal();
688
+ });
689
+ this.bindElementIfExists('flipVerticalButton', 'click', () => {
690
+ void this.flipVertical();
691
+ });
670
692
  this.bindElementIfExists('createMaskButton', 'click', () => {
671
693
  this.createMask();
672
694
  });
@@ -751,7 +773,11 @@ export class ImageEditor {
751
773
  void this.rotateImage(this.currentRotation + step);
752
774
  });
753
775
  this.bindElementIfExists('enterCropModeButton', 'click', () => {
754
- this.enterCropMode();
776
+ this.enterCropMode({ aspectRatio: this.getSelectedCropAspectRatio() });
777
+ });
778
+ this.bindElementIfExists('cropAspectRatioSelect', 'change', () => {
779
+ if (this.cropSession)
780
+ this.setCropAspectRatio(this.getSelectedCropAspectRatio());
755
781
  });
756
782
  this.bindElementIfExists('applyCropButton', 'click', () => {
757
783
  void this.applyCrop().catch((error) => {
@@ -904,7 +930,7 @@ export class ImageEditor {
904
930
  return;
905
931
  if (this.isDisposed)
906
932
  return;
907
- if (typeof base64 !== 'string' || !base64.startsWith('data:image/'))
933
+ if (!isSupportedImageDataUrl(base64))
908
934
  return;
909
935
  if (!this.canRunIdleOperation('loadImage', options))
910
936
  return;
@@ -1038,12 +1064,27 @@ export class ImageEditor {
1038
1064
  return false;
1039
1065
  }
1040
1066
  }
1067
+ getSelectedCropAspectRatio() {
1068
+ const inputId = this.elements.cropAspectRatioSelect;
1069
+ const inputEl = inputId
1070
+ ? document.getElementById(inputId)
1071
+ : null;
1072
+ const value = inputEl && 'value' in inputEl ? String(inputEl.value).trim() : '';
1073
+ return (value || 'free');
1074
+ }
1041
1075
  isExpectedIdleGuardError(error, operationName) {
1042
1076
  return (error instanceof Error &&
1043
1077
  error.message.startsWith(`[ImageEditor] Cannot run "${operationName}" `));
1044
1078
  }
1045
1079
  assertCanQueueAnimation(operationName, options) {
1046
- this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
1080
+ const token = this.getInternalOperationToken(options);
1081
+ this.operationGuard.assertCanQueueAnimation(operationName, token);
1082
+ const activeToolMode = this.getActiveToolMode();
1083
+ if (activeToolMode &&
1084
+ !this.operationGuard.isOwnOperation(token) &&
1085
+ !TOOL_MODE_ALLOWED_OPERATIONS[activeToolMode].has(operationName)) {
1086
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while ${activeToolMode} mode is active.`);
1087
+ }
1047
1088
  }
1048
1089
  isImageLoaded() {
1049
1090
  var _a, _b;
@@ -1158,6 +1199,7 @@ export class ImageEditor {
1158
1199
  return this.getActiveToolMode() !== null;
1159
1200
  }
1160
1201
  getEditorState() {
1202
+ var _a, _b;
1161
1203
  const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
1162
1204
  const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
1163
1205
  const image = this.getImageInfo();
@@ -1168,6 +1210,8 @@ export class ImageEditor {
1168
1210
  annotationCount: this.getAnnotations().length,
1169
1211
  currentScale: this.currentScale,
1170
1212
  currentRotation: this.currentRotation,
1213
+ isFlippedHorizontally: !!((_a = this.originalImage) === null || _a === void 0 ? void 0 : _a.flipX),
1214
+ isFlippedVertically: !!((_b = this.originalImage) === null || _b === void 0 ? void 0 : _b.flipY),
1171
1215
  isBusy: this.isBusy(),
1172
1216
  activeToolMode: this.getActiveToolMode(),
1173
1217
  isCropMode: this.cropSession !== null,
@@ -1497,6 +1541,70 @@ export class ImageEditor {
1497
1541
  this.emitBusyChangeIfChanged(context);
1498
1542
  });
1499
1543
  }
1544
+ flipHorizontal() {
1545
+ if (this.isDisposed || !this.transformController)
1546
+ return Promise.resolve();
1547
+ try {
1548
+ this.assertCanQueueAnimation('flipHorizontal');
1549
+ }
1550
+ catch (error) {
1551
+ return Promise.reject(error);
1552
+ }
1553
+ const controller = this.transformController;
1554
+ const context = this.buildCallbackContext('flipHorizontal', false);
1555
+ const job = this.animQueue.add(async () => {
1556
+ if (this.isDisposed)
1557
+ return;
1558
+ this.updateUi();
1559
+ try {
1560
+ await controller.flipHorizontal();
1561
+ if (!this.isDisposed)
1562
+ this.emitImageChanged(context);
1563
+ }
1564
+ finally {
1565
+ if (!this.isDisposed) {
1566
+ this.updateInputs();
1567
+ }
1568
+ }
1569
+ });
1570
+ this.emitBusyChangeIfChanged(context);
1571
+ return job.finally(() => {
1572
+ this.refreshUiAfterQueuedAnimation();
1573
+ this.emitBusyChangeIfChanged(context);
1574
+ });
1575
+ }
1576
+ flipVertical() {
1577
+ if (this.isDisposed || !this.transformController)
1578
+ return Promise.resolve();
1579
+ try {
1580
+ this.assertCanQueueAnimation('flipVertical');
1581
+ }
1582
+ catch (error) {
1583
+ return Promise.reject(error);
1584
+ }
1585
+ const controller = this.transformController;
1586
+ const context = this.buildCallbackContext('flipVertical', false);
1587
+ const job = this.animQueue.add(async () => {
1588
+ if (this.isDisposed)
1589
+ return;
1590
+ this.updateUi();
1591
+ try {
1592
+ await controller.flipVertical();
1593
+ if (!this.isDisposed)
1594
+ this.emitImageChanged(context);
1595
+ }
1596
+ finally {
1597
+ if (!this.isDisposed) {
1598
+ this.updateInputs();
1599
+ }
1600
+ }
1601
+ });
1602
+ this.emitBusyChangeIfChanged(context);
1603
+ return job.finally(() => {
1604
+ this.refreshUiAfterQueuedAnimation();
1605
+ this.emitBusyChangeIfChanged(context);
1606
+ });
1607
+ }
1500
1608
  resetImageTransform() {
1501
1609
  if (this.isDisposed || !this.transformController)
1502
1610
  return Promise.resolve();
@@ -2385,7 +2493,7 @@ export class ImageEditor {
2385
2493
  this.updateUi();
2386
2494
  }
2387
2495
  }
2388
- downloadImage(options) {
2496
+ async downloadImage(options) {
2389
2497
  if (!this.canvas)
2390
2498
  return;
2391
2499
  if (!this.canRunIdleOperation('downloadImage'))
@@ -2396,7 +2504,7 @@ export class ImageEditor {
2396
2504
  this.emitBusyChangeIfChanged(callbackContext);
2397
2505
  const exportContext = this.buildExportServiceContext();
2398
2506
  try {
2399
- downloadImageImpl(exportContext, options);
2507
+ await downloadImageImpl(exportContext, options);
2400
2508
  }
2401
2509
  finally {
2402
2510
  this.operationGuard.endBusyOperation(operationToken);
@@ -2671,7 +2779,7 @@ export class ImageEditor {
2671
2779
  },
2672
2780
  };
2673
2781
  }
2674
- enterCropMode() {
2782
+ enterCropMode(options = {}) {
2675
2783
  if (!this.canvas || !this.originalImage)
2676
2784
  return;
2677
2785
  if (this.cropSession)
@@ -2681,12 +2789,23 @@ export class ImageEditor {
2681
2789
  if (!this.canRunIdleOperation('enterCropMode'))
2682
2790
  return;
2683
2791
  const cropControllerContext = this.buildCropControllerContext();
2684
- enterCropModeImpl(cropControllerContext);
2792
+ enterCropModeImpl(cropControllerContext, options);
2685
2793
  this.updateUi();
2686
2794
  const callbackContext = this.buildCallbackContext('enterCropMode', false);
2687
2795
  this.emitBusyChangeIfChanged(callbackContext);
2688
2796
  this.emitImageChanged(callbackContext);
2689
2797
  }
2798
+ setCropAspectRatio(aspectRatio) {
2799
+ if (!this.canvas || !this.cropSession)
2800
+ return;
2801
+ if (!this.canRunIdleOperation('setCropAspectRatio'))
2802
+ return;
2803
+ const cropControllerContext = this.buildCropControllerContext();
2804
+ setCropAspectRatioImpl(cropControllerContext, aspectRatio);
2805
+ this.updateUi();
2806
+ const callbackContext = this.buildCallbackContext('setCropAspectRatio', false);
2807
+ this.emitImageChanged(callbackContext);
2808
+ }
2690
2809
  cancelCrop() {
2691
2810
  if (!this.canvas || !this.cropSession)
2692
2811
  return;
@@ -2831,7 +2950,7 @@ export class ImageEditor {
2831
2950
  }
2832
2951
  }
2833
2952
  updateUi() {
2834
- var _a;
2953
+ var _a, _b, _c;
2835
2954
  if (!this.canvas)
2836
2955
  return;
2837
2956
  const hasImage = !!this.originalImage;
@@ -2843,7 +2962,10 @@ export class ImageEditor {
2843
2962
  const hasSelectedMask = !!(activeObject && isMaskObject(activeObject));
2844
2963
  const hasSelectedAnnotation = !!(activeObject && isAnnotationObject(activeObject));
2845
2964
  const hasSelectedEditableObject = !!activeObject && isEditableOverlayObject(activeObject);
2846
- const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
2965
+ const isDefaultTransform = this.currentScale === 1 &&
2966
+ this.currentRotation === 0 &&
2967
+ !((_a = this.originalImage) === null || _a === void 0 ? void 0 : _a.flipX) &&
2968
+ !((_b = this.originalImage) === null || _b === void 0 ? void 0 : _b.flipY);
2847
2969
  const canUndo = this.historyManager.canUndo();
2848
2970
  const canRedo = this.historyManager.canRedo();
2849
2971
  const isInCropMode = this.cropSession !== null;
@@ -2851,7 +2973,7 @@ export class ImageEditor {
2851
2973
  const isInTextMode = this.textSession !== null;
2852
2974
  const isInDrawMode = this.drawSession !== null;
2853
2975
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
2854
- const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
2976
+ const isMosaicApplying = ((_c = this.mosaicSession) === null || _c === void 0 ? void 0 : _c.isApplying) === true;
2855
2977
  if (isInCropMode) {
2856
2978
  CROP_MODE_CONTROL_KEYS.forEach((key) => {
2857
2979
  this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
@@ -2884,6 +3006,8 @@ export class ImageEditor {
2884
3006
  this.setControlEnabled('zoomOutButton', hasImage && !isBusy && this.currentScale > this.options.minScale);
2885
3007
  this.setControlEnabled('rotateLeftButton', hasImage && !isBusy);
2886
3008
  this.setControlEnabled('rotateRightButton', hasImage && !isBusy);
3009
+ this.setControlEnabled('flipHorizontalButton', hasImage && !isBusy);
3010
+ this.setControlEnabled('flipVerticalButton', hasImage && !isBusy);
2887
3011
  this.setControlEnabled('createMaskButton', hasImage && !isBusy);
2888
3012
  this.setControlEnabled('removeSelectedMaskButton', hasSelectedMask && !isBusy);
2889
3013
  this.setControlEnabled('removeAllMasksButton', hasMasks && !isBusy);
@@ -2901,6 +3025,7 @@ export class ImageEditor {
2901
3025
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
2902
3026
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
2903
3027
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
3028
+ this.setControlEnabled('cropAspectRatioSelect', hasImage && !isBusy);
2904
3029
  this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
2905
3030
  this.setControlEnabled('enterTextModeButton', hasImage && !isBusy);
2906
3031
  this.setControlEnabled('enterDrawModeButton', hasImage && !isBusy);