@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.
@@ -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
+