@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
@@ -190,7 +190,7 @@ const DEFAULT_OPTIONS = {
190
190
  groupSelection: false,
191
191
  showPlaceholder: true,
192
192
  initialImageBase64: null,
193
- defaultDownloadFileName: 'edited_image.jpg',
193
+ defaultDownloadFileName: 'edited_image',
194
194
  onImageLoadStart: null,
195
195
  onImageLoaded: null,
196
196
  onImageCleared: null,
@@ -218,6 +218,7 @@ const DEFAULT_LABEL_TEXT_OPTIONS = {
218
218
  const DEFAULT_LABEL = {
219
219
  getText: (mask) => mask.maskName};
220
220
  const DEFAULT_CROP = {
221
+ aspectRatio: 'free',
221
222
  minWidth: 100,
222
223
  minHeight: 100,
223
224
  padding: 10,
@@ -730,7 +731,7 @@ function getInvalidDrawConfigFields(input) {
730
731
  return invalid;
731
732
  }
732
733
  function resolveOptions(input) {
733
- var _a, _b, _c, _d;
734
+ var _a, _b, _c, _d, _e;
734
735
  const raw = input !== null && input !== void 0 ? input : {};
735
736
  const resolved = { ...DEFAULT_OPTIONS };
736
737
  for (const key of Object.keys(raw)) {
@@ -875,13 +876,14 @@ function resolveOptions(input) {
875
876
  Object.freeze(label);
876
877
  const userCrop = raw.crop && typeof raw.crop === 'object' ? raw.crop : {};
877
878
  const crop = {
879
+ aspectRatio: (_a = userCrop.aspectRatio) !== null && _a !== void 0 ? _a : DEFAULT_CROP.aspectRatio,
878
880
  minWidth: normalizePositiveFiniteNumber(userCrop.minWidth, DEFAULT_CROP.minWidth),
879
881
  minHeight: normalizePositiveFiniteNumber(userCrop.minHeight, DEFAULT_CROP.minHeight),
880
882
  padding: normalizeNonNegativeFiniteNumber(userCrop.padding, DEFAULT_CROP.padding),
881
- hideMasksDuringCrop: (_a = userCrop.hideMasksDuringCrop) !== null && _a !== void 0 ? _a : DEFAULT_CROP.hideMasksDuringCrop,
882
- preserveMasksAfterCrop: (_b = userCrop.preserveMasksAfterCrop) !== null && _b !== void 0 ? _b : DEFAULT_CROP.preserveMasksAfterCrop,
883
- allowRotationOfCropRect: (_c = userCrop.allowRotationOfCropRect) !== null && _c !== void 0 ? _c : DEFAULT_CROP.allowRotationOfCropRect,
884
- exportFileType: (_d = userCrop.exportFileType) !== null && _d !== void 0 ? _d : DEFAULT_CROP.exportFileType,
883
+ hideMasksDuringCrop: (_b = userCrop.hideMasksDuringCrop) !== null && _b !== void 0 ? _b : DEFAULT_CROP.hideMasksDuringCrop,
884
+ preserveMasksAfterCrop: (_c = userCrop.preserveMasksAfterCrop) !== null && _c !== void 0 ? _c : DEFAULT_CROP.preserveMasksAfterCrop,
885
+ allowRotationOfCropRect: (_d = userCrop.allowRotationOfCropRect) !== null && _d !== void 0 ? _d : DEFAULT_CROP.allowRotationOfCropRect,
886
+ exportFileType: (_e = userCrop.exportFileType) !== null && _e !== void 0 ? _e : DEFAULT_CROP.exportFileType,
885
887
  exportQuality: normalizeOptionalQuality(userCrop.exportQuality),
886
888
  };
887
889
  Object.freeze(crop);
@@ -1155,6 +1157,8 @@ const SNAPSHOT_CUSTOM_KEYS = [
1155
1157
  'borderColor',
1156
1158
  'cornerColor',
1157
1159
  'cornerSize',
1160
+ 'flipX',
1161
+ 'flipY',
1158
1162
  'isMosaicPreview',
1159
1163
  'annotationId',
1160
1164
  'annotationType',
@@ -1214,6 +1218,12 @@ function copySnapshotCustomPropsFromCanvas(canvasObjects, jsonObjects) {
1214
1218
  if (typeof liveObject.cornerSize === 'number') {
1215
1219
  jsonObject.cornerSize = liveObject.cornerSize;
1216
1220
  }
1221
+ if (typeof liveObject.flipX === 'boolean') {
1222
+ jsonObject.flipX = liveObject.flipX;
1223
+ }
1224
+ if (typeof liveObject.flipY === 'boolean') {
1225
+ jsonObject.flipY = liveObject.flipY;
1226
+ }
1217
1227
  if (liveObject.isCropRect === true)
1218
1228
  jsonObject.isCropRect = true;
1219
1229
  if (liveObject.maskLabel === true)
@@ -2931,7 +2941,174 @@ function reapplyPreservedMasks(context, cropRegion, records) {
2931
2941
  catch {
2932
2942
  }
2933
2943
  }
2934
- function enterCropMode(context) {
2944
+ const CROP_ASPECT_RATIO_PRESETS = Object.freeze({
2945
+ free: null,
2946
+ '1:1': 1,
2947
+ '3:4': 3 / 4,
2948
+ '4:3': 4 / 3,
2949
+ '3:2': 3 / 2,
2950
+ '2:3': 2 / 3,
2951
+ '9:16': 9 / 16,
2952
+ '16:9': 16 / 9,
2953
+ });
2954
+ function normalizeCropAspectRatio(input) {
2955
+ var _a;
2956
+ if (input === null || input === undefined)
2957
+ return null;
2958
+ if (typeof input === 'number') {
2959
+ return Number.isFinite(input) && input > 0 ? input : null;
2960
+ }
2961
+ if (typeof input === 'string') {
2962
+ const trimmed = input.trim();
2963
+ if (Object.prototype.hasOwnProperty.call(CROP_ASPECT_RATIO_PRESETS, trimmed)) {
2964
+ return (_a = CROP_ASPECT_RATIO_PRESETS[trimmed]) !== null && _a !== void 0 ? _a : null;
2965
+ }
2966
+ const parts = trimmed.split(':');
2967
+ if (parts.length !== 2)
2968
+ return null;
2969
+ const width = Number(parts[0]);
2970
+ const height = Number(parts[1]);
2971
+ return Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0
2972
+ ? width / height
2973
+ : null;
2974
+ }
2975
+ if (typeof input === 'object') {
2976
+ const width = Number(input.width);
2977
+ const height = Number(input.height);
2978
+ return Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0
2979
+ ? width / height
2980
+ : null;
2981
+ }
2982
+ return null;
2983
+ }
2984
+ function fitAspectRatioInside(maxWidth, maxHeight, aspectRatio) {
2985
+ const safeMaxWidth = Math.max(1, maxWidth);
2986
+ const safeMaxHeight = Math.max(1, maxHeight);
2987
+ let width = safeMaxWidth;
2988
+ let height = width / aspectRatio;
2989
+ if (height > safeMaxHeight) {
2990
+ height = safeMaxHeight;
2991
+ width = height * aspectRatio;
2992
+ }
2993
+ return {
2994
+ width: Math.max(1, width),
2995
+ height: Math.max(1, height),
2996
+ };
2997
+ }
2998
+ function minimumAspectRatioSizeThatFits(minWidth, minHeight, maxWidth, maxHeight, aspectRatio) {
2999
+ let width = Math.max(1, minWidth);
3000
+ let height = width / aspectRatio;
3001
+ if (height < minHeight) {
3002
+ height = Math.max(1, minHeight);
3003
+ width = height * aspectRatio;
3004
+ }
3005
+ return width <= maxWidth && height <= maxHeight ? { width, height } : null;
3006
+ }
3007
+ function chooseAspectRatioResizeBasis(canvas, cropRect, scaleX, scaleY) {
3008
+ var _a, _b, _c;
3009
+ const corner = String((_c = (_a = cropRect.__corner) !== null && _a !== void 0 ? _a : (_b = canvas._currentTransform) === null || _b === void 0 ? void 0 : _b.corner) !== null && _c !== void 0 ? _c : '').toLowerCase();
3010
+ if (corner === 'mt' || corner === 'mb')
3011
+ return 'height';
3012
+ if (corner === 'ml' || corner === 'mr')
3013
+ return 'width';
3014
+ return Math.abs(scaleY - 1) > Math.abs(scaleX - 1) ? 'height' : 'width';
3015
+ }
3016
+ function constrainAspectRatioSize(requestedWidth, requestedHeight, basis, aspectRatio, minWidth, minHeight, maxWidth, maxHeight) {
3017
+ var _a;
3018
+ const maxSize = fitAspectRatioInside(maxWidth, maxHeight, aspectRatio);
3019
+ const minSize = (_a = minimumAspectRatioSizeThatFits(minWidth, minHeight, maxSize.width, maxSize.height, aspectRatio)) !== null && _a !== void 0 ? _a : maxSize;
3020
+ let width = basis === 'height' ? requestedHeight * aspectRatio : requestedWidth;
3021
+ let height = basis === 'height' ? requestedHeight : requestedWidth / aspectRatio;
3022
+ if (width > maxSize.width || height > maxSize.height) {
3023
+ ({ width, height } = maxSize);
3024
+ }
3025
+ if (width < minSize.width || height < minSize.height) {
3026
+ ({ width, height } = minSize);
3027
+ }
3028
+ return { width, height };
3029
+ }
3030
+ function resolvePaddedCropArea(boundsLeft, boundsTop, maxCropWidth, maxCropHeight, padding) {
3031
+ const insetX = padding * 2 < maxCropWidth ? padding : 0;
3032
+ const insetY = padding * 2 < maxCropHeight ? padding : 0;
3033
+ return {
3034
+ left: boundsLeft + insetX,
3035
+ top: boundsTop + insetY,
3036
+ width: Math.max(1, maxCropWidth - insetX * 2),
3037
+ height: Math.max(1, maxCropHeight - insetY * 2),
3038
+ };
3039
+ }
3040
+ function resolveCropBounds(context) {
3041
+ const originalImage = context.getOriginalImage();
3042
+ if (!originalImage)
3043
+ return null;
3044
+ originalImage.setCoords();
3045
+ const { options } = context;
3046
+ const imageBounds = originalImage.getBoundingRect();
3047
+ const padding = Number.isFinite(Number(options.crop.padding))
3048
+ ? Number(options.crop.padding)
3049
+ : CROP_DEFAULT_PADDING;
3050
+ const boundsLeft = Math.max(0, Math.floor(imageBounds.left));
3051
+ const boundsTop = Math.max(0, Math.floor(imageBounds.top));
3052
+ const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
3053
+ const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
3054
+ const configuredMinWidth = Math.max(1, Number(options.crop.minWidth) || 1);
3055
+ const configuredMinHeight = Math.max(1, Number(options.crop.minHeight) || 1);
3056
+ return {
3057
+ boundsLeft,
3058
+ boundsTop,
3059
+ maxCropWidth,
3060
+ maxCropHeight,
3061
+ minCropWidth: Math.min(configuredMinWidth, maxCropWidth),
3062
+ minCropHeight: Math.min(configuredMinHeight, maxCropHeight),
3063
+ padding,
3064
+ imageBounds,
3065
+ };
3066
+ }
3067
+ function clampCropRectIntoBounds(cropRect, bounds) {
3068
+ const width = Math.min(bounds.maxCropWidth, Math.max(bounds.minCropWidth, (Number(cropRect.width) || 1) * (Number(cropRect.scaleX) || 1)));
3069
+ const height = Math.min(bounds.maxCropHeight, Math.max(bounds.minCropHeight, (Number(cropRect.height) || 1) * (Number(cropRect.scaleY) || 1)));
3070
+ const left = Math.min(bounds.boundsLeft + bounds.maxCropWidth - width, Math.max(bounds.boundsLeft, Number(cropRect.left) || bounds.boundsLeft));
3071
+ const top = Math.min(bounds.boundsTop + bounds.maxCropHeight - height, Math.max(bounds.boundsTop, Number(cropRect.top) || bounds.boundsTop));
3072
+ cropRect.set({ left, top, width, height, scaleX: 1, scaleY: 1 });
3073
+ }
3074
+ function resizeCropRectToAspectRatio(context, cropRect, aspectRatio) {
3075
+ const bounds = resolveCropBounds(context);
3076
+ if (!bounds)
3077
+ return;
3078
+ if (aspectRatio === null) {
3079
+ clampCropRectIntoBounds(cropRect, bounds);
3080
+ cropRect.setCoords();
3081
+ return;
3082
+ }
3083
+ const available = resolvePaddedCropArea(bounds.boundsLeft, bounds.boundsTop, bounds.maxCropWidth, bounds.maxCropHeight, bounds.padding);
3084
+ const fitted = fitAspectRatioInside(available.width, available.height, aspectRatio);
3085
+ cropRect.set({
3086
+ left: available.left + (available.width - fitted.width) / 2,
3087
+ top: available.top + (available.height - fitted.height) / 2,
3088
+ width: fitted.width,
3089
+ height: fitted.height,
3090
+ scaleX: 1,
3091
+ scaleY: 1,
3092
+ });
3093
+ cropRect.setCoords();
3094
+ }
3095
+ function updateCropRectControlVisibility(cropRect, aspectRatio, allowRotationOfCropRect) {
3096
+ const lockedRatio = aspectRatio !== null;
3097
+ cropRect.setControlsVisibility({
3098
+ tl: true,
3099
+ tr: true,
3100
+ br: true,
3101
+ bl: true,
3102
+ mt: !lockedRatio,
3103
+ mb: !lockedRatio,
3104
+ ml: !lockedRatio,
3105
+ mr: !lockedRatio,
3106
+ mtr: allowRotationOfCropRect,
3107
+ });
3108
+ cropRect.setCoords();
3109
+ }
3110
+ function enterCropMode(context, cropModeOptions = {}) {
3111
+ var _a;
2935
3112
  const { canvas, options } = context;
2936
3113
  if (context.getCropSession())
2937
3114
  return;
@@ -2953,18 +3130,35 @@ function enterCropMode(context) {
2953
3130
  const boundsTop = Math.max(0, Math.floor(imageBounds.top));
2954
3131
  const maxCropWidth = Math.max(1, Math.floor(imageBounds.width));
2955
3132
  const maxCropHeight = Math.max(1, Math.floor(imageBounds.height));
2956
- const rectLeft = Math.min(boundsLeft + maxCropWidth - 1, Math.max(boundsLeft, Math.floor(imageBounds.left + padding)));
2957
- const rectTop = Math.min(boundsTop + maxCropHeight - 1, Math.max(boundsTop, Math.floor(imageBounds.top + padding)));
2958
3133
  const configuredMinWidth = Math.max(1, Number(options.crop.minWidth) || 1);
2959
3134
  const configuredMinHeight = Math.max(1, Number(options.crop.minHeight) || 1);
2960
3135
  const minCropWidth = Math.min(configuredMinWidth, maxCropWidth);
2961
3136
  const minCropHeight = Math.min(configuredMinHeight, maxCropHeight);
2962
3137
  const allowRotation = !!options.crop.allowRotationOfCropRect;
3138
+ const aspectRatio = normalizeCropAspectRatio((_a = cropModeOptions.aspectRatio) !== null && _a !== void 0 ? _a : options.crop.aspectRatio);
3139
+ let rectLeft;
3140
+ let rectTop;
3141
+ let rectWidth;
3142
+ let rectHeight;
3143
+ if (aspectRatio === null) {
3144
+ rectLeft = Math.min(boundsLeft + maxCropWidth - 1, Math.max(boundsLeft, Math.floor(imageBounds.left + padding)));
3145
+ rectTop = Math.min(boundsTop + maxCropHeight - 1, Math.max(boundsTop, Math.floor(imageBounds.top + padding)));
3146
+ rectWidth = minCropWidth;
3147
+ rectHeight = minCropHeight;
3148
+ }
3149
+ else {
3150
+ const available = resolvePaddedCropArea(boundsLeft, boundsTop, maxCropWidth, maxCropHeight, padding);
3151
+ const fitted = fitAspectRatioInside(available.width, available.height, aspectRatio);
3152
+ rectWidth = fitted.width;
3153
+ rectHeight = fitted.height;
3154
+ rectLeft = available.left + (available.width - rectWidth) / 2;
3155
+ rectTop = available.top + (available.height - rectHeight) / 2;
3156
+ }
2963
3157
  const cropRect = new context.fabric.Rect({
2964
3158
  left: rectLeft,
2965
3159
  top: rectTop,
2966
- width: minCropWidth,
2967
- height: minCropHeight,
3160
+ width: rectWidth,
3161
+ height: rectHeight,
2968
3162
  originX: 'left',
2969
3163
  originY: 'top',
2970
3164
  fill: CROP_RECT_FILL,
@@ -2978,9 +3172,7 @@ function enterCropMode(context) {
2978
3172
  objectCaching: false,
2979
3173
  lockScalingFlip: true,
2980
3174
  });
2981
- if (!allowRotation) {
2982
- cropRect.setControlVisible('mtr', false);
2983
- }
3175
+ updateCropRectControlVisibility(cropRect, aspectRatio, allowRotation);
2984
3176
  canvas.add(cropRect);
2985
3177
  markSessionObject(cropRect, 'cropRect');
2986
3178
  cropRect.isCropRect = true;
@@ -3022,8 +3214,22 @@ function enterCropMode(context) {
3022
3214
  try {
3023
3215
  const cropWidth = Math.max(1, Number(cropRect.width) || 1);
3024
3216
  const cropHeight = Math.max(1, Number(cropRect.height) || 1);
3025
- const nextScaleX = Math.min(maxCropWidth / cropWidth, Math.max(minCropWidth / cropWidth, Number(cropRect.scaleX) || 1));
3026
- const nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
3217
+ let nextScaleX;
3218
+ let nextScaleY;
3219
+ const activeSession = context.getCropSession();
3220
+ const activeAspectRatio = activeSession ? activeSession.aspectRatio : aspectRatio;
3221
+ if (activeAspectRatio === null) {
3222
+ nextScaleX = Math.min(maxCropWidth / cropWidth, Math.max(minCropWidth / cropWidth, Number(cropRect.scaleX) || 1));
3223
+ nextScaleY = Math.min(maxCropHeight / cropHeight, Math.max(minCropHeight / cropHeight, Number(cropRect.scaleY) || 1));
3224
+ }
3225
+ else {
3226
+ const rawScaleX = Math.max(0.0001, Number(cropRect.scaleX) || 1);
3227
+ const rawScaleY = Math.max(0.0001, Number(cropRect.scaleY) || 1);
3228
+ const basis = chooseAspectRatioResizeBasis(canvas, cropRect, rawScaleX, rawScaleY);
3229
+ const constrained = constrainAspectRatioSize(cropWidth * rawScaleX, cropHeight * rawScaleY, basis, activeAspectRatio, minCropWidth, minCropHeight, maxCropWidth, maxCropHeight);
3230
+ nextScaleX = constrained.width / cropWidth;
3231
+ nextScaleY = constrained.height / cropHeight;
3232
+ }
3027
3233
  const scaledWidth = cropWidth * nextScaleX;
3028
3234
  const scaledHeight = cropHeight * nextScaleY;
3029
3235
  const maxLeft = Math.max(boundsLeft, boundsLeft + maxCropWidth - scaledWidth);
@@ -3051,6 +3257,7 @@ function enterCropMode(context) {
3051
3257
  prevEvented,
3052
3258
  maskBackups,
3053
3259
  cropRect,
3260
+ aspectRatio,
3054
3261
  handlers: [
3055
3262
  {
3056
3263
  target: cropRect,
@@ -3065,6 +3272,17 @@ function enterCropMode(context) {
3065
3272
  context.setCropSession(session);
3066
3273
  canvas.renderAll();
3067
3274
  }
3275
+ function setCropAspectRatio(context, aspectRatioInput) {
3276
+ const session = context.getCropSession();
3277
+ if (!(session === null || session === void 0 ? void 0 : session.cropRect))
3278
+ return;
3279
+ const aspectRatio = normalizeCropAspectRatio(aspectRatioInput);
3280
+ session.aspectRatio = aspectRatio;
3281
+ resizeCropRectToAspectRatio(context, session.cropRect, aspectRatio);
3282
+ updateCropRectControlVisibility(session.cropRect, aspectRatio, !!context.options.crop.allowRotationOfCropRect);
3283
+ context.canvas.setActiveObject(session.cropRect);
3284
+ context.canvas.requestRenderAll();
3285
+ }
3068
3286
  function cancelCrop(context) {
3069
3287
  const session = context.getCropSession();
3070
3288
  if (!session)
@@ -4183,21 +4401,6 @@ function resolveExportOptions(context, options) {
4183
4401
  format: resolveExportFormat(providedOptions, context.options.downsampleQuality),
4184
4402
  };
4185
4403
  }
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
- }
4201
4404
  function readCanvasDimension(canvas, getterName, propertyName) {
4202
4405
  const canvasLike = canvas;
4203
4406
  const getter = canvasLike[getterName];
@@ -4615,16 +4818,23 @@ async function reencodeDataUrlAs(sourceDataUrl, target, backgroundColor, canvas)
4615
4818
  function warnNoImageLoaded(operation) {
4616
4819
  console.warn(`[ImageEditor] ${operation} skipped: no image is loaded on the canvas.`);
4617
4820
  }
4618
- async function exportImageBase64(context, options) {
4619
- if (!context.isImageLoaded()) {
4620
- warnNoImageLoaded('exportImageBase64');
4621
- return '';
4821
+ function extensionForFormat(format) {
4822
+ return format === 'jpeg' ? 'jpg' : format;
4823
+ }
4824
+ function resolveFileName(baseName, format) {
4825
+ const fallback = 'edited_image';
4826
+ const trimmed = String(baseName || fallback).trim() || fallback;
4827
+ const ext = extensionForFormat(format.format);
4828
+ if (/\.(jpe?g|png|webp)$/i.test(trimmed)) {
4829
+ return trimmed.replace(/\.(jpe?g|png|webp)$/i, `.${ext}`);
4622
4830
  }
4831
+ return `${trimmed}.${ext}`;
4832
+ }
4833
+ async function renderExportDataUrl(context, resolved) {
4623
4834
  const activeObject = captureActiveObject(context.canvas);
4624
4835
  const labelBackups = captureMaskLabelBackups(context.canvas);
4625
4836
  try {
4626
4837
  context.canvas.discardActiveObject();
4627
- const resolved = resolveExportOptions(context, options);
4628
4838
  const { region, partialEdges } = computeExportRegion(context, resolved.exportArea);
4629
4839
  assertExportPixelBudget(context, resolved.multiplier, region);
4630
4840
  const renderFormat = region && resolved.format.format === 'jpeg' ? 'png' : resolved.format.format;
@@ -4649,6 +4859,14 @@ async function exportImageBase64(context, options) {
4649
4859
  requestRender(context.canvas);
4650
4860
  }
4651
4861
  }
4862
+ async function exportImageBase64(context, options) {
4863
+ if (!context.isImageLoaded()) {
4864
+ warnNoImageLoaded('exportImageBase64');
4865
+ return '';
4866
+ }
4867
+ const resolved = resolveExportOptions(context, options);
4868
+ return renderExportDataUrl(context, resolved);
4869
+ }
4652
4870
  async function exportImageFile(context, options) {
4653
4871
  var _a;
4654
4872
  if (!context.isImageLoaded()) {
@@ -4656,20 +4874,9 @@ async function exportImageFile(context, options) {
4656
4874
  throw new ExportNotReadyError('exportImageFile');
4657
4875
  }
4658
4876
  const providedOptions = options !== null && options !== void 0 ? options : {};
4659
- const fileName = (_a = providedOptions.fileName) !== null && _a !== void 0 ? _a : context.options.defaultDownloadFileName;
4660
- const resolved = resolveExportFormat(providedOptions, context.options.downsampleQuality);
4661
- const base64 = await exportImageBase64(context, {
4662
- exportArea: providedOptions.exportArea,
4663
- mergeMasks: providedOptions.mergeMasks,
4664
- mergeAnnotations: providedOptions.mergeAnnotations,
4665
- multiplier: providedOptions.multiplier,
4666
- quality: providedOptions.quality,
4667
- fileType: providedOptions.fileType,
4668
- });
4669
- if (!base64) {
4670
- throw new ExportNotReadyError('exportImageFile');
4671
- }
4672
- const finalDataUrl = await reencodeDataUrlAs(base64, resolved, context.options.backgroundColor, context.canvas);
4877
+ const resolved = resolveExportOptions(context, providedOptions);
4878
+ const rawDataUrl = await renderExportDataUrl(context, resolved);
4879
+ const finalDataUrl = await reencodeDataUrlAs(rawDataUrl, resolved.format, context.options.backgroundColor, context.canvas);
4673
4880
  let bytes;
4674
4881
  try {
4675
4882
  bytes = dataUrlToBytes(finalDataUrl);
@@ -4677,35 +4884,47 @@ async function exportImageFile(context, options) {
4677
4884
  catch (error) {
4678
4885
  throw new ExportError('exportImageFile failed to decode rendered data URL.', error);
4679
4886
  }
4680
- return new File([bytes], fileName, { type: resolved.mimeType });
4887
+ const fileName = resolveFileName((_a = providedOptions.fileName) !== null && _a !== void 0 ? _a : context.options.defaultDownloadFileName, resolved.format);
4888
+ return new File([bytes], fileName, { type: resolved.format.mimeType });
4681
4889
  }
4682
- function downloadImage(context, options) {
4890
+ async function downloadImage(context, options) {
4683
4891
  if (!context.isImageLoaded()) {
4684
4892
  warnNoImageLoaded('downloadImage');
4685
4893
  return;
4686
4894
  }
4687
- const resolved = resolveDownloadOptions(context, options);
4688
- void exportImageBase64(context, resolved.exportOptions)
4689
- .then((dataUrl) => {
4690
- if (!dataUrl)
4691
- return;
4692
- const ownerDocument = getCanvasDocument$1(context.canvas);
4693
- const link = ownerDocument.createElement('a');
4694
- link.download = resolved.fileName;
4695
- link.href = dataUrl;
4696
- const body = ownerDocument.body;
4697
- body.appendChild(link);
4698
- try {
4699
- link.click();
4700
- }
4701
- finally {
4702
- body.removeChild(link);
4703
- }
4704
- })
4705
- .catch((error) => {
4895
+ if (options !== undefined && options !== null && typeof options !== 'object') {
4896
+ throw new TypeError('[ImageEditor] downloadImage(options) expects an ImageExportOptions object.');
4897
+ }
4898
+ try {
4899
+ const file = await exportImageFile(context, options);
4900
+ triggerFileDownload(context, file);
4901
+ }
4902
+ catch (error) {
4706
4903
  reportError(context.options, error, 'downloadImage failed.');
4707
4904
  console.error('[ImageEditor] downloadImage failed', error);
4708
- });
4905
+ throw error;
4906
+ }
4907
+ }
4908
+ function triggerFileDownload(context, file) {
4909
+ const ownerDocument = getCanvasDocument$1(context.canvas);
4910
+ const objectUrl = URL.createObjectURL(file);
4911
+ const link = ownerDocument.createElement('a');
4912
+ link.download = file.name;
4913
+ link.href = objectUrl;
4914
+ const body = ownerDocument.body;
4915
+ body.appendChild(link);
4916
+ try {
4917
+ link.click();
4918
+ }
4919
+ finally {
4920
+ body.removeChild(link);
4921
+ if (typeof globalThis.setTimeout === 'function') {
4922
+ globalThis.setTimeout(() => URL.revokeObjectURL(objectUrl), 0);
4923
+ }
4924
+ else {
4925
+ URL.revokeObjectURL(objectUrl);
4926
+ }
4927
+ }
4709
4928
  }
4710
4929
  async function mergeMasks(context) {
4711
4930
  await flattenOverlayGroupToBaseImage(context, {
@@ -4744,6 +4963,69 @@ async function mergeAnnotations(context) {
4744
4963
  });
4745
4964
  }
4746
4965
 
4966
+ const SUPPORTED_IMAGE_EXTENSIONS = {
4967
+ png: 'image/png',
4968
+ jpg: 'image/jpeg',
4969
+ jpeg: 'image/jpeg',
4970
+ webp: 'image/webp',
4971
+ gif: 'image/gif',
4972
+ bmp: 'image/bmp',
4973
+ };
4974
+ const SUPPORTED_IMAGE_MIME_TYPES = new Set(Object.values(SUPPORTED_IMAGE_EXTENSIONS));
4975
+ function isSupportedImageDataUrl(value) {
4976
+ if (typeof value !== 'string')
4977
+ return false;
4978
+ if (!value.startsWith('data:image/'))
4979
+ return false;
4980
+ const match = /^data:(image\/[^;,]+)(?:[;,])/.exec(value);
4981
+ if (!match)
4982
+ return false;
4983
+ return SUPPORTED_IMAGE_MIME_TYPES.has(match[1].toLowerCase());
4984
+ }
4985
+ function inferImageMimeType(file) {
4986
+ var _a, _b;
4987
+ if (file.type && SUPPORTED_IMAGE_MIME_TYPES.has(file.type))
4988
+ return file.type;
4989
+ if (file.type)
4990
+ return null;
4991
+ const match = /\.([a-z0-9]+)$/i.exec(file.name);
4992
+ const ext = (_a = match === null || match === void 0 ? void 0 : match[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
4993
+ if (!ext)
4994
+ return null;
4995
+ return (_b = SUPPORTED_IMAGE_EXTENSIONS[ext]) !== null && _b !== void 0 ? _b : null;
4996
+ }
4997
+ function readFileAsDataUrl(file) {
4998
+ return new Promise((resolve, reject) => {
4999
+ const reader = new FileReader();
5000
+ reader.onload = () => {
5001
+ const fileReaderResult = reader.result;
5002
+ if (typeof fileReaderResult === 'string') {
5003
+ resolve(fileReaderResult);
5004
+ }
5005
+ else {
5006
+ reject(new Error('FileReader returned a non-string result'));
5007
+ }
5008
+ };
5009
+ reader.onerror = () => {
5010
+ var _a;
5011
+ reject((_a = reader.error) !== null && _a !== void 0 ? _a : new Error('FileReader error'));
5012
+ };
5013
+ reader.onabort = () => {
5014
+ reject(new Error('FileReader read aborted'));
5015
+ };
5016
+ reader.readAsDataURL(file);
5017
+ });
5018
+ }
5019
+ function resetFileInput(input) {
5020
+ if (!input)
5021
+ return;
5022
+ try {
5023
+ input.value = '';
5024
+ }
5025
+ catch {
5026
+ }
5027
+ }
5028
+
4747
5029
  function forceReflow(element) {
4748
5030
  if (!element)
4749
5031
  return;
@@ -4956,9 +5238,8 @@ function applyCanvasDimensions(canvas, width, height, containerElement) {
4956
5238
  }
4957
5239
 
4958
5240
  async function loadImage(context, imageBase64, loadOptions = {}) {
4959
- if (typeof imageBase64 !== 'string' || !imageBase64.startsWith('data:image/')) {
5241
+ if (!isSupportedImageDataUrl(imageBase64))
4960
5242
  return;
4961
- }
4962
5243
  const placeholderHidden = context.placeholderElement
4963
5244
  ? !!context.placeholderElement.hidden
4964
5245
  : null;
@@ -5348,6 +5629,41 @@ class TransformController {
5348
5629
  }
5349
5630
  this.context.saveCanvasState();
5350
5631
  }
5632
+ async flipHorizontal() {
5633
+ await this.flipImage('flipX');
5634
+ }
5635
+ async flipVertical() {
5636
+ await this.flipImage('flipY');
5637
+ }
5638
+ async flipImage(property) {
5639
+ const imageObject = this.context.getOriginalImage();
5640
+ if (!imageObject)
5641
+ return;
5642
+ if (this.context.guard.isAnimating())
5643
+ return;
5644
+ if (this.context.guard.isDisposed())
5645
+ return;
5646
+ try {
5647
+ const centre = imageObject.getCenterPoint();
5648
+ imageObject.set({ originX: 'center', originY: 'center' });
5649
+ imageObject.setPositionByOrigin(centre, 'center', 'center');
5650
+ imageObject.set({ [property]: !imageObject[property] });
5651
+ imageObject.setCoords();
5652
+ const newTopLeft = computeTopLeftPoint(imageObject);
5653
+ imageObject.set({ originX: 'left', originY: 'top' });
5654
+ imageObject.setPositionByOrigin(newTopLeft, 'left', 'top');
5655
+ imageObject.setCoords();
5656
+ }
5657
+ catch (error) {
5658
+ console.warn(`[ImageEditor] ${property === 'flipX' ? 'flipHorizontal' : 'flipVertical'} failed`, error);
5659
+ return;
5660
+ }
5661
+ if (this.context.guard.isDisposed())
5662
+ return;
5663
+ if (this.context.afterTransformSnap)
5664
+ this.context.afterTransformSnap();
5665
+ this.context.saveCanvasState();
5666
+ }
5351
5667
  async resetImageTransform() {
5352
5668
  if (!this.context.getOriginalImage())
5353
5669
  return;
@@ -5355,6 +5671,13 @@ class TransformController {
5355
5671
  try {
5356
5672
  await this.scaleImage(1);
5357
5673
  await this.rotateImage(0);
5674
+ const imageObject = this.context.getOriginalImage();
5675
+ if (imageObject && !this.context.guard.isDisposed()) {
5676
+ imageObject.set({ flipX: false, flipY: false });
5677
+ imageObject.setCoords();
5678
+ if (this.context.afterTransformSnap)
5679
+ this.context.afterTransformSnap();
5680
+ }
5358
5681
  }
5359
5682
  finally {
5360
5683
  this.context.setSuppressSaveState(false);
@@ -5998,59 +6321,6 @@ function setPlaceholderVisible(placeholderElement, containerElement, show) {
5998
6321
  }
5999
6322
  }
6000
6323
 
6001
- const SUPPORTED_IMAGE_EXTENSIONS = {
6002
- png: 'image/png',
6003
- jpg: 'image/jpeg',
6004
- jpeg: 'image/jpeg',
6005
- webp: 'image/webp',
6006
- gif: 'image/gif',
6007
- bmp: 'image/bmp',
6008
- };
6009
- const SUPPORTED_IMAGE_MIME_TYPES = new Set(Object.values(SUPPORTED_IMAGE_EXTENSIONS));
6010
- function inferImageMimeType(file) {
6011
- var _a, _b;
6012
- if (file.type && SUPPORTED_IMAGE_MIME_TYPES.has(file.type))
6013
- return file.type;
6014
- if (file.type)
6015
- return null;
6016
- const match = /\.([a-z0-9]+)$/i.exec(file.name);
6017
- const ext = (_a = match === null || match === void 0 ? void 0 : match[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
6018
- if (!ext)
6019
- return null;
6020
- return (_b = SUPPORTED_IMAGE_EXTENSIONS[ext]) !== null && _b !== void 0 ? _b : null;
6021
- }
6022
- function readFileAsDataUrl(file) {
6023
- return new Promise((resolve, reject) => {
6024
- const reader = new FileReader();
6025
- reader.onload = () => {
6026
- const fileReaderResult = reader.result;
6027
- if (typeof fileReaderResult === 'string') {
6028
- resolve(fileReaderResult);
6029
- }
6030
- else {
6031
- reject(new Error('FileReader returned a non-string result'));
6032
- }
6033
- };
6034
- reader.onerror = () => {
6035
- var _a;
6036
- reject((_a = reader.error) !== null && _a !== void 0 ? _a : new Error('FileReader error'));
6037
- };
6038
- reader.onabort = () => {
6039
- reject(new Error('FileReader read aborted'));
6040
- };
6041
- reader.readAsDataURL(file);
6042
- });
6043
- }
6044
- function resetFileInput(input) {
6045
- if (!input)
6046
- return;
6047
- try {
6048
- input.value = '';
6049
- }
6050
- catch {
6051
- }
6052
- }
6053
-
6054
6324
  const LAYOUT_EPSILON = 0.5;
6055
6325
  const INTERNAL_OPERATION_TOKEN = Symbol('ImageEditorInternalOperation');
6056
6326
  const INTERNAL_ALLOW_DURING_ANIMATION_QUEUE = Symbol('ImageEditorAllowDuringAnimationQueue');
@@ -6060,6 +6330,8 @@ const CROP_MODE_CONTROL_KEYS = [
6060
6330
  'rotateRightDegreesInput',
6061
6331
  'rotateLeftButton',
6062
6332
  'rotateRightButton',
6333
+ 'flipHorizontalButton',
6334
+ 'flipVerticalButton',
6063
6335
  'createMaskButton',
6064
6336
  'removeSelectedMaskButton',
6065
6337
  'removeAllMasksButton',
@@ -6088,6 +6360,7 @@ const CROP_MODE_CONTROL_KEYS = [
6088
6360
  'redoButton',
6089
6361
  'imageInput',
6090
6362
  'enterCropModeButton',
6363
+ 'cropAspectRatioSelect',
6091
6364
  'applyCropButton',
6092
6365
  'cancelCropButton',
6093
6366
  'enterMosaicModeButton',
@@ -6095,8 +6368,12 @@ const CROP_MODE_CONTROL_KEYS = [
6095
6368
  'mosaicBrushSizeInput',
6096
6369
  'mosaicBlockSizeInput',
6097
6370
  ];
6098
- const CROP_MODE_ENABLED_KEYS = ['applyCropButton', 'cancelCropButton'];
6099
- const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['applyCrop', 'cancelCrop']);
6371
+ const CROP_MODE_ENABLED_KEYS = [
6372
+ 'cropAspectRatioSelect',
6373
+ 'applyCropButton',
6374
+ 'cancelCropButton',
6375
+ ];
6376
+ const CROP_SESSION_ALLOWED_OPERATIONS = new Set(['setCropAspectRatio', 'applyCrop', 'cancelCrop']);
6100
6377
  const TEXT_MODE_ENABLED_KEYS = [
6101
6378
  'exitTextModeButton',
6102
6379
  'textColorInput',
@@ -6113,6 +6390,8 @@ const MOSAIC_MODE_CONTROL_KEYS = [
6113
6390
  'rotateRightDegreesInput',
6114
6391
  'rotateLeftButton',
6115
6392
  'rotateRightButton',
6393
+ 'flipHorizontalButton',
6394
+ 'flipVerticalButton',
6116
6395
  'createMaskButton',
6117
6396
  'removeSelectedMaskButton',
6118
6397
  'removeAllMasksButton',
@@ -6141,6 +6420,7 @@ const MOSAIC_MODE_CONTROL_KEYS = [
6141
6420
  'redoButton',
6142
6421
  'imageInput',
6143
6422
  'enterCropModeButton',
6423
+ 'cropAspectRatioSelect',
6144
6424
  'applyCropButton',
6145
6425
  'cancelCropButton',
6146
6426
  'enterMosaicModeButton',
@@ -6170,6 +6450,8 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
6170
6450
  'saveState',
6171
6451
  'scaleImage',
6172
6452
  'rotateImage',
6453
+ 'flipHorizontal',
6454
+ 'flipVertical',
6173
6455
  'resetImageTransform',
6174
6456
  'createMask',
6175
6457
  'removeSelectedMask',
@@ -6199,6 +6481,7 @@ const IMAGE_EDITOR_OPERATIONS = new Set([
6199
6481
  'bringSelectedObjectToFront',
6200
6482
  'sendSelectedObjectToBack',
6201
6483
  'enterCropMode',
6484
+ 'setCropAspectRatio',
6202
6485
  'applyCrop',
6203
6486
  'cancelCrop',
6204
6487
  'enterMosaicMode',
@@ -6555,6 +6838,8 @@ class ImageEditor {
6555
6838
  rotateRightDegreesInput: 'rotateRightDegreesInput',
6556
6839
  rotateLeftButton: 'rotateLeftButton',
6557
6840
  rotateRightButton: 'rotateRightButton',
6841
+ flipHorizontalButton: 'flipHorizontalButton',
6842
+ flipVerticalButton: 'flipVerticalButton',
6558
6843
  createMaskButton: 'createMaskButton',
6559
6844
  removeSelectedMaskButton: 'removeSelectedMaskButton',
6560
6845
  removeAllMasksButton: 'removeAllMasksButton',
@@ -6585,6 +6870,7 @@ class ImageEditor {
6585
6870
  redoButton: 'redoButton',
6586
6871
  imageInput: 'imageInput',
6587
6872
  enterCropModeButton: 'enterCropModeButton',
6873
+ cropAspectRatioSelect: 'cropAspectRatioSelect',
6588
6874
  applyCropButton: 'applyCropButton',
6589
6875
  cancelCropButton: 'cancelCropButton',
6590
6876
  enterMosaicModeButton: 'enterMosaicModeButton',
@@ -6692,6 +6978,12 @@ class ImageEditor {
6692
6978
  this.bindElementIfExists('resetImageTransformButton', 'click', () => {
6693
6979
  void this.resetImageTransform();
6694
6980
  });
6981
+ this.bindElementIfExists('flipHorizontalButton', 'click', () => {
6982
+ void this.flipHorizontal();
6983
+ });
6984
+ this.bindElementIfExists('flipVerticalButton', 'click', () => {
6985
+ void this.flipVertical();
6986
+ });
6695
6987
  this.bindElementIfExists('createMaskButton', 'click', () => {
6696
6988
  this.createMask();
6697
6989
  });
@@ -6776,7 +7068,11 @@ class ImageEditor {
6776
7068
  void this.rotateImage(this.currentRotation + step);
6777
7069
  });
6778
7070
  this.bindElementIfExists('enterCropModeButton', 'click', () => {
6779
- this.enterCropMode();
7071
+ this.enterCropMode({ aspectRatio: this.getSelectedCropAspectRatio() });
7072
+ });
7073
+ this.bindElementIfExists('cropAspectRatioSelect', 'change', () => {
7074
+ if (this.cropSession)
7075
+ this.setCropAspectRatio(this.getSelectedCropAspectRatio());
6780
7076
  });
6781
7077
  this.bindElementIfExists('applyCropButton', 'click', () => {
6782
7078
  void this.applyCrop().catch((error) => {
@@ -6929,7 +7225,7 @@ class ImageEditor {
6929
7225
  return;
6930
7226
  if (this.isDisposed)
6931
7227
  return;
6932
- if (typeof base64 !== 'string' || !base64.startsWith('data:image/'))
7228
+ if (!isSupportedImageDataUrl(base64))
6933
7229
  return;
6934
7230
  if (!this.canRunIdleOperation('loadImage', options))
6935
7231
  return;
@@ -7063,12 +7359,27 @@ class ImageEditor {
7063
7359
  return false;
7064
7360
  }
7065
7361
  }
7362
+ getSelectedCropAspectRatio() {
7363
+ const inputId = this.elements.cropAspectRatioSelect;
7364
+ const inputEl = inputId
7365
+ ? document.getElementById(inputId)
7366
+ : null;
7367
+ const value = inputEl && 'value' in inputEl ? String(inputEl.value).trim() : '';
7368
+ return (value || 'free');
7369
+ }
7066
7370
  isExpectedIdleGuardError(error, operationName) {
7067
7371
  return (error instanceof Error &&
7068
7372
  error.message.startsWith(`[ImageEditor] Cannot run "${operationName}" `));
7069
7373
  }
7070
7374
  assertCanQueueAnimation(operationName, options) {
7071
- this.operationGuard.assertCanQueueAnimation(operationName, this.getInternalOperationToken(options));
7375
+ const token = this.getInternalOperationToken(options);
7376
+ this.operationGuard.assertCanQueueAnimation(operationName, token);
7377
+ const activeToolMode = this.getActiveToolMode();
7378
+ if (activeToolMode &&
7379
+ !this.operationGuard.isOwnOperation(token) &&
7380
+ !TOOL_MODE_ALLOWED_OPERATIONS[activeToolMode].has(operationName)) {
7381
+ throw new Error(`[ImageEditor] Cannot run "${operationName}" while ${activeToolMode} mode is active.`);
7382
+ }
7072
7383
  }
7073
7384
  isImageLoaded() {
7074
7385
  var _a, _b;
@@ -7183,6 +7494,7 @@ class ImageEditor {
7183
7494
  return this.getActiveToolMode() !== null;
7184
7495
  }
7185
7496
  getEditorState() {
7497
+ var _a, _b;
7186
7498
  const canvasWidth = this.canvas ? this.canvas.getWidth() : 0;
7187
7499
  const canvasHeight = this.canvas ? this.canvas.getHeight() : 0;
7188
7500
  const image = this.getImageInfo();
@@ -7193,6 +7505,8 @@ class ImageEditor {
7193
7505
  annotationCount: this.getAnnotations().length,
7194
7506
  currentScale: this.currentScale,
7195
7507
  currentRotation: this.currentRotation,
7508
+ isFlippedHorizontally: !!((_a = this.originalImage) === null || _a === void 0 ? void 0 : _a.flipX),
7509
+ isFlippedVertically: !!((_b = this.originalImage) === null || _b === void 0 ? void 0 : _b.flipY),
7196
7510
  isBusy: this.isBusy(),
7197
7511
  activeToolMode: this.getActiveToolMode(),
7198
7512
  isCropMode: this.cropSession !== null,
@@ -7522,6 +7836,70 @@ class ImageEditor {
7522
7836
  this.emitBusyChangeIfChanged(context);
7523
7837
  });
7524
7838
  }
7839
+ flipHorizontal() {
7840
+ if (this.isDisposed || !this.transformController)
7841
+ return Promise.resolve();
7842
+ try {
7843
+ this.assertCanQueueAnimation('flipHorizontal');
7844
+ }
7845
+ catch (error) {
7846
+ return Promise.reject(error);
7847
+ }
7848
+ const controller = this.transformController;
7849
+ const context = this.buildCallbackContext('flipHorizontal', false);
7850
+ const job = this.animQueue.add(async () => {
7851
+ if (this.isDisposed)
7852
+ return;
7853
+ this.updateUi();
7854
+ try {
7855
+ await controller.flipHorizontal();
7856
+ if (!this.isDisposed)
7857
+ this.emitImageChanged(context);
7858
+ }
7859
+ finally {
7860
+ if (!this.isDisposed) {
7861
+ this.updateInputs();
7862
+ }
7863
+ }
7864
+ });
7865
+ this.emitBusyChangeIfChanged(context);
7866
+ return job.finally(() => {
7867
+ this.refreshUiAfterQueuedAnimation();
7868
+ this.emitBusyChangeIfChanged(context);
7869
+ });
7870
+ }
7871
+ flipVertical() {
7872
+ if (this.isDisposed || !this.transformController)
7873
+ return Promise.resolve();
7874
+ try {
7875
+ this.assertCanQueueAnimation('flipVertical');
7876
+ }
7877
+ catch (error) {
7878
+ return Promise.reject(error);
7879
+ }
7880
+ const controller = this.transformController;
7881
+ const context = this.buildCallbackContext('flipVertical', false);
7882
+ const job = this.animQueue.add(async () => {
7883
+ if (this.isDisposed)
7884
+ return;
7885
+ this.updateUi();
7886
+ try {
7887
+ await controller.flipVertical();
7888
+ if (!this.isDisposed)
7889
+ this.emitImageChanged(context);
7890
+ }
7891
+ finally {
7892
+ if (!this.isDisposed) {
7893
+ this.updateInputs();
7894
+ }
7895
+ }
7896
+ });
7897
+ this.emitBusyChangeIfChanged(context);
7898
+ return job.finally(() => {
7899
+ this.refreshUiAfterQueuedAnimation();
7900
+ this.emitBusyChangeIfChanged(context);
7901
+ });
7902
+ }
7525
7903
  resetImageTransform() {
7526
7904
  if (this.isDisposed || !this.transformController)
7527
7905
  return Promise.resolve();
@@ -8410,7 +8788,7 @@ class ImageEditor {
8410
8788
  this.updateUi();
8411
8789
  }
8412
8790
  }
8413
- downloadImage(options) {
8791
+ async downloadImage(options) {
8414
8792
  if (!this.canvas)
8415
8793
  return;
8416
8794
  if (!this.canRunIdleOperation('downloadImage'))
@@ -8421,7 +8799,7 @@ class ImageEditor {
8421
8799
  this.emitBusyChangeIfChanged(callbackContext);
8422
8800
  const exportContext = this.buildExportServiceContext();
8423
8801
  try {
8424
- downloadImage(exportContext, options);
8802
+ await downloadImage(exportContext, options);
8425
8803
  }
8426
8804
  finally {
8427
8805
  this.operationGuard.endBusyOperation(operationToken);
@@ -8696,7 +9074,7 @@ class ImageEditor {
8696
9074
  },
8697
9075
  };
8698
9076
  }
8699
- enterCropMode() {
9077
+ enterCropMode(options = {}) {
8700
9078
  if (!this.canvas || !this.originalImage)
8701
9079
  return;
8702
9080
  if (this.cropSession)
@@ -8706,12 +9084,23 @@ class ImageEditor {
8706
9084
  if (!this.canRunIdleOperation('enterCropMode'))
8707
9085
  return;
8708
9086
  const cropControllerContext = this.buildCropControllerContext();
8709
- enterCropMode(cropControllerContext);
9087
+ enterCropMode(cropControllerContext, options);
8710
9088
  this.updateUi();
8711
9089
  const callbackContext = this.buildCallbackContext('enterCropMode', false);
8712
9090
  this.emitBusyChangeIfChanged(callbackContext);
8713
9091
  this.emitImageChanged(callbackContext);
8714
9092
  }
9093
+ setCropAspectRatio(aspectRatio) {
9094
+ if (!this.canvas || !this.cropSession)
9095
+ return;
9096
+ if (!this.canRunIdleOperation('setCropAspectRatio'))
9097
+ return;
9098
+ const cropControllerContext = this.buildCropControllerContext();
9099
+ setCropAspectRatio(cropControllerContext, aspectRatio);
9100
+ this.updateUi();
9101
+ const callbackContext = this.buildCallbackContext('setCropAspectRatio', false);
9102
+ this.emitImageChanged(callbackContext);
9103
+ }
8715
9104
  cancelCrop() {
8716
9105
  if (!this.canvas || !this.cropSession)
8717
9106
  return;
@@ -8856,7 +9245,7 @@ class ImageEditor {
8856
9245
  }
8857
9246
  }
8858
9247
  updateUi() {
8859
- var _a;
9248
+ var _a, _b, _c;
8860
9249
  if (!this.canvas)
8861
9250
  return;
8862
9251
  const hasImage = !!this.originalImage;
@@ -8868,7 +9257,10 @@ class ImageEditor {
8868
9257
  const hasSelectedMask = !!(activeObject && isMaskObject(activeObject));
8869
9258
  const hasSelectedAnnotation = !!(activeObject && isAnnotationObject(activeObject));
8870
9259
  const hasSelectedEditableObject = !!activeObject && isEditableOverlayObject(activeObject);
8871
- const isDefaultTransform = this.currentScale === 1 && this.currentRotation === 0;
9260
+ const isDefaultTransform = this.currentScale === 1 &&
9261
+ this.currentRotation === 0 &&
9262
+ !((_a = this.originalImage) === null || _a === void 0 ? void 0 : _a.flipX) &&
9263
+ !((_b = this.originalImage) === null || _b === void 0 ? void 0 : _b.flipY);
8872
9264
  const canUndo = this.historyManager.canUndo();
8873
9265
  const canRedo = this.historyManager.canRedo();
8874
9266
  const isInCropMode = this.cropSession !== null;
@@ -8876,7 +9268,7 @@ class ImageEditor {
8876
9268
  const isInTextMode = this.textSession !== null;
8877
9269
  const isInDrawMode = this.drawSession !== null;
8878
9270
  const isBusy = this.operationGuard.isBusy() || this.animQueue.isBusy();
8879
- const isMosaicApplying = ((_a = this.mosaicSession) === null || _a === void 0 ? void 0 : _a.isApplying) === true;
9271
+ const isMosaicApplying = ((_c = this.mosaicSession) === null || _c === void 0 ? void 0 : _c.isApplying) === true;
8880
9272
  if (isInCropMode) {
8881
9273
  CROP_MODE_CONTROL_KEYS.forEach((key) => {
8882
9274
  this.setControlEnabled(key, !isBusy && CROP_MODE_ENABLED_KEYS.includes(key));
@@ -8909,6 +9301,8 @@ class ImageEditor {
8909
9301
  this.setControlEnabled('zoomOutButton', hasImage && !isBusy && this.currentScale > this.options.minScale);
8910
9302
  this.setControlEnabled('rotateLeftButton', hasImage && !isBusy);
8911
9303
  this.setControlEnabled('rotateRightButton', hasImage && !isBusy);
9304
+ this.setControlEnabled('flipHorizontalButton', hasImage && !isBusy);
9305
+ this.setControlEnabled('flipVerticalButton', hasImage && !isBusy);
8912
9306
  this.setControlEnabled('createMaskButton', hasImage && !isBusy);
8913
9307
  this.setControlEnabled('removeSelectedMaskButton', hasSelectedMask && !isBusy);
8914
9308
  this.setControlEnabled('removeAllMasksButton', hasMasks && !isBusy);
@@ -8926,6 +9320,7 @@ class ImageEditor {
8926
9320
  this.setControlEnabled('undoButton', hasImage && !isBusy && canUndo);
8927
9321
  this.setControlEnabled('redoButton', hasImage && !isBusy && canRedo);
8928
9322
  this.setControlEnabled('enterCropModeButton', hasImage && !isBusy);
9323
+ this.setControlEnabled('cropAspectRatioSelect', hasImage && !isBusy);
8929
9324
  this.setControlEnabled('enterMosaicModeButton', hasImage && !isBusy);
8930
9325
  this.setControlEnabled('enterTextModeButton', hasImage && !isBusy);
8931
9326
  this.setControlEnabled('enterDrawModeButton', hasImage && !isBusy);