@umituz/react-native-image 1.1.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/LICENSE +22 -0
- package/README.md +250 -0
- package/package.json +53 -0
- package/src/domain/entities/Image.ts +273 -0
- package/src/index.ts +55 -0
- package/src/infrastructure/services/ImageService.ts +316 -0
- package/src/infrastructure/services/ImageViewerService.ts +89 -0
- package/src/presentation/hooks/useImage.ts +393 -0
- package/src/presentation/hooks/useImageGallery.ts +149 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Domain - Image Service
|
|
3
|
+
*
|
|
4
|
+
* Service for image manipulation using expo-image-manipulator.
|
|
5
|
+
* Provides abstraction layer for resizing, cropping, rotating images.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: File operations use @umituz/react-native-filesystem (centralized)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
11
|
+
import { FileSystemService } from '@umituz/react-native-filesystem';
|
|
12
|
+
import type {
|
|
13
|
+
ImageManipulateAction,
|
|
14
|
+
ImageSaveOptions,
|
|
15
|
+
ImageManipulationResult,
|
|
16
|
+
SaveFormat,
|
|
17
|
+
} from '../../domain/entities/Image';
|
|
18
|
+
import { IMAGE_CONSTANTS, ImageUtils } from '../../domain/entities/Image';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Image manipulation service
|
|
22
|
+
*/
|
|
23
|
+
export class ImageService {
|
|
24
|
+
/**
|
|
25
|
+
* Resize image to specified dimensions
|
|
26
|
+
*/
|
|
27
|
+
static async resize(
|
|
28
|
+
uri: string,
|
|
29
|
+
width?: number,
|
|
30
|
+
height?: number,
|
|
31
|
+
options?: ImageSaveOptions
|
|
32
|
+
): Promise<ImageManipulationResult | null> {
|
|
33
|
+
try {
|
|
34
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
35
|
+
uri,
|
|
36
|
+
[{ resize: { width, height } }],
|
|
37
|
+
{
|
|
38
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
39
|
+
format: ImageService.mapFormatToManipulator(options?.format),
|
|
40
|
+
base64: options?.base64,
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return result;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Crop image to specified area
|
|
52
|
+
*/
|
|
53
|
+
static async crop(
|
|
54
|
+
uri: string,
|
|
55
|
+
cropArea: { originX: number; originY: number; width: number; height: number },
|
|
56
|
+
options?: ImageSaveOptions
|
|
57
|
+
): Promise<ImageManipulationResult | null> {
|
|
58
|
+
try {
|
|
59
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
60
|
+
uri,
|
|
61
|
+
[{ crop: cropArea }],
|
|
62
|
+
{
|
|
63
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
64
|
+
format: ImageService.mapFormatToManipulator(options?.format),
|
|
65
|
+
base64: options?.base64,
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Rotate image by degrees
|
|
77
|
+
*/
|
|
78
|
+
static async rotate(
|
|
79
|
+
uri: string,
|
|
80
|
+
degrees: number,
|
|
81
|
+
options?: ImageSaveOptions
|
|
82
|
+
): Promise<ImageManipulationResult | null> {
|
|
83
|
+
try {
|
|
84
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
85
|
+
uri,
|
|
86
|
+
[{ rotate: degrees }],
|
|
87
|
+
{
|
|
88
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
89
|
+
format: ImageService.mapFormatToManipulator(options?.format),
|
|
90
|
+
base64: options?.base64,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Flip image horizontally or vertically
|
|
102
|
+
*/
|
|
103
|
+
static async flip(
|
|
104
|
+
uri: string,
|
|
105
|
+
flip: { horizontal?: boolean; vertical?: boolean },
|
|
106
|
+
options?: ImageSaveOptions
|
|
107
|
+
): Promise<ImageManipulationResult | null> {
|
|
108
|
+
try {
|
|
109
|
+
const actions: ImageManipulator.Action[] = [];
|
|
110
|
+
|
|
111
|
+
if (flip.horizontal) {
|
|
112
|
+
actions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (flip.vertical) {
|
|
116
|
+
actions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
120
|
+
uri,
|
|
121
|
+
actions,
|
|
122
|
+
{
|
|
123
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
124
|
+
format: ImageService.mapFormatToManipulator(options?.format),
|
|
125
|
+
base64: options?.base64,
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Perform multiple image manipulations
|
|
137
|
+
*/
|
|
138
|
+
static async manipulate(
|
|
139
|
+
uri: string,
|
|
140
|
+
action: ImageManipulateAction,
|
|
141
|
+
options?: ImageSaveOptions
|
|
142
|
+
): Promise<ImageManipulationResult | null> {
|
|
143
|
+
try {
|
|
144
|
+
const manipulatorActions: ImageManipulator.Action[] = [];
|
|
145
|
+
|
|
146
|
+
if (action.resize) {
|
|
147
|
+
manipulatorActions.push({ resize: action.resize });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (action.crop) {
|
|
151
|
+
manipulatorActions.push({ crop: action.crop });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action.rotate) {
|
|
155
|
+
manipulatorActions.push({ rotate: action.rotate });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (action.flip) {
|
|
159
|
+
if (action.flip.horizontal) {
|
|
160
|
+
manipulatorActions.push({ flip: ImageManipulator.FlipType.Horizontal });
|
|
161
|
+
}
|
|
162
|
+
if (action.flip.vertical) {
|
|
163
|
+
manipulatorActions.push({ flip: ImageManipulator.FlipType.Vertical });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
168
|
+
uri,
|
|
169
|
+
manipulatorActions,
|
|
170
|
+
{
|
|
171
|
+
compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
172
|
+
format: ImageService.mapFormatToManipulator(options?.format),
|
|
173
|
+
base64: options?.base64,
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Compress image to reduce file size
|
|
185
|
+
*/
|
|
186
|
+
static async compress(
|
|
187
|
+
uri: string,
|
|
188
|
+
quality: number = IMAGE_CONSTANTS.DEFAULT_QUALITY
|
|
189
|
+
): Promise<ImageManipulationResult | null> {
|
|
190
|
+
try {
|
|
191
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
192
|
+
uri,
|
|
193
|
+
[],
|
|
194
|
+
{
|
|
195
|
+
compress: quality,
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resize image to fit within max dimensions (maintaining aspect ratio)
|
|
207
|
+
*/
|
|
208
|
+
static async resizeToFit(
|
|
209
|
+
uri: string,
|
|
210
|
+
maxWidth: number,
|
|
211
|
+
maxHeight: number,
|
|
212
|
+
options?: ImageSaveOptions
|
|
213
|
+
): Promise<ImageManipulationResult | null> {
|
|
214
|
+
try {
|
|
215
|
+
// Use ImageUtils to calculate fitted dimensions
|
|
216
|
+
const dimensions = ImageUtils.fitToSize(maxWidth, maxHeight, maxWidth, maxHeight);
|
|
217
|
+
|
|
218
|
+
return ImageService.resize(uri, dimensions.width, dimensions.height, options);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Create thumbnail (small preview image)
|
|
226
|
+
*/
|
|
227
|
+
static async createThumbnail(
|
|
228
|
+
uri: string,
|
|
229
|
+
size: number = IMAGE_CONSTANTS.THUMBNAIL_SIZE,
|
|
230
|
+
options?: ImageSaveOptions
|
|
231
|
+
): Promise<ImageManipulationResult | null> {
|
|
232
|
+
try {
|
|
233
|
+
return ImageService.resizeToFit(uri, size, size, {
|
|
234
|
+
...options,
|
|
235
|
+
compress: options?.compress || IMAGE_CONSTANTS.COMPRESS_QUALITY.MEDIUM,
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Crop image to square (centered)
|
|
244
|
+
*/
|
|
245
|
+
static async cropToSquare(
|
|
246
|
+
uri: string,
|
|
247
|
+
width: number,
|
|
248
|
+
height: number,
|
|
249
|
+
options?: ImageSaveOptions
|
|
250
|
+
): Promise<ImageManipulationResult | null> {
|
|
251
|
+
try {
|
|
252
|
+
const cropArea = ImageUtils.getSquareCrop(width, height);
|
|
253
|
+
return ImageService.crop(uri, cropArea, options);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Convert image format
|
|
261
|
+
*/
|
|
262
|
+
static async convertFormat(
|
|
263
|
+
uri: string,
|
|
264
|
+
format: SaveFormat,
|
|
265
|
+
quality?: number
|
|
266
|
+
): Promise<ImageManipulationResult | null> {
|
|
267
|
+
try {
|
|
268
|
+
const result = await ImageManipulator.manipulateAsync(
|
|
269
|
+
uri,
|
|
270
|
+
[],
|
|
271
|
+
{
|
|
272
|
+
compress: quality || IMAGE_CONSTANTS.DEFAULT_QUALITY,
|
|
273
|
+
format: ImageService.mapFormatToManipulator(format),
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Save manipulated image to device using FileSystemService
|
|
285
|
+
*/
|
|
286
|
+
static async saveImage(
|
|
287
|
+
uri: string,
|
|
288
|
+
filename?: string
|
|
289
|
+
): Promise<string | null> {
|
|
290
|
+
try {
|
|
291
|
+
const result = await FileSystemService.copyToDocuments(uri, filename);
|
|
292
|
+
return result.success ? result.uri : null;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Map SaveFormat to ImageManipulator.SaveFormat
|
|
300
|
+
*/
|
|
301
|
+
private static mapFormatToManipulator(
|
|
302
|
+
format?: SaveFormat
|
|
303
|
+
): ImageManipulator.SaveFormat {
|
|
304
|
+
if (!format || format === 'jpeg') {
|
|
305
|
+
return ImageManipulator.SaveFormat.JPEG;
|
|
306
|
+
}
|
|
307
|
+
if (format === 'png') {
|
|
308
|
+
return ImageManipulator.SaveFormat.PNG;
|
|
309
|
+
}
|
|
310
|
+
if (format === 'webp') {
|
|
311
|
+
return ImageManipulator.SaveFormat.WEBP;
|
|
312
|
+
}
|
|
313
|
+
return ImageManipulator.SaveFormat.JPEG;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Domain - Image Viewer Service
|
|
3
|
+
*
|
|
4
|
+
* Service for image viewing and gallery using react-native-image-viewing.
|
|
5
|
+
* Provides full-screen image viewer with zoom, swipe, and gallery features.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ImageViewerItem,
|
|
10
|
+
ImageGalleryOptions,
|
|
11
|
+
} from '../../domain/entities/Image';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Image viewer configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface ImageViewerConfig {
|
|
17
|
+
images: ImageViewerItem[];
|
|
18
|
+
index?: number;
|
|
19
|
+
visible: boolean;
|
|
20
|
+
onDismiss?: () => void;
|
|
21
|
+
options?: ImageGalleryOptions;
|
|
22
|
+
}
|
|
23
|
+
|
|
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
|
+
export class ImageViewerService {
|
|
31
|
+
/**
|
|
32
|
+
* Prepare images for viewer
|
|
33
|
+
* Converts image URIs to viewer format
|
|
34
|
+
*/
|
|
35
|
+
static prepareImages(uris: string[]): ImageViewerItem[] {
|
|
36
|
+
return uris.map(uri => ({
|
|
37
|
+
uri,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prepare images with metadata
|
|
43
|
+
*/
|
|
44
|
+
static prepareImagesWithMetadata(items: ImageViewerItem[]): ImageViewerItem[] {
|
|
45
|
+
return items.map(item => ({
|
|
46
|
+
uri: item.uri,
|
|
47
|
+
title: item.title,
|
|
48
|
+
description: item.description,
|
|
49
|
+
width: item.width,
|
|
50
|
+
height: item.height,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create viewer configuration
|
|
56
|
+
*/
|
|
57
|
+
static createViewerConfig(
|
|
58
|
+
images: ImageViewerItem[],
|
|
59
|
+
startIndex: number = 0,
|
|
60
|
+
onDismiss?: () => void,
|
|
61
|
+
options?: ImageGalleryOptions
|
|
62
|
+
): ImageViewerConfig {
|
|
63
|
+
return {
|
|
64
|
+
images,
|
|
65
|
+
index: options?.index ?? startIndex,
|
|
66
|
+
visible: true,
|
|
67
|
+
onDismiss: onDismiss || options?.onDismiss,
|
|
68
|
+
options: {
|
|
69
|
+
backgroundColor: options?.backgroundColor || '#000000',
|
|
70
|
+
swipeToCloseEnabled: options?.swipeToCloseEnabled ?? true,
|
|
71
|
+
doubleTapToZoomEnabled: options?.doubleTapToZoomEnabled ?? true,
|
|
72
|
+
onIndexChange: options?.onIndexChange,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get default gallery options
|
|
79
|
+
*/
|
|
80
|
+
static getDefaultOptions(): ImageGalleryOptions {
|
|
81
|
+
return {
|
|
82
|
+
index: 0,
|
|
83
|
+
backgroundColor: '#000000',
|
|
84
|
+
swipeToCloseEnabled: true,
|
|
85
|
+
doubleTapToZoomEnabled: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|