@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-image",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Image manipulation and viewing for React Native apps - resize, crop, rotate, flip, compress, gallery viewer",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -29,17 +29,23 @@
29
29
  "url": "https://github.com/umituz/react-native-image"
30
30
  },
31
31
  "peerDependencies": {
32
+ "@umituz/react-native-filesystem": "latest",
33
+ "expo-image-manipulator": ">=11.0.0",
32
34
  "react": ">=18.2.0",
33
35
  "react-native": ">=0.74.0",
34
- "expo-image-manipulator": ">=11.0.0",
35
- "react-native-image-viewing": ">=0.2.0",
36
- "@umituz/react-native-filesystem": "latest"
36
+ "react-native-image-viewing": ">=0.2.0"
37
37
  },
38
38
  "devDependencies": {
39
+ "@react-native/typescript-config": "^0.83.0",
39
40
  "@types/react": "^18.2.45",
40
41
  "@types/react-native": "^0.73.0",
42
+ "@umituz/react-native-design-system-theme": "^1.12.0",
43
+ "@umituz/react-native-filesystem": "^2.1.1",
44
+ "expo-file-system": "^19.0.21",
45
+ "expo-image-manipulator": "^14.0.8",
41
46
  "react": "^18.2.0",
42
47
  "react-native": "^0.74.0",
48
+ "react-native-image-viewing": "^0.2.2",
43
49
  "typescript": "^5.3.3"
44
50
  },
45
51
  "publishConfig": {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Image Constants
3
+ */
4
+ import { SaveFormat } from "./ImageTypes";
5
+
6
+ export const IMAGE_CONSTANTS = {
7
+ MAX_WIDTH: 2048,
8
+ MAX_HEIGHT: 2048,
9
+ DEFAULT_QUALITY: 0.8,
10
+ THUMBNAIL_SIZE: 200,
11
+ COMPRESS_QUALITY: {
12
+ LOW: 0.5,
13
+ MEDIUM: 0.7,
14
+ HIGH: 0.9,
15
+ },
16
+ FORMAT: {
17
+ JPEG: 'jpeg' as SaveFormat,
18
+ PNG: 'png' as SaveFormat,
19
+ WEBP: 'webp' as SaveFormat,
20
+ },
21
+ } as const;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Image Type Definitions
3
+ */
4
+
5
+ export enum ImageFormat {
6
+ JPEG = 'jpeg',
7
+ PNG = 'png',
8
+ WEBP = 'webp',
9
+ }
10
+
11
+ export type SaveFormat = 'jpeg' | 'png' | 'webp';
12
+
13
+ export enum ImageOrientation {
14
+ PORTRAIT = 'portrait',
15
+ LANDSCAPE = 'landscape',
16
+ SQUARE = 'square',
17
+ }
18
+
19
+ export interface ImageManipulateAction {
20
+ resize?: { width?: number; height?: number };
21
+ crop?: { originX: number; originY: number; width: number; height: number };
22
+ rotate?: number;
23
+ flip?: { vertical?: boolean; horizontal?: boolean };
24
+ }
25
+
26
+ export interface ImageSaveOptions {
27
+ format?: SaveFormat;
28
+ compress?: number;
29
+ base64?: boolean;
30
+ }
31
+
32
+ export interface ImageManipulationResult {
33
+ uri: string;
34
+ width: number;
35
+ height: number;
36
+ base64?: string;
37
+ }
38
+
39
+ export interface ImageMetadata {
40
+ uri: string;
41
+ width: number;
42
+ height: number;
43
+ format?: ImageFormat;
44
+ size?: number;
45
+ orientation?: ImageOrientation;
46
+ }
47
+
48
+ export interface ImageViewerItem {
49
+ uri: string;
50
+ title?: string;
51
+ description?: string;
52
+ width?: number;
53
+ height?: number;
54
+ }
55
+
56
+ export interface ImageGalleryOptions {
57
+ index?: number;
58
+ backgroundColor?: string;
59
+ swipeToCloseEnabled?: boolean;
60
+ doubleTapToZoomEnabled?: boolean;
61
+ onDismiss?: () => void;
62
+ onIndexChange?: (index: number) => void;
63
+ }
64
+
65
+ export interface ImageOperationResult {
66
+ success: boolean;
67
+ uri?: string;
68
+ error?: string;
69
+ width?: number;
70
+ height?: number;
71
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Image Utilities
3
+ */
4
+ import { ImageFormat, ImageOrientation, ImageManipulateAction } from "../entities/ImageTypes";
5
+ import { IMAGE_CONSTANTS } from "../entities/ImageConstants";
6
+
7
+ export class ImageUtils {
8
+ static getOrientation(width: number, height: number): ImageOrientation {
9
+ if (width > height) return ImageOrientation.LANDSCAPE;
10
+ if (height > width) return ImageOrientation.PORTRAIT;
11
+ return ImageOrientation.SQUARE;
12
+ }
13
+
14
+ static getAspectRatio(width: number, height: number): number {
15
+ return width / height;
16
+ }
17
+
18
+ static fitToSize(
19
+ width: number,
20
+ height: number,
21
+ maxWidth: number,
22
+ maxHeight: number
23
+ ): { width: number; height: number } {
24
+ const aspectRatio = ImageUtils.getAspectRatio(width, height);
25
+ let newWidth = width;
26
+ let newHeight = height;
27
+
28
+ if (width > maxWidth) {
29
+ newWidth = maxWidth;
30
+ newHeight = newWidth / aspectRatio;
31
+ }
32
+
33
+ if (newHeight > maxHeight) {
34
+ newHeight = maxHeight;
35
+ newWidth = newHeight * aspectRatio;
36
+ }
37
+
38
+ return {
39
+ width: Math.round(newWidth),
40
+ height: Math.round(newHeight),
41
+ };
42
+ }
43
+
44
+ static getThumbnailSize(
45
+ width: number,
46
+ height: number,
47
+ thumbnailSize: number = IMAGE_CONSTANTS.THUMBNAIL_SIZE
48
+ ): { width: number; height: number } {
49
+ return ImageUtils.fitToSize(width, height, thumbnailSize, thumbnailSize);
50
+ }
51
+
52
+ static isValidImageUri(uri: string): boolean {
53
+ if (!uri) return false;
54
+ return (
55
+ uri.startsWith('file://') ||
56
+ uri.startsWith('content://') ||
57
+ uri.startsWith('http://') ||
58
+ uri.startsWith('https://') ||
59
+ uri.startsWith('data:image/')
60
+ );
61
+ }
62
+
63
+ static getFormatFromUri(uri: string): ImageFormat | null {
64
+ const lowerUri = uri.toLowerCase();
65
+ if (lowerUri.includes('.jpg') || lowerUri.includes('.jpeg')) return ImageFormat.JPEG;
66
+ if (lowerUri.includes('.png')) return ImageFormat.PNG;
67
+ if (lowerUri.includes('.webp')) return ImageFormat.WEBP;
68
+ return null;
69
+ }
70
+
71
+ static getExtensionFromFormat(format: ImageFormat): string {
72
+ switch (format) {
73
+ case ImageFormat.JPEG: return 'jpg';
74
+ case ImageFormat.PNG: return 'png';
75
+ case ImageFormat.WEBP: return 'webp';
76
+ default: return 'jpg';
77
+ }
78
+ }
79
+
80
+ static getSquareCrop(width: number, height: number): ImageManipulateAction['crop'] {
81
+ const size = Math.min(width, height);
82
+ const originX = (width - size) / 2;
83
+ const originY = (height - size) / 2;
84
+
85
+ return {
86
+ originX: Math.round(originX),
87
+ originY: Math.round(originY),
88
+ width: Math.round(size),
89
+ height: Math.round(size),
90
+ };
91
+ }
92
+
93
+ static formatFileSize(bytes: number): string {
94
+ if (bytes < 1024) return `${bytes} B`;
95
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
96
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
97
+ }
98
+
99
+ static needsCompression(bytes: number, maxSizeMB: number = 2): boolean {
100
+ const maxBytes = maxSizeMB * 1024 * 1024;
101
+ return bytes > maxBytes;
102
+ }
103
+ }
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * Resize, crop, rotate, flip, compress, gallery viewer
6
6
  *
7
7
  * Usage:
8
- * import { useImage, useImageGallery, ImageService, ImageUtils } from '@umituz/react-native-image';
8
+ * import { useImage, ImageGallery, ImageService, ImageUtils } from '@umituz/react-native-image';
9
9
  */
10
10
 
11
11
  // =============================================================================
@@ -21,35 +21,41 @@ export type {
21
21
  ImageGalleryOptions,
22
22
  ImageOperationResult,
23
23
  SaveFormat,
24
- } from './domain/entities/Image';
24
+ } from './domain/entities/ImageTypes';
25
25
 
26
26
  export {
27
27
  ImageFormat,
28
28
  ImageOrientation,
29
- IMAGE_CONSTANTS,
30
- ImageUtils,
31
- } from './domain/entities/Image';
29
+ } from './domain/entities/ImageTypes';
30
+
31
+ export { IMAGE_CONSTANTS } from './domain/entities/ImageConstants';
32
+ export { ImageUtils } from './domain/utils/ImageUtils';
32
33
 
33
34
  // =============================================================================
34
35
  // INFRASTRUCTURE LAYER - Services
35
36
  // =============================================================================
36
37
 
37
- export { ImageService } from './infrastructure/services/ImageService';
38
+ export { ImageTransformService } from './infrastructure/services/ImageTransformService';
39
+ export { ImageConversionService } from './infrastructure/services/ImageConversionService';
40
+ export { ImageStorageService } from './infrastructure/services/ImageStorageService';
38
41
  export {
39
42
  ImageViewerService,
40
43
  type ImageViewerConfig,
41
44
  } from './infrastructure/services/ImageViewerService';
42
45
 
43
46
  // =============================================================================
44
- // PRESENTATION LAYER - Hooks
47
+ // PRESENTATION LAYER - Components & Hooks
45
48
  // =============================================================================
46
49
 
47
- export {
48
- useImage,
49
- } from './presentation/hooks/useImage';
50
+ export { ImageGallery, type ImageGalleryProps } from './presentation/components/ImageGallery';
51
+
52
+ export { useImage } from './presentation/hooks/useImage';
53
+ export { useImageTransform } from './presentation/hooks/useImageTransform';
54
+ export { useImageConversion } from './presentation/hooks/useImageConversion';
50
55
 
51
56
  export {
52
57
  useImageGallery,
53
58
  type UseImageGalleryReturn,
54
59
  } from './presentation/hooks/useImageGallery';
55
60
 
61
+
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Image Conversion Service
3
+ *
4
+ * Handles format conversion, compression, and thumbnail generation.
5
+ * (Thumbnail is treated as a specialized compression/resize)
6
+ */
7
+
8
+ import * as ImageManipulator from 'expo-image-manipulator';
9
+ import type {
10
+ ImageSaveOptions,
11
+ ImageManipulationResult,
12
+ SaveFormat,
13
+ } from '../../domain/entities/ImageTypes';
14
+ import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
15
+ import { ImageTransformService } from './ImageTransformService';
16
+
17
+ export class ImageConversionService {
18
+ static async compress(
19
+ uri: string,
20
+ quality: number = IMAGE_CONSTANTS.DEFAULT_QUALITY
21
+ ): Promise<ImageManipulationResult | null> {
22
+ try {
23
+ return await ImageManipulator.manipulateAsync(
24
+ uri,
25
+ [],
26
+ { compress: quality }
27
+ );
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ static async convertFormat(
34
+ uri: string,
35
+ format: SaveFormat,
36
+ quality?: number
37
+ ): Promise<ImageManipulationResult | null> {
38
+ try {
39
+ return await ImageManipulator.manipulateAsync(
40
+ uri,
41
+ [],
42
+ {
43
+ compress: quality || IMAGE_CONSTANTS.DEFAULT_QUALITY,
44
+ format: ImageTransformService.mapFormat(format),
45
+ }
46
+ );
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ static async createThumbnail(
53
+ uri: string,
54
+ size: number = IMAGE_CONSTANTS.THUMBNAIL_SIZE,
55
+ options?: ImageSaveOptions
56
+ ): Promise<ImageManipulationResult | null> {
57
+ try {
58
+ return ImageTransformService.resizeToFit(uri, size, size, {
59
+ ...options,
60
+ compress: options?.compress || IMAGE_CONSTANTS.COMPRESS_QUALITY.MEDIUM,
61
+ });
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Image Storage Service
3
+ *
4
+ * Handles saving images to the device filesystem.
5
+ * Wraps @umituz/react-native-filesystem.
6
+ */
7
+
8
+ import { FileSystemService } from '@umituz/react-native-filesystem';
9
+
10
+ export class ImageStorageService {
11
+ static async saveImage(
12
+ uri: string,
13
+ filename?: string
14
+ ): Promise<string | null> {
15
+ try {
16
+ const result = await FileSystemService.copyToDocuments(uri, filename);
17
+ return result.success && result.uri ? result.uri : null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Image Transform Service
3
+ *
4
+ * Handles basic geometric transformations: resize, crop, rotate, flip.
5
+ * Also handles combined manipulations.
6
+ */
7
+
8
+ import * as ImageManipulator from 'expo-image-manipulator';
9
+ import type {
10
+ ImageManipulateAction,
11
+ ImageSaveOptions,
12
+ ImageManipulationResult,
13
+ SaveFormat,
14
+ } from '../../domain/entities/ImageTypes';
15
+ import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
16
+ import { ImageUtils } from '../../domain/utils/ImageUtils';
17
+
18
+ export class ImageTransformService {
19
+ /**
20
+ * Helper to map SaveFormat to Manipulator format
21
+ */
22
+ static mapFormat(format?: SaveFormat): ImageManipulator.SaveFormat {
23
+ if (format === 'png') return ImageManipulator.SaveFormat.PNG;
24
+ if (format === 'webp') return ImageManipulator.SaveFormat.WEBP;
25
+ return ImageManipulator.SaveFormat.JPEG;
26
+ }
27
+
28
+ static async resize(
29
+ uri: string,
30
+ width?: number,
31
+ height?: number,
32
+ options?: ImageSaveOptions
33
+ ): Promise<ImageManipulationResult | null> {
34
+ try {
35
+ return await ImageManipulator.manipulateAsync(
36
+ uri,
37
+ [{ resize: { width, height } }],
38
+ {
39
+ compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
40
+ format: ImageTransformService.mapFormat(options?.format),
41
+ base64: options?.base64,
42
+ }
43
+ );
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ static async crop(
50
+ uri: string,
51
+ cropArea: { originX: number; originY: number; width: number; height: number },
52
+ options?: ImageSaveOptions
53
+ ): Promise<ImageManipulationResult | null> {
54
+ try {
55
+ return await ImageManipulator.manipulateAsync(
56
+ uri,
57
+ [{ crop: cropArea }],
58
+ {
59
+ compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
60
+ format: ImageTransformService.mapFormat(options?.format),
61
+ base64: options?.base64,
62
+ }
63
+ );
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ static async rotate(
70
+ uri: string,
71
+ degrees: number,
72
+ options?: ImageSaveOptions
73
+ ): Promise<ImageManipulationResult | null> {
74
+ try {
75
+ return await ImageManipulator.manipulateAsync(
76
+ uri,
77
+ [{ rotate: degrees }],
78
+ {
79
+ compress: options?.compress || IMAGE_CONSTANTS.DEFAULT_QUALITY,
80
+ format: ImageTransformService.mapFormat(options?.format),
81
+ base64: options?.base64,
82
+ }
83
+ );
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ static async flip(
90
+ uri: string,
91
+ flip: { horizontal?: boolean; vertical?: boolean },
92
+ options?: ImageSaveOptions
93
+ ): Promise<ImageManipulationResult | null> {
94
+ try {
95
+ const actions: ImageManipulator.Action[] = [];
96
+ if (flip.horizontal) actions.push({ flip: ImageManipulator.FlipType.Horizontal });
97
+ if (flip.vertical) actions.push({ flip: ImageManipulator.FlipType.Vertical });
98
+
99
+ return await ImageManipulator.manipulateAsync(
100
+ uri,
101
+ 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
+ }
139
+ );
140
+ } catch {
141
+ return null;
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;
170
+ }
171
+ }
172
+ }
@@ -8,7 +8,7 @@
8
8
  import type {
9
9
  ImageViewerItem,
10
10
  ImageGalleryOptions,
11
- } from '../../domain/entities/Image';
11
+ } from '../../domain/entities/ImageTypes';
12
12
 
13
13
  /**
14
14
  * Image viewer configuration
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Image Gallery Component
3
+ *
4
+ * A wrapper around react-native-image-viewing that provides
5
+ * theme integration and standard configuration.
6
+ */
7
+
8
+ import React from 'react';
9
+ import ImageViewing from 'react-native-image-viewing';
10
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
11
+ import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
12
+
13
+ export interface ImageGalleryProps extends ImageGalleryOptions {
14
+ images: ImageViewerItem[];
15
+ visible: boolean;
16
+ onDismiss: () => void;
17
+ index?: number;
18
+ }
19
+
20
+ export const ImageGallery: React.FC<ImageGalleryProps> = ({
21
+ images,
22
+ visible,
23
+ onDismiss,
24
+ index = 0,
25
+ backgroundColor,
26
+ swipeToCloseEnabled = true,
27
+ doubleTapToZoomEnabled = true,
28
+ onIndexChange,
29
+ }) => {
30
+ const tokens = useAppDesignTokens();
31
+
32
+ // Use theme background if not provided
33
+ const bg = backgroundColor || tokens.colors.backgroundPrimary;
34
+
35
+ // Map images to structure expected by ImageViewing (uri object)
36
+ const viewerImages = React.useMemo(() =>
37
+ images.map(img => ({ uri: img.uri })),
38
+ [images]
39
+ );
40
+
41
+ return (
42
+ <ImageViewing
43
+ images={viewerImages}
44
+ imageIndex={index}
45
+ visible={visible}
46
+ onRequestClose={onDismiss}
47
+ onImageIndexChange={onIndexChange}
48
+ backgroundColor={bg}
49
+ swipeToCloseEnabled={swipeToCloseEnabled}
50
+ doubleTapToZoomEnabled={doubleTapToZoomEnabled}
51
+ // Can add custom Header/Footer here using theme tokens if needed
52
+ />
53
+ );
54
+ };