@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.
- package/README.md +152 -96
- package/dist/cjs/index.cjs +534 -139
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/core/default-options.js +8 -6
- package/dist/esm/core/default-options.js.map +1 -1
- package/dist/esm/core/public-types.js.map +1 -1
- package/dist/esm/core/state-serializer.js +8 -0
- package/dist/esm/core/state-serializer.js.map +1 -1
- package/dist/esm/crop/crop-controller.js +218 -10
- package/dist/esm/crop/crop-controller.js.map +1 -1
- package/dist/esm/export/export-format.js.map +1 -1
- package/dist/esm/export/export-service.js +57 -56
- package/dist/esm/export/export-service.js.map +1 -1
- package/dist/esm/image/image-loader.js +2 -2
- package/dist/esm/image/image-loader.js.map +1 -1
- package/dist/esm/image/transform-controller.js +42 -0
- package/dist/esm/image/transform-controller.js.map +1 -1
- package/dist/esm/image-editor.js +139 -14
- package/dist/esm/image-editor.js.map +1 -1
- package/dist/esm/utils/file.js +10 -0
- package/dist/esm/utils/file.js.map +1 -1
- package/dist/types/core/default-options.d.ts.map +1 -1
- package/dist/types/core/public-types.d.ts +68 -50
- package/dist/types/core/public-types.d.ts.map +1 -1
- package/dist/types/core/state-serializer.d.ts +5 -1
- package/dist/types/core/state-serializer.d.ts.map +1 -1
- package/dist/types/crop/crop-controller.d.ts +12 -7
- package/dist/types/crop/crop-controller.d.ts.map +1 -1
- package/dist/types/export/export-format.d.ts +7 -10
- package/dist/types/export/export-format.d.ts.map +1 -1
- package/dist/types/export/export-service.d.ts +27 -32
- package/dist/types/export/export-service.d.ts.map +1 -1
- package/dist/types/export/overlay-merge-service.d.ts +3 -3
- package/dist/types/export/overlay-merge-service.d.ts.map +1 -1
- package/dist/types/image/image-loader.d.ts +5 -5
- package/dist/types/image/image-loader.d.ts.map +1 -1
- package/dist/types/image/transform-controller.d.ts +14 -7
- package/dist/types/image/transform-controller.d.ts.map +1 -1
- package/dist/types/image-editor.d.ts +22 -12
- package/dist/types/image-editor.d.ts.map +1 -1
- package/dist/types/index.d.cts +1 -1
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils/file.d.ts +13 -0
- package/dist/types/utils/file.d.ts.map +1 -1
- package/dist/umd/image-editor.umd.js +1 -1
- package/dist/umd/image-editor.umd.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -190,7 +190,7 @@ const DEFAULT_OPTIONS = {
|
|
|
190
190
|
groupSelection: false,
|
|
191
191
|
showPlaceholder: true,
|
|
192
192
|
initialImageBase64: null,
|
|
193
|
-
defaultDownloadFileName: 'edited_image
|
|
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: (
|
|
882
|
-
preserveMasksAfterCrop: (
|
|
883
|
-
allowRotationOfCropRect: (
|
|
884
|
-
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
|
-
|
|
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:
|
|
2967
|
-
height:
|
|
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
|
-
|
|
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
|
-
|
|
3026
|
-
|
|
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
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
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
|
|
4660
|
-
const
|
|
4661
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
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 (
|
|
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 = [
|
|
6099
|
-
|
|
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 (
|
|
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
|
-
|
|
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 &&
|
|
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 = ((
|
|
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);
|