@umituz/react-native-image 1.3.8 → 1.3.10
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/package.json +8 -2
- package/src/index.ts +5 -4
- package/src/infrastructure/services/ImageConversionService.ts +7 -32
- package/src/infrastructure/services/ImageEditorService.ts +41 -179
- package/src/infrastructure/services/{ImageAIEnhancementService.ts → ImageEnhanceService.ts} +42 -55
- package/src/infrastructure/services/ImageTemplateService.ts +10 -4
- package/src/infrastructure/services/ImageTransformService.ts +43 -93
- package/src/infrastructure/utils/FilterProcessor.ts +26 -263
- package/src/infrastructure/utils/{AIImageAnalysisUtils.ts → ImageAnalysisUtils.ts} +3 -3
- package/src/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
- package/src/infrastructure/utils/ImageFilterUtils.ts +152 -0
- package/src/infrastructure/utils/ImageTransformUtils.ts +25 -0
- package/src/infrastructure/utils/LayerManager.ts +0 -81
- package/src/presentation/components/editor/FilterPickerSheet.tsx +75 -0
- package/src/presentation/components/editor/StickerPickerSheet.tsx +62 -0
- package/src/presentation/components/editor/TextEditorSheet.tsx +98 -0
- package/src/presentation/components/editor/TextEditorTabs.tsx +111 -0
- package/src/presentation/hooks/useImage.ts +5 -8
- package/src/presentation/hooks/useImageEnhance.ts +32 -0
- package/src/presentation/hooks/useImageTransform.ts +3 -4
- package/src/infrastructure/services/ImageAdvancedTransformService.ts +0 -106
- package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +0 -57
- package/src/presentation/hooks/useImageAIEnhancement.ts +0 -33
|
@@ -9,131 +9,81 @@ import type {
|
|
|
9
9
|
ImageManipulateAction,
|
|
10
10
|
ImageSaveOptions,
|
|
11
11
|
ImageManipulationResult,
|
|
12
|
-
SaveFormat,
|
|
13
12
|
ImageCropArea,
|
|
14
13
|
ImageFlipOptions,
|
|
15
14
|
} from '../../domain/entities/ImageTypes';
|
|
16
|
-
import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
|
|
17
15
|
import { ImageUtils } from '../../domain/utils/ImageUtils';
|
|
18
16
|
import { ImageValidator } from '../utils/ImageValidator';
|
|
19
17
|
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
18
|
+
import { ImageTransformUtils } from '../utils/ImageTransformUtils';
|
|
20
19
|
|
|
21
20
|
export class ImageTransformService {
|
|
22
|
-
|
|
23
|
-
if (format === 'png') return ImageManipulator.SaveFormat.PNG;
|
|
24
|
-
if (format === 'webp') return ImageManipulator.SaveFormat.WEBP;
|
|
25
|
-
return ImageManipulator.SaveFormat.JPEG;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private static buildSaveOptions(options?: ImageSaveOptions): ImageManipulator.SaveOptions {
|
|
29
|
-
return {
|
|
30
|
-
compress: options?.compress ?? IMAGE_CONSTANTS.defaultQuality,
|
|
31
|
-
format: this.mapFormat(options?.format),
|
|
32
|
-
base64: options?.base64,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
static async resize(
|
|
37
|
-
uri: string,
|
|
38
|
-
width?: number,
|
|
39
|
-
height?: number,
|
|
40
|
-
options?: ImageSaveOptions
|
|
41
|
-
): Promise<ImageManipulationResult> {
|
|
21
|
+
static async resize(uri: string, width?: number, height?: number, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
42
22
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const dimValidation = ImageValidator.validateDimensions({ width, height });
|
|
49
|
-
if (!dimValidation.isValid) {
|
|
50
|
-
throw ImageErrorHandler.createError(dimValidation.error!, IMAGE_ERROR_CODES.INVALID_DIMENSIONS, 'resize');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return await ImageManipulator.manipulateAsync(
|
|
54
|
-
uri,
|
|
55
|
-
[{ resize: { width, height } }],
|
|
56
|
-
this.buildSaveOptions(options)
|
|
57
|
-
);
|
|
23
|
+
ImageValidator.validateUri(uri);
|
|
24
|
+
ImageValidator.validateDimensions({ width, height });
|
|
25
|
+
return await ImageManipulator.manipulateAsync(uri, [{ resize: { width, height } }], ImageTransformUtils.buildSaveOptions(options));
|
|
58
26
|
} catch (error) {
|
|
59
27
|
throw ImageErrorHandler.handleUnknownError(error, 'resize');
|
|
60
28
|
}
|
|
61
29
|
}
|
|
62
30
|
|
|
63
|
-
static async crop(
|
|
64
|
-
uri: string,
|
|
65
|
-
cropArea: ImageCropArea,
|
|
66
|
-
options?: ImageSaveOptions
|
|
67
|
-
): Promise<ImageManipulationResult> {
|
|
31
|
+
static async crop(uri: string, cropArea: ImageCropArea, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
68
32
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const dimValidation = ImageValidator.validateDimensions(cropArea);
|
|
75
|
-
if (!dimValidation.isValid) {
|
|
76
|
-
throw ImageErrorHandler.createError(dimValidation.error!, IMAGE_ERROR_CODES.INVALID_DIMENSIONS, 'crop');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return await ImageManipulator.manipulateAsync(
|
|
80
|
-
uri,
|
|
81
|
-
[{ crop: cropArea }],
|
|
82
|
-
this.buildSaveOptions(options)
|
|
83
|
-
);
|
|
33
|
+
ImageValidator.validateUri(uri);
|
|
34
|
+
ImageValidator.validateDimensions(cropArea);
|
|
35
|
+
return await ImageManipulator.manipulateAsync(uri, [{ crop: cropArea }], ImageTransformUtils.buildSaveOptions(options));
|
|
84
36
|
} catch (error) {
|
|
85
37
|
throw ImageErrorHandler.handleUnknownError(error, 'crop');
|
|
86
38
|
}
|
|
87
39
|
}
|
|
88
40
|
|
|
89
|
-
static async rotate(
|
|
90
|
-
uri: string,
|
|
91
|
-
degrees: number,
|
|
92
|
-
options?: ImageSaveOptions
|
|
93
|
-
): Promise<ImageManipulationResult> {
|
|
41
|
+
static async rotate(uri: string, degrees: number, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
94
42
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const rotationValidation = ImageValidator.validateRotation(degrees);
|
|
101
|
-
if (!rotationValidation.isValid) {
|
|
102
|
-
throw ImageErrorHandler.createError(rotationValidation.error!, IMAGE_ERROR_CODES.VALIDATION_ERROR, 'rotate');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return await ImageManipulator.manipulateAsync(
|
|
106
|
-
uri,
|
|
107
|
-
[{ rotate: degrees }],
|
|
108
|
-
this.buildSaveOptions(options)
|
|
109
|
-
);
|
|
43
|
+
ImageValidator.validateUri(uri);
|
|
44
|
+
ImageValidator.validateRotation(degrees);
|
|
45
|
+
return await ImageManipulator.manipulateAsync(uri, [{ rotate: degrees }], ImageTransformUtils.buildSaveOptions(options));
|
|
110
46
|
} catch (error) {
|
|
111
47
|
throw ImageErrorHandler.handleUnknownError(error, 'rotate');
|
|
112
48
|
}
|
|
113
49
|
}
|
|
114
50
|
|
|
115
|
-
static async flip(
|
|
116
|
-
uri: string,
|
|
117
|
-
flip: ImageFlipOptions,
|
|
118
|
-
options?: ImageSaveOptions
|
|
119
|
-
): Promise<ImageManipulationResult> {
|
|
51
|
+
static async flip(uri: string, flip: ImageFlipOptions, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
120
52
|
try {
|
|
121
|
-
|
|
122
|
-
if (!uriValidation.isValid) {
|
|
123
|
-
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'flip');
|
|
124
|
-
}
|
|
125
|
-
|
|
53
|
+
ImageValidator.validateUri(uri);
|
|
126
54
|
const actions: ImageManipulator.Action[] = [];
|
|
127
55
|
if (flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
128
56
|
if (flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
129
|
-
|
|
130
|
-
return await ImageManipulator.manipulateAsync(
|
|
131
|
-
uri,
|
|
132
|
-
actions,
|
|
133
|
-
this.buildSaveOptions(options)
|
|
134
|
-
);
|
|
57
|
+
return await ImageManipulator.manipulateAsync(uri, actions, ImageTransformUtils.buildSaveOptions(options));
|
|
135
58
|
} catch (error) {
|
|
136
59
|
throw ImageErrorHandler.handleUnknownError(error, 'flip');
|
|
137
60
|
}
|
|
138
61
|
}
|
|
62
|
+
|
|
63
|
+
static async manipulate(uri: string, action: ImageManipulateAction, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
64
|
+
try {
|
|
65
|
+
ImageValidator.validateUri(uri);
|
|
66
|
+
const actions: ImageManipulator.Action[] = [];
|
|
67
|
+
if (action.resize) actions.push({ resize: action.resize });
|
|
68
|
+
if (action.crop) actions.push({ crop: action.crop });
|
|
69
|
+
if (action.rotate) actions.push({ rotate: action.rotate });
|
|
70
|
+
if (action.flip) {
|
|
71
|
+
if (action.flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
72
|
+
if (action.flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
73
|
+
}
|
|
74
|
+
return await ImageManipulator.manipulateAsync(uri, actions, ImageTransformUtils.buildSaveOptions(options));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw ImageErrorHandler.handleUnknownError(error, 'manipulate');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static async resizeToFit(uri: string, maxWidth: number, maxHeight: number, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
81
|
+
const dimensions = ImageUtils.fitToSize(maxWidth, maxHeight, maxWidth, maxHeight);
|
|
82
|
+
return this.resize(uri, dimensions.width, dimensions.height, options);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static async cropToSquare(uri: string, width: number, height: number, options?: ImageSaveOptions): Promise<ImageManipulationResult> {
|
|
86
|
+
const cropArea = ImageUtils.getSquareCrop(width, height);
|
|
87
|
+
return this.crop(uri, cropArea, options);
|
|
88
|
+
}
|
|
139
89
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Infrastructure - Filter Processor
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Filter processing with preset management
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { ImageFilterUtils } from './ImageFilterUtils';
|
|
8
|
+
|
|
7
9
|
export interface FilterPreset {
|
|
8
10
|
id: string;
|
|
9
11
|
name: string;
|
|
@@ -35,92 +37,39 @@ export class FilterProcessor {
|
|
|
35
37
|
id: 'brightness',
|
|
36
38
|
name: 'Brightness',
|
|
37
39
|
category: 'basic',
|
|
38
|
-
parameters: [
|
|
39
|
-
{
|
|
40
|
-
name: 'brightness',
|
|
41
|
-
type: 'slider',
|
|
42
|
-
min: -100,
|
|
43
|
-
max: 100,
|
|
44
|
-
value: 0,
|
|
45
|
-
label: 'Brightness',
|
|
46
|
-
},
|
|
47
|
-
],
|
|
40
|
+
parameters: [{ name: 'brightness', type: 'slider', min: -100, max: 100, value: 0, label: 'Brightness' }],
|
|
48
41
|
},
|
|
49
42
|
{
|
|
50
43
|
id: 'contrast',
|
|
51
44
|
name: 'Contrast',
|
|
52
45
|
category: 'basic',
|
|
53
|
-
parameters: [
|
|
54
|
-
{
|
|
55
|
-
name: 'contrast',
|
|
56
|
-
type: 'slider',
|
|
57
|
-
min: -100,
|
|
58
|
-
max: 100,
|
|
59
|
-
value: 0,
|
|
60
|
-
label: 'Contrast',
|
|
61
|
-
},
|
|
62
|
-
],
|
|
46
|
+
parameters: [{ name: 'contrast', type: 'slider', min: -100, max: 100, value: 0, label: 'Contrast' }],
|
|
63
47
|
},
|
|
64
48
|
{
|
|
65
49
|
id: 'saturation',
|
|
66
50
|
name: 'Saturation',
|
|
67
51
|
category: 'color',
|
|
68
|
-
parameters: [
|
|
69
|
-
{
|
|
70
|
-
name: 'saturation',
|
|
71
|
-
type: 'slider',
|
|
72
|
-
min: -100,
|
|
73
|
-
max: 100,
|
|
74
|
-
value: 0,
|
|
75
|
-
label: 'Saturation',
|
|
76
|
-
},
|
|
77
|
-
],
|
|
52
|
+
parameters: [{ name: 'saturation', type: 'slider', min: -100, max: 100, value: 0, label: 'Saturation' }],
|
|
78
53
|
},
|
|
79
54
|
{
|
|
80
55
|
id: 'vintage',
|
|
81
56
|
name: 'Vintage',
|
|
82
57
|
category: 'vintage',
|
|
83
58
|
parameters: [
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
type: 'slider',
|
|
87
|
-
min: 0,
|
|
88
|
-
max: 100,
|
|
89
|
-
value: 50,
|
|
90
|
-
label: 'Intensity',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: 'warmth',
|
|
94
|
-
type: 'slider',
|
|
95
|
-
min: 0,
|
|
96
|
-
max: 100,
|
|
97
|
-
value: 30,
|
|
98
|
-
label: 'Warmth',
|
|
99
|
-
},
|
|
59
|
+
{ name: 'intensity', type: 'slider', min: 0, max: 100, value: 50, label: 'Intensity' },
|
|
60
|
+
{ name: 'warmth', type: 'slider', min: 0, max: 100, value: 30, label: 'Warmth' },
|
|
100
61
|
],
|
|
101
62
|
},
|
|
102
63
|
{
|
|
103
64
|
id: 'blur',
|
|
104
65
|
name: 'Blur',
|
|
105
66
|
category: 'artistic',
|
|
106
|
-
parameters: [
|
|
107
|
-
{
|
|
108
|
-
name: 'radius',
|
|
109
|
-
type: 'slider',
|
|
110
|
-
min: 0,
|
|
111
|
-
max: 20,
|
|
112
|
-
value: 0,
|
|
113
|
-
label: 'Blur Radius',
|
|
114
|
-
},
|
|
115
|
-
],
|
|
67
|
+
parameters: [{ name: 'radius', type: 'slider', min: 0, max: 20, value: 0, label: 'Blur Radius' }],
|
|
116
68
|
},
|
|
117
69
|
];
|
|
118
70
|
|
|
119
71
|
static getPresets(category?: string): FilterPreset[] {
|
|
120
|
-
|
|
121
|
-
return this.PRESETS.filter(preset => preset.category === category);
|
|
122
|
-
}
|
|
123
|
-
return this.PRESETS;
|
|
72
|
+
return category ? this.PRESETS.filter(preset => preset.category === category) : this.PRESETS;
|
|
124
73
|
}
|
|
125
74
|
|
|
126
75
|
static getPreset(id: string): FilterPreset | undefined {
|
|
@@ -129,233 +78,47 @@ export class FilterProcessor {
|
|
|
129
78
|
|
|
130
79
|
static createFilterState(presetId: string): FilterState {
|
|
131
80
|
const preset = this.getPreset(presetId);
|
|
132
|
-
if (!preset) {
|
|
133
|
-
throw new Error(`Filter preset not found: ${presetId}`);
|
|
134
|
-
}
|
|
81
|
+
if (!preset) throw new Error(`Filter preset not found: ${presetId}`);
|
|
135
82
|
|
|
136
83
|
const parameters: Record<string, any> = {};
|
|
137
|
-
preset.parameters.forEach(param => {
|
|
138
|
-
parameters[param.name] = param.value;
|
|
139
|
-
});
|
|
84
|
+
preset.parameters.forEach(param => { parameters[param.name] = param.value; });
|
|
140
85
|
|
|
141
|
-
return {
|
|
142
|
-
id: presetId,
|
|
143
|
-
intensity: 100,
|
|
144
|
-
parameters,
|
|
145
|
-
enabled: true,
|
|
146
|
-
};
|
|
86
|
+
return { id: presetId, intensity: 100, parameters, enabled: true };
|
|
147
87
|
}
|
|
148
88
|
|
|
149
89
|
static applyFilter(
|
|
150
|
-
imageData:
|
|
90
|
+
imageData: Uint8ClampedArray,
|
|
91
|
+
width: number,
|
|
92
|
+
height: number,
|
|
151
93
|
filterState: FilterState
|
|
152
|
-
):
|
|
94
|
+
): Uint8ClampedArray {
|
|
153
95
|
const preset = this.getPreset(filterState.id);
|
|
154
|
-
if (!preset || !filterState.enabled)
|
|
155
|
-
return imageData;
|
|
156
|
-
}
|
|
96
|
+
if (!preset || !filterState.enabled) return imageData;
|
|
157
97
|
|
|
158
|
-
let processedData = new Uint8ClampedArray(imageData
|
|
98
|
+
let processedData = new Uint8ClampedArray(imageData);
|
|
159
99
|
|
|
160
|
-
// Apply filter based on preset type
|
|
161
100
|
switch (filterState.id) {
|
|
162
101
|
case 'brightness':
|
|
163
|
-
processedData =
|
|
102
|
+
processedData = ImageFilterUtils.applyBrightness(processedData, filterState.parameters.brightness) as any;
|
|
164
103
|
break;
|
|
165
104
|
case 'contrast':
|
|
166
|
-
processedData =
|
|
105
|
+
processedData = ImageFilterUtils.applyContrast(processedData, filterState.parameters.contrast) as any;
|
|
167
106
|
break;
|
|
168
107
|
case 'saturation':
|
|
169
|
-
processedData =
|
|
108
|
+
processedData = ImageFilterUtils.applySaturation(processedData, filterState.parameters.saturation) as any;
|
|
170
109
|
break;
|
|
171
110
|
case 'vintage':
|
|
172
|
-
processedData =
|
|
111
|
+
processedData = ImageFilterUtils.applyVintage(processedData, filterState.parameters.intensity, filterState.parameters.warmth) as any;
|
|
173
112
|
break;
|
|
174
113
|
case 'blur':
|
|
175
|
-
processedData =
|
|
114
|
+
processedData = ImageFilterUtils.applyBlur(processedData, filterState.parameters.radius, width, height) as any;
|
|
176
115
|
break;
|
|
177
116
|
}
|
|
178
117
|
|
|
179
|
-
// Apply intensity
|
|
180
118
|
if (filterState.intensity < 100) {
|
|
181
|
-
processedData =
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
...imageData,
|
|
186
|
-
data: processedData,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private static applyBrightness(
|
|
191
|
-
data: Uint8ClampedArray,
|
|
192
|
-
brightness: number
|
|
193
|
-
): Uint8ClampedArray {
|
|
194
|
-
const result = new Uint8ClampedArray(data);
|
|
195
|
-
const adjustment = brightness * 2.55; // Convert -100 to 100 to -255 to 255
|
|
196
|
-
|
|
197
|
-
for (let i = 0; i < result.length; i += 4) {
|
|
198
|
-
result[i] = Math.min(255, Math.max(0, result[i] + adjustment));
|
|
199
|
-
result[i + 1] = Math.min(255, Math.max(0, result[i + 1] + adjustment));
|
|
200
|
-
result[i + 2] = Math.min(255, Math.max(0, result[i + 2] + adjustment));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private static applyContrast(
|
|
207
|
-
data: Uint8ClampedArray,
|
|
208
|
-
contrast: number
|
|
209
|
-
): Uint8ClampedArray {
|
|
210
|
-
const result = new Uint8ClampedArray(data);
|
|
211
|
-
const factor = (259 * (contrast * 255 + 255)) / (255 * (259 - contrast * 255));
|
|
212
|
-
|
|
213
|
-
for (let i = 0; i < result.length; i += 4) {
|
|
214
|
-
result[i] = Math.min(255, Math.max(0, factor * (result[i] - 128) + 128));
|
|
215
|
-
result[i + 1] = Math.min(255, Math.max(0, factor * (result[i + 1] - 128) + 128));
|
|
216
|
-
result[i + 2] = Math.min(255, Math.max(0, factor * (result[i + 2] - 128) + 128));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private static applySaturation(
|
|
223
|
-
data: Uint8ClampedArray,
|
|
224
|
-
saturation: number
|
|
225
|
-
): Uint8ClampedArray {
|
|
226
|
-
const result = new Uint8ClampedArray(data);
|
|
227
|
-
const adjustment = 1 + (saturation / 100);
|
|
228
|
-
|
|
229
|
-
for (let i = 0; i < result.length; i += 4) {
|
|
230
|
-
const r = result[i];
|
|
231
|
-
const g = result[i + 1];
|
|
232
|
-
const b = result[i + 2];
|
|
233
|
-
|
|
234
|
-
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
235
|
-
|
|
236
|
-
result[i] = Math.min(255, Math.max(0, gray + adjustment * (r - gray)));
|
|
237
|
-
result[i + 1] = Math.min(255, Math.max(0, gray + adjustment * (g - gray)));
|
|
238
|
-
result[i + 2] = Math.min(255, Math.max(0, gray + adjustment * (b - gray)));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private static applyVintage(
|
|
245
|
-
data: Uint8ClampedArray,
|
|
246
|
-
intensity: number,
|
|
247
|
-
warmth: number
|
|
248
|
-
): Uint8ClampedArray {
|
|
249
|
-
const result = new Uint8ClampedArray(data);
|
|
250
|
-
const factor = intensity / 100;
|
|
251
|
-
const warmFactor = warmth / 100;
|
|
252
|
-
|
|
253
|
-
for (let i = 0; i < result.length; i += 4) {
|
|
254
|
-
let r = result[i];
|
|
255
|
-
let g = result[i + 1];
|
|
256
|
-
let b = result[i + 2];
|
|
257
|
-
|
|
258
|
-
// Apply sepia effect
|
|
259
|
-
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
|
|
260
|
-
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
|
|
261
|
-
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
|
|
262
|
-
|
|
263
|
-
// Mix with original based on intensity
|
|
264
|
-
r = r * (1 - factor) + tr * factor;
|
|
265
|
-
g = g * (1 - factor) + tg * factor;
|
|
266
|
-
b = b * (1 - factor) + tb * factor;
|
|
267
|
-
|
|
268
|
-
// Apply warmth
|
|
269
|
-
if (warmFactor > 0) {
|
|
270
|
-
result[i] = Math.min(255, r + warmFactor * 20);
|
|
271
|
-
result[i + 1] = Math.min(255, g + warmFactor * 10);
|
|
272
|
-
result[i + 2] = Math.min(255, b * (1 - warmFactor * 0.3));
|
|
273
|
-
} else {
|
|
274
|
-
result[i] = r;
|
|
275
|
-
result[i + 1] = g;
|
|
276
|
-
result[i + 2] = Math.min(255, b * (1 - Math.abs(warmFactor) * 0.3));
|
|
277
|
-
}
|
|
119
|
+
processedData = ImageFilterUtils.applyIntensity(imageData, processedData, filterState.intensity / 100) as any;
|
|
278
120
|
}
|
|
279
121
|
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private static applyBlur(
|
|
284
|
-
data: Uint8ClampedArray,
|
|
285
|
-
radius: number
|
|
286
|
-
): Uint8ClampedArray {
|
|
287
|
-
// Simple box blur implementation
|
|
288
|
-
const result = new Uint8ClampedArray(data);
|
|
289
|
-
const width = Math.sqrt(data.length / 4);
|
|
290
|
-
const height = width;
|
|
291
|
-
const size = Math.floor(radius) || 1;
|
|
292
|
-
|
|
293
|
-
for (let y = 0; y < height; y++) {
|
|
294
|
-
for (let x = 0; x < width; x++) {
|
|
295
|
-
let r = 0, g = 0, b = 0, a = 0;
|
|
296
|
-
let count = 0;
|
|
297
|
-
|
|
298
|
-
for (let dy = -size; dy <= size; dy++) {
|
|
299
|
-
for (let dx = -size; dx <= size; dx++) {
|
|
300
|
-
const ny = y + dy;
|
|
301
|
-
const nx = x + dx;
|
|
302
|
-
|
|
303
|
-
if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
|
|
304
|
-
const idx = (ny * width + nx) * 4;
|
|
305
|
-
r += data[idx];
|
|
306
|
-
g += data[idx + 1];
|
|
307
|
-
b += data[idx + 2];
|
|
308
|
-
a += data[idx + 3];
|
|
309
|
-
count++;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const idx = (y * width + x) * 4;
|
|
315
|
-
result[idx] = r / count;
|
|
316
|
-
result[idx + 1] = g / count;
|
|
317
|
-
result[idx + 2] = b / count;
|
|
318
|
-
result[idx + 3] = a / count;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return result;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private static applyIntensity(
|
|
326
|
-
originalData: Uint8ClampedArray,
|
|
327
|
-
processedData: Uint8ClampedArray,
|
|
328
|
-
intensity: number
|
|
329
|
-
): Uint8ClampedArray {
|
|
330
|
-
const result = new Uint8ClampedArray(originalData.length);
|
|
331
|
-
|
|
332
|
-
for (let i = 0; i < originalData.length; i++) {
|
|
333
|
-
result[i] = originalData[i] * (1 - intensity) + processedData[i] * intensity;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return result;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
static createPreview(
|
|
340
|
-
imageData: ImageData,
|
|
341
|
-
filterState: FilterState,
|
|
342
|
-
previewSize: { width: number; height: number }
|
|
343
|
-
): ImageData {
|
|
344
|
-
// Create a smaller preview version
|
|
345
|
-
const previewCanvas = document.createElement('canvas') || {} as any;
|
|
346
|
-
previewCanvas.width = previewSize.width;
|
|
347
|
-
previewCanvas.height = previewSize.height;
|
|
348
|
-
const ctx = previewCanvas.getContext('2d');
|
|
349
|
-
|
|
350
|
-
if (!ctx) return imageData;
|
|
351
|
-
|
|
352
|
-
// Scale down the image for preview
|
|
353
|
-
ctx.drawImage(
|
|
354
|
-
{} as HTMLImageElement, // Would be the actual image
|
|
355
|
-
0, 0, previewSize.width, previewSize.height
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
const previewImageData = ctx.getImageData(0, 0, previewSize.width, previewSize.height);
|
|
359
|
-
return this.applyFilter(previewImageData, filterState);
|
|
122
|
+
return processedData as any;
|
|
360
123
|
}
|
|
361
124
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Image Infrastructure -
|
|
2
|
+
* Image Infrastructure - Image Analysis
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Image analysis utilities
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export class
|
|
7
|
+
export class ImageAnalysisUtils {
|
|
8
8
|
static calculateColorBalance(imageData: Uint8ClampedArray): {
|
|
9
9
|
redBalance: number;
|
|
10
10
|
greenBalance: number;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure - Editor History Utils
|
|
3
|
+
*
|
|
4
|
+
* Logic for managing editor state history and undo/redo
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type EditorState, type EditorHistory } from '../../domain/entities/EditorTypes';
|
|
8
|
+
|
|
9
|
+
export class ImageEditorHistoryUtils {
|
|
10
|
+
static addToHistory(
|
|
11
|
+
state: EditorState,
|
|
12
|
+
newHistory: EditorHistory,
|
|
13
|
+
maxHistory: number = 50
|
|
14
|
+
): EditorState {
|
|
15
|
+
const newHistoryArray = [...state.history.slice(0, state.historyIndex + 1), newHistory];
|
|
16
|
+
|
|
17
|
+
if (newHistoryArray.length > maxHistory) {
|
|
18
|
+
newHistoryArray.shift();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
history: newHistoryArray,
|
|
24
|
+
historyIndex: newHistoryArray.length - 1,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static undo(state: EditorState): EditorState {
|
|
29
|
+
if (state.historyIndex <= 0) return state;
|
|
30
|
+
|
|
31
|
+
const newIndex = state.historyIndex - 1;
|
|
32
|
+
const historyState = state.history[newIndex];
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...state,
|
|
36
|
+
layers: historyState.layers,
|
|
37
|
+
historyIndex: newIndex,
|
|
38
|
+
isDirty: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static redo(state: EditorState): EditorState {
|
|
43
|
+
if (state.historyIndex >= state.history.length - 1) return state;
|
|
44
|
+
|
|
45
|
+
const newIndex = state.historyIndex + 1;
|
|
46
|
+
const historyState = state.history[newIndex];
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
...state,
|
|
50
|
+
layers: historyState.layers,
|
|
51
|
+
historyIndex: newIndex,
|
|
52
|
+
isDirty: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static canUndo(state: EditorState): boolean {
|
|
57
|
+
return state.historyIndex > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static canRedo(state: EditorState): boolean {
|
|
61
|
+
return state.historyIndex < state.history.length - 1;
|
|
62
|
+
}
|
|
63
|
+
}
|