@umituz/react-native-image 1.1.5 → 1.2.1
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 +25 -10
- package/src/domain/entities/EditorTypes.ts +180 -0
- package/src/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/index.ts +54 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageEditorService.ts +274 -0
- package/src/infrastructure/services/ImageFilterService.ts +168 -0
- package/src/infrastructure/services/ImageMetadataService.ts +187 -0
- package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +57 -0
- package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
- package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
- package/src/infrastructure/utils/CropTool.ts +260 -0
- package/src/infrastructure/utils/DrawingEngine.ts +210 -0
- package/src/infrastructure/utils/FilterEffects.ts +51 -0
- package/src/infrastructure/utils/FilterProcessor.ts +361 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/infrastructure/utils/LayerManager.ts +158 -0
- package/src/infrastructure/utils/ShapeRenderer.ts +168 -0
- package/src/infrastructure/utils/TextEditor.ts +273 -0
- package/src/presentation/components/CropComponent.tsx +183 -0
- package/src/presentation/components/Editor.tsx +261 -0
- package/src/presentation/components/EditorCanvas.tsx +135 -0
- package/src/presentation/components/EditorPanel.tsx +321 -0
- package/src/presentation/components/EditorToolbar.tsx +180 -0
- package/src/presentation/components/FilterSlider.tsx +123 -0
- package/src/presentation/components/GalleryHeader.tsx +87 -25
- package/src/presentation/components/ImageGallery.tsx +97 -48
- package/src/presentation/hooks/useEditorTools.ts +188 -0
- package/src/presentation/hooks/useImage.ts +33 -2
- package/src/presentation/hooks/useImageAIEnhancement.ts +33 -0
- package/src/presentation/hooks/useImageAnnotation.ts +32 -0
- package/src/presentation/hooks/useImageBatch.ts +33 -0
- package/src/presentation/hooks/useImageEditor.ts +165 -38
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Filter Service
|
|
3
|
+
*
|
|
4
|
+
* Advanced image filtering and effects using canvas and image processing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ImageFilter,
|
|
9
|
+
ImageFilterType,
|
|
10
|
+
ImageColorAdjustment,
|
|
11
|
+
ImageQualityMetrics,
|
|
12
|
+
ImageColorPalette,
|
|
13
|
+
} from '../../domain/entities/ImageFilterTypes';
|
|
14
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
15
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
16
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
17
|
+
import { FilterEffects } from '../utils/FilterEffects';
|
|
18
|
+
|
|
19
|
+
export class ImageFilterService {
|
|
20
|
+
private static createCanvasImageData(
|
|
21
|
+
width: number,
|
|
22
|
+
height: number,
|
|
23
|
+
data: Uint8ClampedArray
|
|
24
|
+
): ImageData {
|
|
25
|
+
return { data, width, height } as ImageData;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static applyBrightness(
|
|
29
|
+
imageData: ImageData,
|
|
30
|
+
intensity: number
|
|
31
|
+
): ImageData {
|
|
32
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
33
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
34
|
+
data[i] = Math.min(255, Math.max(0, data[i] + intensity * 255));
|
|
35
|
+
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] + intensity * 255));
|
|
36
|
+
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] + intensity * 255));
|
|
37
|
+
}
|
|
38
|
+
return ImageFilterService.createCanvasImageData(imageData.width, imageData.height, data);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private static applyContrast(
|
|
42
|
+
imageData: ImageData,
|
|
43
|
+
intensity: number
|
|
44
|
+
): ImageData {
|
|
45
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
46
|
+
const factor = (259 * (intensity * 255 + 255)) / (255 * (259 - intensity * 255));
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
49
|
+
data[i] = Math.min(255, Math.max(0, factor * (data[i] - 128) + 128));
|
|
50
|
+
data[i + 1] = Math.min(255, Math.max(0, factor * (data[i + 1] - 128) + 128));
|
|
51
|
+
data[i + 2] = Math.min(255, Math.max(0, factor * (data[i + 2] - 128) + 128));
|
|
52
|
+
}
|
|
53
|
+
return ImageFilterService.createCanvasImageData(imageData.width, imageData.height, data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static applyGrayscale(imageData: ImageData): ImageData {
|
|
57
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
58
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
59
|
+
const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
|
|
60
|
+
data[i] = gray;
|
|
61
|
+
data[i + 1] = gray;
|
|
62
|
+
data[i + 2] = gray;
|
|
63
|
+
}
|
|
64
|
+
return ImageFilterService.createCanvasImageData(imageData.width, imageData.height, data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static applySepia(imageData: ImageData, intensity: number = 1): ImageData {
|
|
68
|
+
return FilterEffects.applySepia(imageData, intensity);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
static async applyFilter(
|
|
74
|
+
uri: string,
|
|
75
|
+
filter: ImageFilter
|
|
76
|
+
): Promise<ImageManipulationResult> {
|
|
77
|
+
try {
|
|
78
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
79
|
+
if (!uriValidation.isValid) {
|
|
80
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'applyFilter');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// In a real implementation, we would:
|
|
84
|
+
// 1. Load the image into a canvas
|
|
85
|
+
// 2. Apply the filter to the pixel data
|
|
86
|
+
// 3. Export the canvas to a new URI
|
|
87
|
+
|
|
88
|
+
// For now, return a mock implementation
|
|
89
|
+
return {
|
|
90
|
+
uri, // Would be the processed URI
|
|
91
|
+
width: 0,
|
|
92
|
+
height: 0,
|
|
93
|
+
base64: undefined,
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw ImageErrorHandler.handleUnknownError(error, 'applyFilter');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static async applyColorAdjustment(
|
|
101
|
+
uri: string,
|
|
102
|
+
adjustment: ImageColorAdjustment
|
|
103
|
+
): Promise<ImageManipulationResult> {
|
|
104
|
+
try {
|
|
105
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
106
|
+
if (!uriValidation.isValid) {
|
|
107
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'applyColorAdjustment');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Apply brightness, contrast, saturation adjustments
|
|
111
|
+
return {
|
|
112
|
+
uri,
|
|
113
|
+
width: 0,
|
|
114
|
+
height: 0,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw ImageErrorHandler.handleUnknownError(error, 'applyColorAdjustment');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static async analyzeQuality(uri: string): Promise<ImageQualityMetrics> {
|
|
122
|
+
try {
|
|
123
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
124
|
+
if (!uriValidation.isValid) {
|
|
125
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'analyzeQuality');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Mock implementation - would analyze actual image data
|
|
129
|
+
return {
|
|
130
|
+
sharpness: Math.random() * 100,
|
|
131
|
+
brightness: Math.random() * 100,
|
|
132
|
+
contrast: Math.random() * 100,
|
|
133
|
+
colorfulness: Math.random() * 100,
|
|
134
|
+
overallQuality: Math.random() * 100,
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw ImageErrorHandler.handleUnknownError(error, 'analyzeQuality');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static async extractColorPalette(
|
|
142
|
+
uri: string,
|
|
143
|
+
colorCount: number = 5
|
|
144
|
+
): Promise<ImageColorPalette> {
|
|
145
|
+
try {
|
|
146
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
147
|
+
if (!uriValidation.isValid) {
|
|
148
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'extractColorPalette');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Mock implementation - would extract actual colors
|
|
152
|
+
const colors = Array.from({ length: colorCount }, () =>
|
|
153
|
+
`#${Math.floor(Math.random()*16777215).toString(16).padStart(6, '0')}`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
dominant: colors.slice(0, 3),
|
|
158
|
+
palette: colors.map((color, index) => ({
|
|
159
|
+
color,
|
|
160
|
+
percentage: Math.random() * 30 + 10,
|
|
161
|
+
population: Math.floor(Math.random() * 1000) + 100,
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw ImageErrorHandler.handleUnknownError(error, 'extractColorPalette');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Metadata Service
|
|
3
|
+
*
|
|
4
|
+
* Extracts and manages image metadata including EXIF data
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageMetadataExtended } from '../../domain/entities/ImageFilterTypes';
|
|
8
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
9
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
10
|
+
|
|
11
|
+
export interface ImageMetadataExtractionOptions {
|
|
12
|
+
includeExif?: boolean;
|
|
13
|
+
includeGPS?: boolean;
|
|
14
|
+
includeCamera?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ImageMetadataService {
|
|
18
|
+
private static async getImageDimensions(uri: string): Promise<{ width: number; height: number }> {
|
|
19
|
+
try {
|
|
20
|
+
// In a real implementation, we would use:
|
|
21
|
+
// - expo-image-manipulator for basic dimensions
|
|
22
|
+
// - react-native-image-picker for metadata
|
|
23
|
+
// - react-native-exif-reader for EXIF data
|
|
24
|
+
|
|
25
|
+
// Mock implementation
|
|
26
|
+
return {
|
|
27
|
+
width: Math.floor(Math.random() * 2000) + 100,
|
|
28
|
+
height: Math.floor(Math.random() * 2000) + 100,
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw ImageErrorHandler.handleUnknownError(error, 'getDimensions');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static async getFileSize(uri: string): Promise<number> {
|
|
36
|
+
try {
|
|
37
|
+
// In real implementation, use expo-file-system or similar
|
|
38
|
+
return Math.floor(Math.random() * 5000000) + 10000; // Random size between 10KB-5MB
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw ImageErrorHandler.handleUnknownError(error, 'getFileSize');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private static async extractExifData(uri: string): Promise<any> {
|
|
45
|
+
try {
|
|
46
|
+
// Mock EXIF data extraction
|
|
47
|
+
return {
|
|
48
|
+
DateTimeOriginal: new Date().toISOString(),
|
|
49
|
+
Make: 'Mock Camera',
|
|
50
|
+
Model: 'Mock Phone',
|
|
51
|
+
ISO: Math.floor(Math.random() * 1600) + 100,
|
|
52
|
+
FocalLength: Math.random() * 50 + 10,
|
|
53
|
+
Flash: Math.random() > 0.5,
|
|
54
|
+
ExposureTime: `1/${Math.floor(Math.random() * 1000) + 100}`,
|
|
55
|
+
FNumber: Math.random() * 8 + 1.4,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static async extractGPSData(uri: string): Promise<{ latitude: number; longitude: number } | null> {
|
|
63
|
+
try {
|
|
64
|
+
// Mock GPS data extraction
|
|
65
|
+
return Math.random() > 0.7 ? {
|
|
66
|
+
latitude: Math.random() * 180 - 90,
|
|
67
|
+
longitude: Math.random() * 360 - 180,
|
|
68
|
+
} : null;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static detectFormat(uri: string): string {
|
|
75
|
+
const extension = uri.toLowerCase().split('.').pop();
|
|
76
|
+
switch (extension) {
|
|
77
|
+
case 'jpg':
|
|
78
|
+
case 'jpeg':
|
|
79
|
+
return 'JPEG';
|
|
80
|
+
case 'png':
|
|
81
|
+
return 'PNG';
|
|
82
|
+
case 'webp':
|
|
83
|
+
return 'WebP';
|
|
84
|
+
case 'gif':
|
|
85
|
+
return 'GIF';
|
|
86
|
+
default:
|
|
87
|
+
return 'Unknown';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static async extractMetadata(
|
|
92
|
+
uri: string,
|
|
93
|
+
options: ImageMetadataExtractionOptions = {}
|
|
94
|
+
): Promise<ImageMetadataExtended> {
|
|
95
|
+
try {
|
|
96
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
97
|
+
if (!uriValidation.isValid) {
|
|
98
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'extractMetadata');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const {
|
|
102
|
+
includeExif = true,
|
|
103
|
+
includeGPS = true,
|
|
104
|
+
includeCamera = true,
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
// Get basic image info
|
|
108
|
+
const dimensions = await ImageMetadataService.getImageDimensions(uri);
|
|
109
|
+
const size = await ImageMetadataService.getFileSize(uri);
|
|
110
|
+
const format = ImageMetadataService.detectFormat(uri);
|
|
111
|
+
|
|
112
|
+
// Build metadata object
|
|
113
|
+
const metadata: ImageMetadataExtended = {
|
|
114
|
+
format,
|
|
115
|
+
size,
|
|
116
|
+
dimensions,
|
|
117
|
+
colorSpace: 'sRGB', // Default assumption
|
|
118
|
+
hasAlpha: format === 'PNG' || format === 'WebP',
|
|
119
|
+
orientation: 1,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Extract EXIF data if requested
|
|
123
|
+
if (includeExif) {
|
|
124
|
+
const exifData = await ImageMetadataService.extractExifData(uri);
|
|
125
|
+
if (exifData) {
|
|
126
|
+
metadata.creationDate = exifData.DateTimeOriginal ? new Date(exifData.DateTimeOriginal) : undefined;
|
|
127
|
+
metadata.modificationDate = new Date();
|
|
128
|
+
|
|
129
|
+
if (includeCamera) {
|
|
130
|
+
metadata.camera = {
|
|
131
|
+
make: exifData.Make,
|
|
132
|
+
model: exifData.Model,
|
|
133
|
+
iso: exifData.ISO,
|
|
134
|
+
flash: exifData.Flash,
|
|
135
|
+
focalLength: exifData.FocalLength,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Extract GPS data if requested
|
|
142
|
+
if (includeGPS) {
|
|
143
|
+
const gps = await ImageMetadataService.extractGPSData(uri);
|
|
144
|
+
metadata.gps = gps || undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return metadata;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw ImageErrorHandler.handleUnknownError(error, 'extractMetadata');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static async getBasicInfo(uri: string): Promise<{
|
|
154
|
+
format: string;
|
|
155
|
+
size: number;
|
|
156
|
+
dimensions: { width: number; height: number };
|
|
157
|
+
}> {
|
|
158
|
+
try {
|
|
159
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
160
|
+
if (!uriValidation.isValid) {
|
|
161
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'getBasicInfo');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const dimensions = await ImageMetadataService.getImageDimensions(uri);
|
|
165
|
+
const size = await ImageMetadataService.getFileSize(uri);
|
|
166
|
+
const format = ImageMetadataService.detectFormat(uri);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
format,
|
|
170
|
+
size,
|
|
171
|
+
dimensions,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw ImageErrorHandler.handleUnknownError(error, 'getBasicInfo');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static async hasMetadata(uri: string): Promise<boolean> {
|
|
179
|
+
try {
|
|
180
|
+
const exifData = await ImageMetadataService.extractExifData(uri);
|
|
181
|
+
const gpsData = await ImageMetadataService.extractGPSData(uri);
|
|
182
|
+
return !!(exifData || gpsData);
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Specialized Enhancement
|
|
3
|
+
*
|
|
4
|
+
* Portrait and landscape specific enhancement services
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
9
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
10
|
+
|
|
11
|
+
export class ImageSpecializedEnhancementService {
|
|
12
|
+
static async enhancePortrait(uri: string): Promise<ImageManipulationResult> {
|
|
13
|
+
try {
|
|
14
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
15
|
+
if (!uriValidation.isValid) {
|
|
16
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'enhancePortrait');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Portrait-specific enhancements:
|
|
20
|
+
// - Skin smoothing
|
|
21
|
+
// - Eye enhancement
|
|
22
|
+
// - Face detection and lighting adjustment
|
|
23
|
+
// - Background blur (bokeh effect)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
uri,
|
|
27
|
+
width: 0,
|
|
28
|
+
height: 0,
|
|
29
|
+
};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw ImageErrorHandler.handleUnknownError(error, 'enhancePortrait');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static async enhanceLandscape(uri: string): Promise<ImageManipulationResult> {
|
|
36
|
+
try {
|
|
37
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
38
|
+
if (!uriValidation.isValid) {
|
|
39
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'enhanceLandscape');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Landscape-specific enhancements:
|
|
43
|
+
// - Sky enhancement
|
|
44
|
+
// - Green tone adjustment
|
|
45
|
+
// - HDR effect simulation
|
|
46
|
+
// - Perspective correction
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
uri,
|
|
50
|
+
width: 0,
|
|
51
|
+
height: 0,
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw ImageErrorHandler.handleUnknownError(error, 'enhanceLandscape');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - AI Image Analysis
|
|
3
|
+
*
|
|
4
|
+
* Advanced image analysis utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class AIImageAnalysisUtils {
|
|
8
|
+
static calculateColorBalance(imageData: Uint8ClampedArray): {
|
|
9
|
+
redBalance: number;
|
|
10
|
+
greenBalance: number;
|
|
11
|
+
blueBalance: number;
|
|
12
|
+
} {
|
|
13
|
+
let redSum = 0, greenSum = 0, blueSum = 0;
|
|
14
|
+
const pixelCount = imageData.length / 4;
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
17
|
+
redSum += imageData[i];
|
|
18
|
+
greenSum += imageData[i + 1];
|
|
19
|
+
blueSum += imageData[i + 2];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const redMean = redSum / pixelCount;
|
|
23
|
+
const greenMean = greenSum / pixelCount;
|
|
24
|
+
const blueMean = blueSum / pixelCount;
|
|
25
|
+
const grayMean = (redMean + greenMean + blueMean) / 3;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
redBalance: (grayMean - redMean) / 255,
|
|
29
|
+
greenBalance: (grayMean - greenMean) / 255,
|
|
30
|
+
blueBalance: (grayMean - blueMean) / 255,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static detectNoise(imageData: Uint8ClampedArray): number {
|
|
35
|
+
let noiseLevel = 0;
|
|
36
|
+
let sampleCount = 0;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < imageData.length - 40; i += 40) {
|
|
39
|
+
const r1 = imageData[i];
|
|
40
|
+
const g1 = imageData[i + 1];
|
|
41
|
+
const b1 = imageData[i + 2];
|
|
42
|
+
|
|
43
|
+
const r2 = imageData[i + 4];
|
|
44
|
+
const g2 = imageData[i + 5];
|
|
45
|
+
const b2 = imageData[i + 6];
|
|
46
|
+
|
|
47
|
+
const diff = Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2);
|
|
48
|
+
noiseLevel += diff;
|
|
49
|
+
sampleCount++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return noiseLevel / (sampleCount * 3 * 255);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static applySharpening(imageData: Uint8ClampedArray): Uint8ClampedArray {
|
|
56
|
+
const result = new Uint8ClampedArray(imageData.length);
|
|
57
|
+
const width = Math.sqrt(imageData.length / 4);
|
|
58
|
+
const kernel = [
|
|
59
|
+
0, -1, 0,
|
|
60
|
+
-1, 5, -1,
|
|
61
|
+
0, -1, 0
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
65
|
+
const pixelIndex = i / 4;
|
|
66
|
+
const x = pixelIndex % width;
|
|
67
|
+
const y = Math.floor(pixelIndex / width);
|
|
68
|
+
|
|
69
|
+
for (let c = 0; c < 3; c++) {
|
|
70
|
+
let sum = 0;
|
|
71
|
+
for (let ky = -1; ky <= 1; ky++) {
|
|
72
|
+
for (let kx = -1; kx <= 1; kx++) {
|
|
73
|
+
const nx = x + kx;
|
|
74
|
+
const ny = y + ky;
|
|
75
|
+
|
|
76
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < width) {
|
|
77
|
+
const neighborIndex = (ny * width + nx) * 4 + c;
|
|
78
|
+
sum += imageData[neighborIndex] * kernel[(ky + 1) * 3 + (kx + 1)];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
result[i + c] = Math.min(255, Math.max(0, sum));
|
|
83
|
+
}
|
|
84
|
+
result[i + 3] = imageData[i + 3];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static applyNoiseReduction(imageData: Uint8ClampedArray): Uint8ClampedArray {
|
|
91
|
+
const result = new Uint8ClampedArray(imageData.length);
|
|
92
|
+
const width = Math.sqrt(imageData.length / 4);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
95
|
+
const pixelIndex = i / 4;
|
|
96
|
+
const x = pixelIndex % width;
|
|
97
|
+
const y = Math.floor(pixelIndex / width);
|
|
98
|
+
|
|
99
|
+
for (let c = 0; c < 3; c++) {
|
|
100
|
+
const values = [];
|
|
101
|
+
|
|
102
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
103
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
104
|
+
const nx = x + dx;
|
|
105
|
+
const ny = y + dy;
|
|
106
|
+
|
|
107
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < width) {
|
|
108
|
+
const neighborIndex = (ny * width + nx) * 4 + c;
|
|
109
|
+
values.push(imageData[neighborIndex]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
values.sort((a, b) => a - b);
|
|
115
|
+
result[i + c] = values[Math.floor(values.length / 2)];
|
|
116
|
+
}
|
|
117
|
+
result[i + 3] = imageData[i + 3];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Canvas Rendering Service
|
|
3
|
+
*
|
|
4
|
+
* Canvas-based rendering utilities for annotations and filters
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class CanvasRenderingService {
|
|
8
|
+
static renderTextOnCanvas(
|
|
9
|
+
ctx: CanvasRenderingContext2D,
|
|
10
|
+
overlay: import('../services/ImageAnnotationService').TextOverlay
|
|
11
|
+
): void {
|
|
12
|
+
ctx.save();
|
|
13
|
+
|
|
14
|
+
ctx.font = `${overlay.fontSize || 16}px ${overlay.fontFamily || 'Arial'}`;
|
|
15
|
+
ctx.fillStyle = overlay.color || '#000000';
|
|
16
|
+
|
|
17
|
+
if (overlay.backgroundColor) {
|
|
18
|
+
const metrics = ctx.measureText(overlay.text);
|
|
19
|
+
const padding = 4;
|
|
20
|
+
ctx.fillStyle = overlay.backgroundColor;
|
|
21
|
+
ctx.fillRect(
|
|
22
|
+
overlay.x - padding,
|
|
23
|
+
overlay.y - (overlay.fontSize || 16) - padding,
|
|
24
|
+
metrics.width + padding * 2,
|
|
25
|
+
(overlay.fontSize || 16) + padding * 2
|
|
26
|
+
);
|
|
27
|
+
ctx.fillStyle = overlay.color || '#000000';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (overlay.rotation) {
|
|
31
|
+
ctx.translate(overlay.x, overlay.y);
|
|
32
|
+
ctx.rotate((overlay.rotation * Math.PI) / 180);
|
|
33
|
+
ctx.fillText(overlay.text, 0, 0);
|
|
34
|
+
} else {
|
|
35
|
+
ctx.fillText(overlay.text, overlay.x, overlay.y);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ctx.restore();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static renderDrawingOnCanvas(
|
|
42
|
+
ctx: CanvasRenderingContext2D,
|
|
43
|
+
drawing: import('../services/ImageAnnotationService').DrawingElement
|
|
44
|
+
): void {
|
|
45
|
+
ctx.save();
|
|
46
|
+
ctx.strokeStyle = drawing.color || '#000000';
|
|
47
|
+
ctx.lineWidth = drawing.strokeWidth || 2;
|
|
48
|
+
|
|
49
|
+
if (drawing.fillColor) {
|
|
50
|
+
ctx.fillStyle = drawing.fillColor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (drawing.type) {
|
|
54
|
+
case 'line':
|
|
55
|
+
if (drawing.points.length >= 2) {
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
ctx.moveTo(drawing.points[0].x, drawing.points[0].y);
|
|
58
|
+
for (let i = 1; i < drawing.points.length; i++) {
|
|
59
|
+
ctx.lineTo(drawing.points[i].x, drawing.points[i].y);
|
|
60
|
+
}
|
|
61
|
+
ctx.stroke();
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'rectangle':
|
|
66
|
+
if (drawing.points.length >= 2) {
|
|
67
|
+
const width = drawing.points[1].x - drawing.points[0].x;
|
|
68
|
+
const height = drawing.points[1].y - drawing.points[0].y;
|
|
69
|
+
if (drawing.fillColor) {
|
|
70
|
+
ctx.fillRect(drawing.points[0].x, drawing.points[0].y, width, height);
|
|
71
|
+
}
|
|
72
|
+
ctx.strokeRect(drawing.points[0].x, drawing.points[0].y, width, height);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case 'circle':
|
|
77
|
+
if (drawing.points.length >= 2) {
|
|
78
|
+
const radius = Math.sqrt(
|
|
79
|
+
Math.pow(drawing.points[1].x - drawing.points[0].x, 2) +
|
|
80
|
+
Math.pow(drawing.points[1].y - drawing.points[0].y, 2)
|
|
81
|
+
);
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.arc(drawing.points[0].x, drawing.points[0].y, radius, 0, 2 * Math.PI);
|
|
84
|
+
if (drawing.fillColor) {
|
|
85
|
+
ctx.fill();
|
|
86
|
+
}
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'arrow':
|
|
92
|
+
if (drawing.points.length >= 2) {
|
|
93
|
+
// Draw line
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(drawing.points[0].x, drawing.points[0].y);
|
|
96
|
+
ctx.lineTo(drawing.points[1].x, drawing.points[1].y);
|
|
97
|
+
ctx.stroke();
|
|
98
|
+
|
|
99
|
+
// Draw arrowhead
|
|
100
|
+
const angle = Math.atan2(
|
|
101
|
+
drawing.points[1].y - drawing.points[0].y,
|
|
102
|
+
drawing.points[1].x - drawing.points[0].x
|
|
103
|
+
);
|
|
104
|
+
const arrowLength = 10;
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.moveTo(drawing.points[1].x, drawing.points[1].y);
|
|
107
|
+
ctx.lineTo(
|
|
108
|
+
drawing.points[1].x - arrowLength * Math.cos(angle - Math.PI / 6),
|
|
109
|
+
drawing.points[1].y - arrowLength * Math.sin(angle - Math.PI / 6)
|
|
110
|
+
);
|
|
111
|
+
ctx.moveTo(drawing.points[1].x, drawing.points[1].y);
|
|
112
|
+
ctx.lineTo(
|
|
113
|
+
drawing.points[1].x - arrowLength * Math.cos(angle + Math.PI / 6),
|
|
114
|
+
drawing.points[1].y - arrowLength * Math.sin(angle + Math.PI / 6)
|
|
115
|
+
);
|
|
116
|
+
ctx.stroke();
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'freehand':
|
|
121
|
+
if (drawing.points.length >= 2) {
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.moveTo(drawing.points[0].x, drawing.points[0].y);
|
|
124
|
+
for (let i = 1; i < drawing.points.length; i++) {
|
|
125
|
+
ctx.lineTo(drawing.points[i].x, drawing.points[i].y);
|
|
126
|
+
}
|
|
127
|
+
ctx.stroke();
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
ctx.restore();
|
|
133
|
+
}
|
|
134
|
+
}
|