@umituz/react-native-image 1.1.0 → 1.1.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.
@@ -1,273 +0,0 @@
1
- /**
2
- * Image Domain - Core Entities
3
- *
4
- * This file defines core types and interfaces for image manipulation and viewing.
5
- * Handles image operations using expo-image-manipulator and react-native-image-viewing.
6
- */
7
-
8
- /**
9
- * Image format types
10
- */
11
- export enum ImageFormat {
12
- JPEG = 'jpeg',
13
- PNG = 'png',
14
- WEBP = 'webp',
15
- }
16
-
17
- /**
18
- * Image save format for expo-image-manipulator
19
- */
20
- export type SaveFormat = 'jpeg' | 'png' | 'webp';
21
-
22
- /**
23
- * Image manipulation action types
24
- */
25
- export interface ImageManipulateAction {
26
- resize?: { width?: number; height?: number };
27
- crop?: { originX: number; originY: number; width: number; height: number };
28
- rotate?: number; // degrees
29
- flip?: { vertical?: boolean; horizontal?: boolean };
30
- }
31
-
32
- /**
33
- * Image save options
34
- */
35
- export interface ImageSaveOptions {
36
- format?: SaveFormat;
37
- compress?: number; // 0-1
38
- base64?: boolean;
39
- }
40
-
41
- /**
42
- * Image manipulation result
43
- */
44
- export interface ImageManipulationResult {
45
- uri: string;
46
- width: number;
47
- height: number;
48
- base64?: string;
49
- }
50
-
51
- /**
52
- * Image metadata
53
- */
54
- export interface ImageMetadata {
55
- uri: string;
56
- width: number;
57
- height: number;
58
- format?: ImageFormat;
59
- size?: number; // bytes
60
- orientation?: ImageOrientation;
61
- }
62
-
63
- /**
64
- * Image orientation
65
- */
66
- export enum ImageOrientation {
67
- PORTRAIT = 'portrait',
68
- LANDSCAPE = 'landscape',
69
- SQUARE = 'square',
70
- }
71
-
72
- /**
73
- * Image viewer item
74
- */
75
- export interface ImageViewerItem {
76
- uri: string;
77
- title?: string;
78
- description?: string;
79
- width?: number;
80
- height?: number;
81
- }
82
-
83
- /**
84
- * Image gallery options
85
- */
86
- export interface ImageGalleryOptions {
87
- index?: number; // Starting index
88
- backgroundColor?: string;
89
- swipeToCloseEnabled?: boolean;
90
- doubleTapToZoomEnabled?: boolean;
91
- onDismiss?: () => void;
92
- onIndexChange?: (index: number) => void;
93
- }
94
-
95
- /**
96
- * Image operation result
97
- */
98
- export interface ImageOperationResult {
99
- success: boolean;
100
- uri?: string;
101
- error?: string;
102
- width?: number;
103
- height?: number;
104
- }
105
-
106
- /**
107
- * Image constants
108
- */
109
- export const IMAGE_CONSTANTS = {
110
- MAX_WIDTH: 2048,
111
- MAX_HEIGHT: 2048,
112
- DEFAULT_QUALITY: 0.8,
113
- THUMBNAIL_SIZE: 200,
114
- COMPRESS_QUALITY: {
115
- LOW: 0.5,
116
- MEDIUM: 0.7,
117
- HIGH: 0.9,
118
- },
119
- FORMAT: {
120
- JPEG: 'jpeg' as SaveFormat,
121
- PNG: 'png' as SaveFormat,
122
- WEBP: 'webp' as SaveFormat,
123
- },
124
- } as const;
125
-
126
- /**
127
- * Image utilities
128
- */
129
- export class ImageUtils {
130
- /**
131
- * Get image orientation from dimensions
132
- */
133
- static getOrientation(width: number, height: number): ImageOrientation {
134
- if (width > height) return ImageOrientation.LANDSCAPE;
135
- if (height > width) return ImageOrientation.PORTRAIT;
136
- return ImageOrientation.SQUARE;
137
- }
138
-
139
- /**
140
- * Calculate aspect ratio
141
- */
142
- static getAspectRatio(width: number, height: number): number {
143
- return width / height;
144
- }
145
-
146
- /**
147
- * Calculate dimensions to fit within max size while preserving aspect ratio
148
- */
149
- static fitToSize(
150
- width: number,
151
- height: number,
152
- maxWidth: number,
153
- maxHeight: number
154
- ): { width: number; height: number } {
155
- const aspectRatio = ImageUtils.getAspectRatio(width, height);
156
-
157
- let newWidth = width;
158
- let newHeight = height;
159
-
160
- if (width > maxWidth) {
161
- newWidth = maxWidth;
162
- newHeight = newWidth / aspectRatio;
163
- }
164
-
165
- if (newHeight > maxHeight) {
166
- newHeight = maxHeight;
167
- newWidth = newHeight * aspectRatio;
168
- }
169
-
170
- return {
171
- width: Math.round(newWidth),
172
- height: Math.round(newHeight),
173
- };
174
- }
175
-
176
- /**
177
- * Calculate thumbnail dimensions (maintains aspect ratio)
178
- */
179
- static getThumbnailSize(
180
- width: number,
181
- height: number,
182
- thumbnailSize: number = IMAGE_CONSTANTS.THUMBNAIL_SIZE
183
- ): { width: number; height: number } {
184
- return ImageUtils.fitToSize(width, height, thumbnailSize, thumbnailSize);
185
- }
186
-
187
- /**
188
- * Validate image URI
189
- */
190
- static isValidImageUri(uri: string): boolean {
191
- if (!uri) return false;
192
-
193
- // Check if it's a valid URI format
194
- return (
195
- uri.startsWith('file://') ||
196
- uri.startsWith('content://') ||
197
- uri.startsWith('http://') ||
198
- uri.startsWith('https://') ||
199
- uri.startsWith('data:image/')
200
- );
201
- }
202
-
203
- /**
204
- * Get image format from URI
205
- */
206
- static getFormatFromUri(uri: string): ImageFormat | null {
207
- const lowerUri = uri.toLowerCase();
208
-
209
- if (lowerUri.includes('.jpg') || lowerUri.includes('.jpeg')) {
210
- return ImageFormat.JPEG;
211
- }
212
-
213
- if (lowerUri.includes('.png')) {
214
- return ImageFormat.PNG;
215
- }
216
-
217
- if (lowerUri.includes('.webp')) {
218
- return ImageFormat.WEBP;
219
- }
220
-
221
- return null;
222
- }
223
-
224
- /**
225
- * Get file extension from format
226
- */
227
- static getExtensionFromFormat(format: ImageFormat): string {
228
- switch (format) {
229
- case ImageFormat.JPEG:
230
- return 'jpg';
231
- case ImageFormat.PNG:
232
- return 'png';
233
- case ImageFormat.WEBP:
234
- return 'webp';
235
- default:
236
- return 'jpg';
237
- }
238
- }
239
-
240
- /**
241
- * Calculate crop dimensions for square crop (centered)
242
- */
243
- static getSquareCrop(width: number, height: number): ImageManipulateAction['crop'] {
244
- const size = Math.min(width, height);
245
- const originX = (width - size) / 2;
246
- const originY = (height - size) / 2;
247
-
248
- return {
249
- originX: Math.round(originX),
250
- originY: Math.round(originY),
251
- width: Math.round(size),
252
- height: Math.round(size),
253
- };
254
- }
255
-
256
- /**
257
- * Format file size to human-readable string
258
- */
259
- static formatFileSize(bytes: number): string {
260
- if (bytes < 1024) return `${bytes} B`;
261
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
262
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
263
- }
264
-
265
- /**
266
- * Check if image needs compression based on size
267
- */
268
- static needsCompression(bytes: number, maxSizeMB: number = 2): boolean {
269
- const maxBytes = maxSizeMB * 1024 * 1024;
270
- return bytes > maxBytes;
271
- }
272
- }
273
-
@@ -1,316 +0,0 @@
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
-