@umituz/react-native-image 1.1.4 → 1.1.6
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 +2 -2
- package/src/domain/entities/ImageConstants.ts +32 -15
- package/src/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/domain/entities/ImageTypes.ts +22 -7
- package/src/domain/utils/ImageUtils.ts +6 -6
- package/src/index.ts +47 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAdvancedTransformService.ts +106 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageConversionService.ts +51 -18
- 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/services/ImageStorageService.ts +22 -7
- package/src/infrastructure/services/ImageTransformService.ts +68 -101
- package/src/infrastructure/services/ImageViewerService.ts +3 -28
- package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
- package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
- package/src/infrastructure/utils/FilterEffects.ts +51 -0
- package/src/infrastructure/utils/ImageErrorHandler.ts +40 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/infrastructure/utils/ImageValidator.ts +59 -0
- package/src/presentation/components/GalleryHeader.tsx +3 -4
- package/src/presentation/components/ImageGallery.tsx +7 -20
- package/src/presentation/hooks/useImage.ts +35 -5
- 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/useImageConversion.ts +6 -3
- package/src/presentation/hooks/useImageEditor.ts +5 -11
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageGallery.ts +1 -60
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
- package/src/presentation/hooks/useImageOperation.ts +14 -10
- package/src/presentation/hooks/useImageTransform.ts +13 -7
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Image Storage Service
|
|
2
|
+
* Image Infrastructure - Storage Service
|
|
3
3
|
*
|
|
4
|
-
* Handles saving images to the device filesystem
|
|
5
|
-
* Wraps @umituz/react-native-filesystem.
|
|
4
|
+
* Handles saving images to the device filesystem
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { FileSystemService } from '@umituz/react-native-filesystem';
|
|
8
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
9
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
9
10
|
|
|
10
11
|
export class ImageStorageService {
|
|
11
12
|
static async saveImage(
|
|
12
13
|
uri: string,
|
|
13
14
|
filename?: string
|
|
14
|
-
): Promise<string
|
|
15
|
+
): Promise<string> {
|
|
15
16
|
try {
|
|
17
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
18
|
+
if (!uriValidation.isValid) {
|
|
19
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'saveImage');
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
const result = await FileSystemService.copyToDocuments(uri, filename);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
|
|
24
|
+
if (!result.success || !result.uri) {
|
|
25
|
+
throw ImageErrorHandler.createError(
|
|
26
|
+
'Failed to save image to filesystem',
|
|
27
|
+
IMAGE_ERROR_CODES.STORAGE_FAILED,
|
|
28
|
+
'saveImage'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result.uri;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw ImageErrorHandler.handleUnknownError(error, 'saveImage');
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Image Transform Service
|
|
2
|
+
* Image Infrastructure - Transform Service
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
5
|
-
* Also handles combined manipulations.
|
|
4
|
+
* Handles geometric transformations: resize, crop, rotate, flip
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import * as ImageManipulator from 'expo-image-manipulator';
|
|
@@ -11,58 +10,79 @@ import type {
|
|
|
11
10
|
ImageSaveOptions,
|
|
12
11
|
ImageManipulationResult,
|
|
13
12
|
SaveFormat,
|
|
13
|
+
ImageCropArea,
|
|
14
|
+
ImageFlipOptions,
|
|
14
15
|
} from '../../domain/entities/ImageTypes';
|
|
15
16
|
import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
|
|
16
17
|
import { ImageUtils } from '../../domain/utils/ImageUtils';
|
|
18
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
19
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
17
20
|
|
|
18
21
|
export class ImageTransformService {
|
|
19
|
-
|
|
20
|
-
* Helper to map SaveFormat to Manipulator format
|
|
21
|
-
*/
|
|
22
|
-
static mapFormat(format?: SaveFormat): ImageManipulator.SaveFormat {
|
|
22
|
+
private static mapFormat(format?: SaveFormat): ImageManipulator.SaveFormat {
|
|
23
23
|
if (format === 'png') return ImageManipulator.SaveFormat.PNG;
|
|
24
24
|
if (format === 'webp') return ImageManipulator.SaveFormat.WEBP;
|
|
25
25
|
return ImageManipulator.SaveFormat.JPEG;
|
|
26
26
|
}
|
|
27
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
|
+
|
|
28
36
|
static async resize(
|
|
29
37
|
uri: string,
|
|
30
38
|
width?: number,
|
|
31
39
|
height?: number,
|
|
32
40
|
options?: ImageSaveOptions
|
|
33
|
-
): Promise<ImageManipulationResult
|
|
41
|
+
): Promise<ImageManipulationResult> {
|
|
34
42
|
try {
|
|
43
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
44
|
+
if (!uriValidation.isValid) {
|
|
45
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'resize');
|
|
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
|
+
|
|
35
53
|
return await ImageManipulator.manipulateAsync(
|
|
36
54
|
uri,
|
|
37
55
|
[{ resize: { width, height } }],
|
|
38
|
-
|
|
39
|
-
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
40
|
-
format: ImageTransformService.mapFormat(options?.format),
|
|
41
|
-
base64: options?.base64,
|
|
42
|
-
}
|
|
56
|
+
this.buildSaveOptions(options)
|
|
43
57
|
);
|
|
44
|
-
} catch {
|
|
45
|
-
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw ImageErrorHandler.handleUnknownError(error, 'resize');
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
static async crop(
|
|
50
64
|
uri: string,
|
|
51
|
-
cropArea:
|
|
65
|
+
cropArea: ImageCropArea,
|
|
52
66
|
options?: ImageSaveOptions
|
|
53
|
-
): Promise<ImageManipulationResult
|
|
67
|
+
): Promise<ImageManipulationResult> {
|
|
54
68
|
try {
|
|
69
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
70
|
+
if (!uriValidation.isValid) {
|
|
71
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'crop');
|
|
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
|
+
|
|
55
79
|
return await ImageManipulator.manipulateAsync(
|
|
56
80
|
uri,
|
|
57
81
|
[{ crop: cropArea }],
|
|
58
|
-
|
|
59
|
-
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
60
|
-
format: ImageTransformService.mapFormat(options?.format),
|
|
61
|
-
base64: options?.base64,
|
|
62
|
-
}
|
|
82
|
+
this.buildSaveOptions(options)
|
|
63
83
|
);
|
|
64
|
-
} catch {
|
|
65
|
-
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw ImageErrorHandler.handleUnknownError(error, 'crop');
|
|
66
86
|
}
|
|
67
87
|
}
|
|
68
88
|
|
|
@@ -70,28 +90,39 @@ export class ImageTransformService {
|
|
|
70
90
|
uri: string,
|
|
71
91
|
degrees: number,
|
|
72
92
|
options?: ImageSaveOptions
|
|
73
|
-
): Promise<ImageManipulationResult
|
|
93
|
+
): Promise<ImageManipulationResult> {
|
|
74
94
|
try {
|
|
95
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
96
|
+
if (!uriValidation.isValid) {
|
|
97
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'rotate');
|
|
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
|
+
|
|
75
105
|
return await ImageManipulator.manipulateAsync(
|
|
76
106
|
uri,
|
|
77
107
|
[{ rotate: degrees }],
|
|
78
|
-
|
|
79
|
-
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
80
|
-
format: ImageTransformService.mapFormat(options?.format),
|
|
81
|
-
base64: options?.base64,
|
|
82
|
-
}
|
|
108
|
+
this.buildSaveOptions(options)
|
|
83
109
|
);
|
|
84
|
-
} catch {
|
|
85
|
-
|
|
110
|
+
} catch (error) {
|
|
111
|
+
throw ImageErrorHandler.handleUnknownError(error, 'rotate');
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
static async flip(
|
|
90
116
|
uri: string,
|
|
91
|
-
flip:
|
|
117
|
+
flip: ImageFlipOptions,
|
|
92
118
|
options?: ImageSaveOptions
|
|
93
|
-
): Promise<ImageManipulationResult
|
|
119
|
+
): Promise<ImageManipulationResult> {
|
|
94
120
|
try {
|
|
121
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
122
|
+
if (!uriValidation.isValid) {
|
|
123
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'flip');
|
|
124
|
+
}
|
|
125
|
+
|
|
95
126
|
const actions: ImageManipulator.Action[] = [];
|
|
96
127
|
if (flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
97
128
|
if (flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
@@ -99,74 +130,10 @@ export class ImageTransformService {
|
|
|
99
130
|
return await ImageManipulator.manipulateAsync(
|
|
100
131
|
uri,
|
|
101
132
|
actions,
|
|
102
|
-
|
|
103
|
-
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
104
|
-
format: ImageTransformService.mapFormat(options?.format),
|
|
105
|
-
base64: options?.base64,
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
} catch {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
static async manipulate(
|
|
114
|
-
uri: string,
|
|
115
|
-
action: ImageManipulateAction,
|
|
116
|
-
options?: ImageSaveOptions
|
|
117
|
-
): Promise<ImageManipulationResult | null> {
|
|
118
|
-
try {
|
|
119
|
-
const actions: ImageManipulator.Action[] = [];
|
|
120
|
-
if (action.resize) actions.push({ resize: action.resize });
|
|
121
|
-
if (action.crop) {
|
|
122
|
-
// @ts-ignore - guarded by check above
|
|
123
|
-
actions.push({ crop: action.crop });
|
|
124
|
-
}
|
|
125
|
-
if (action.rotate) actions.push({ rotate: action.rotate });
|
|
126
|
-
if (action.flip) {
|
|
127
|
-
if (action.flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
128
|
-
if (action.flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return await ImageManipulator.manipulateAsync(
|
|
132
|
-
uri,
|
|
133
|
-
actions,
|
|
134
|
-
{
|
|
135
|
-
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
136
|
-
format: ImageTransformService.mapFormat(options?.format),
|
|
137
|
-
base64: options?.base64,
|
|
138
|
-
}
|
|
133
|
+
this.buildSaveOptions(options)
|
|
139
134
|
);
|
|
140
|
-
} catch {
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
static async resizeToFit(
|
|
146
|
-
uri: string,
|
|
147
|
-
maxWidth: number,
|
|
148
|
-
maxHeight: number,
|
|
149
|
-
options?: ImageSaveOptions
|
|
150
|
-
): Promise<ImageManipulationResult | null> {
|
|
151
|
-
try {
|
|
152
|
-
const dimensions = ImageUtils.fitToSize(maxWidth, maxHeight, maxWidth, maxHeight);
|
|
153
|
-
return ImageTransformService.resize(uri, dimensions.width, dimensions.height, options);
|
|
154
|
-
} catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
static async cropToSquare(
|
|
160
|
-
uri: string,
|
|
161
|
-
width: number,
|
|
162
|
-
height: number,
|
|
163
|
-
options?: ImageSaveOptions
|
|
164
|
-
): Promise<ImageManipulationResult | null> {
|
|
165
|
-
try {
|
|
166
|
-
const cropArea = ImageUtils.getSquareCrop(width, height);
|
|
167
|
-
return ImageTransformService.crop(uri, cropArea, options);
|
|
168
|
-
} catch {
|
|
169
|
-
return null;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw ImageErrorHandler.handleUnknownError(error, 'flip');
|
|
170
137
|
}
|
|
171
138
|
}
|
|
172
139
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Image
|
|
2
|
+
* Image Infrastructure - Viewer Service
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Provides full-screen image viewer with zoom, swipe, and gallery features.
|
|
4
|
+
* Provides configuration for react-native-image-viewing component
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import type {
|
|
@@ -10,9 +9,6 @@ import type {
|
|
|
10
9
|
ImageGalleryOptions,
|
|
11
10
|
} from '../../domain/entities/ImageTypes';
|
|
12
11
|
|
|
13
|
-
/**
|
|
14
|
-
* Image viewer configuration
|
|
15
|
-
*/
|
|
16
12
|
export interface ImageViewerConfig {
|
|
17
13
|
images: ImageViewerItem[];
|
|
18
14
|
index?: number;
|
|
@@ -21,26 +17,11 @@ export interface ImageViewerConfig {
|
|
|
21
17
|
options?: ImageGalleryOptions;
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
/**
|
|
25
|
-
* Image viewer service
|
|
26
|
-
*
|
|
27
|
-
* NOTE: This service provides configuration for react-native-image-viewing component.
|
|
28
|
-
* The actual viewer component is rendered in the presentation layer.
|
|
29
|
-
*/
|
|
30
20
|
export class ImageViewerService {
|
|
31
|
-
/**
|
|
32
|
-
* Prepare images for viewer
|
|
33
|
-
* Converts image URIs to viewer format
|
|
34
|
-
*/
|
|
35
21
|
static prepareImages(uris: string[]): ImageViewerItem[] {
|
|
36
|
-
return uris.map(uri => ({
|
|
37
|
-
uri,
|
|
38
|
-
}));
|
|
22
|
+
return uris.map(uri => ({ uri }));
|
|
39
23
|
}
|
|
40
24
|
|
|
41
|
-
/**
|
|
42
|
-
* Prepare images with metadata
|
|
43
|
-
*/
|
|
44
25
|
static prepareImagesWithMetadata(items: ImageViewerItem[]): ImageViewerItem[] {
|
|
45
26
|
return items.map(item => ({
|
|
46
27
|
uri: item.uri,
|
|
@@ -51,9 +32,6 @@ export class ImageViewerService {
|
|
|
51
32
|
}));
|
|
52
33
|
}
|
|
53
34
|
|
|
54
|
-
/**
|
|
55
|
-
* Create viewer configuration
|
|
56
|
-
*/
|
|
57
35
|
static createViewerConfig(
|
|
58
36
|
images: ImageViewerItem[],
|
|
59
37
|
startIndex: number = 0,
|
|
@@ -74,9 +52,6 @@ export class ImageViewerService {
|
|
|
74
52
|
};
|
|
75
53
|
}
|
|
76
54
|
|
|
77
|
-
/**
|
|
78
|
-
* Get default gallery options
|
|
79
|
-
*/
|
|
80
55
|
static getDefaultOptions(): ImageGalleryOptions {
|
|
81
56
|
return {
|
|
82
57
|
index: 0,
|