@umituz/react-native-image 1.3.13 → 1.3.15

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.3.13",
3
+ "version": "1.3.15",
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",
@@ -1,180 +1,23 @@
1
1
  /**
2
- * Domain - Advanced Editor Types
2
+ * Domain - Editor Types (Main Export)
3
+ *
4
+ * Central export point for all editor-related types
3
5
  */
4
6
 
5
- export enum EditorTool {
6
- MOVE = 'move',
7
- BRUSH = 'brush',
8
- ERASER = 'eraser',
9
- TEXT = 'text',
10
- SHAPE = 'shape',
11
- CROP = 'crop',
12
- FILTER = 'filter',
13
- STICKER = 'sticker',
14
- SELECT = 'select',
15
- }
7
+ // Tools
8
+ export * from './editor/EditorToolTypes';
16
9
 
17
- export enum ShapeType {
18
- RECTANGLE = 'rectangle',
19
- CIRCLE = 'circle',
20
- LINE = 'line',
21
- ARROW = 'arrow',
22
- TRIANGLE = 'triangle',
23
- STAR = 'star',
24
- HEART = 'heart',
25
- }
10
+ // Elements
11
+ export * from './editor/EditorElementTypes';
26
12
 
27
- export enum BrushStyle {
28
- NORMAL = 'normal',
29
- MARKER = 'marker',
30
- SPRAY = 'spray',
31
- PENCIL = 'pencil',
32
- CALIGRAPHY = 'caligraphy',
33
- }
13
+ // Layers & History
14
+ export * from './editor/EditorLayerTypes';
34
15
 
35
- export interface EditorPoint {
36
- x: number;
37
- y: number;
38
- }
16
+ // State
17
+ export * from './editor/EditorStateTypes';
39
18
 
40
- export interface EditorDimensions {
41
- width: number;
42
- height: number;
43
- }
19
+ // Configuration
20
+ export * from './editor/EditorConfigTypes';
44
21
 
45
- export interface EditorStroke {
46
- points: EditorPoint[];
47
- color: string;
48
- size: number;
49
- style: BrushStyle;
50
- opacity: number;
51
- }
52
-
53
- export interface EditorShape {
54
- type: ShapeType;
55
- startPoint: EditorPoint;
56
- endPoint: EditorPoint;
57
- color: string;
58
- strokeWidth: number;
59
- fillColor?: string;
60
- opacity: number;
61
- rotation?: number;
62
- }
63
-
64
- export interface EditorText {
65
- id: string;
66
- text: string;
67
- position: EditorPoint;
68
- fontSize: number;
69
- fontFamily: string;
70
- color: string;
71
- backgroundColor?: string;
72
- rotation: number;
73
- opacity: number;
74
- maxWidth?: number;
75
- textAlign: 'left' | 'center' | 'right';
76
- fontWeight: 'normal' | 'bold';
77
- fontStyle: 'normal' | 'italic';
78
- }
79
-
80
- export interface EditorSticker {
81
- id: string;
82
- uri: string;
83
- position: EditorPoint;
84
- size: EditorDimensions;
85
- rotation: number;
86
- opacity: number;
87
- scale: number;
88
- }
89
-
90
- export interface EditorLayer {
91
- id: string;
92
- name: string;
93
- visible: boolean;
94
- opacity: number;
95
- locked: boolean;
96
- elements: Array<{
97
- type: 'stroke' | 'shape' | 'text' | 'sticker';
98
- data: EditorStroke | EditorShape | EditorText | EditorSticker;
99
- }>;
100
- }
101
-
102
- export interface EditorSelection {
103
- bounds: {
104
- x: number;
105
- y: number;
106
- width: number;
107
- height: number;
108
- };
109
- elements: string[];
110
- }
111
-
112
- export interface EditorHistory {
113
- id: string;
114
- timestamp: Date;
115
- layers: EditorLayer[];
116
- thumbnail?: string;
117
- }
118
-
119
- export interface EditorCropArea {
120
- x: number;
121
- y: number;
122
- width: number;
123
- height: number;
124
- aspectRatio?: number;
125
- }
126
-
127
- export interface EditorFilter {
128
- type: string;
129
- intensity: number;
130
- preview?: string;
131
- }
132
-
133
- export interface EditorState {
134
- originalUri: string;
135
- currentUri?: string;
136
- tool: EditorTool;
137
- selectedLayer?: string;
138
- layers: EditorLayer[];
139
- history: EditorHistory[];
140
- historyIndex: number;
141
- selection?: EditorSelection;
142
- cropArea?: EditorCropArea;
143
- activeFilter?: EditorFilter;
144
- isDirty: boolean;
145
- dimensions: EditorDimensions;
146
- zoom: number;
147
- pan: EditorPoint;
148
- }
149
-
150
- export interface EditorOptions {
151
- maxLayers?: number;
152
- maxHistory?: number;
153
- enableUndo?: boolean;
154
- enableRedo?: boolean;
155
- enableFilters?: boolean;
156
- enableShapes?: boolean;
157
- enableText?: boolean;
158
- enableStickers?: boolean;
159
- enableCrop?: boolean;
160
- brushSizeRange?: [number, number];
161
- strokeWidthRange?: [number, number];
162
- fontSizeRange?: [number, number];
163
- defaultColors?: string[];
164
- stickerPacks?: string[];
165
- customFonts?: string[];
166
- }
167
-
168
- export interface EditorEvent {
169
- type: 'toolChange' | 'layerAdd' | 'layerRemove' | 'layerUpdate' | 'selectionChange' | 'historyChange';
170
- data: any;
171
- }
172
-
173
- export interface EditorExportOptions {
174
- format: 'jpeg' | 'png' | 'webp';
175
- quality: number;
176
- backgroundColor?: string;
177
- includeHiddenLayers?: boolean;
178
- flattenLayers?: boolean;
179
- maxSize?: number;
180
- }
22
+ // Filters
23
+ export * from './editor/EditorFilterTypes';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Domain - Editor Configuration Types
3
+ */
4
+
5
+ export interface EditorOptions {
6
+ maxLayers?: number;
7
+ maxHistory?: number;
8
+ enableUndo?: boolean;
9
+ enableRedo?: boolean;
10
+ enableFilters?: boolean;
11
+ enableShapes?: boolean;
12
+ enableText?: boolean;
13
+ enableStickers?: boolean;
14
+ enableCrop?: boolean;
15
+ brushSizeRange?: [number, number];
16
+ strokeWidthRange?: [number, number];
17
+ fontSizeRange?: [number, number];
18
+ defaultColors?: string[];
19
+ stickerPacks?: string[];
20
+ customFonts?: string[];
21
+ }
22
+
23
+ export interface EditorExportOptions {
24
+ format: 'jpeg' | 'png' | 'webp';
25
+ quality: number;
26
+ backgroundColor?: string;
27
+ includeHiddenLayers?: boolean;
28
+ flattenLayers?: boolean;
29
+ maxSize?: number;
30
+ }
31
+
32
+ export interface EditorEvent {
33
+ type: 'toolChange' | 'layerAdd' | 'layerRemove' | 'layerUpdate' | 'selectionChange' | 'historyChange';
34
+ data: unknown;
35
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Domain - Editor Element Types
3
+ */
4
+
5
+ import type { ShapeType } from './EditorToolTypes';
6
+
7
+ export interface EditorPoint {
8
+ x: number;
9
+ y: number;
10
+ }
11
+
12
+ export interface EditorDimensions {
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export interface EditorStroke {
18
+ points: EditorPoint[];
19
+ color: string;
20
+ size: number;
21
+ style: string;
22
+ opacity: number;
23
+ }
24
+
25
+ export interface EditorShape {
26
+ type: ShapeType;
27
+ startPoint: EditorPoint;
28
+ endPoint: EditorPoint;
29
+ color: string;
30
+ strokeWidth: number;
31
+ fillColor?: string;
32
+ opacity: number;
33
+ rotation?: number;
34
+ }
35
+
36
+ export interface EditorText {
37
+ id: string;
38
+ text: string;
39
+ position: EditorPoint;
40
+ fontSize: number;
41
+ fontFamily: string;
42
+ color: string;
43
+ backgroundColor?: string;
44
+ rotation: number;
45
+ opacity: number;
46
+ maxWidth?: number;
47
+ textAlign: 'left' | 'center' | 'right';
48
+ fontWeight: 'normal' | 'bold';
49
+ fontStyle: 'normal' | 'italic';
50
+ }
51
+
52
+ export interface EditorSticker {
53
+ id: string;
54
+ uri: string;
55
+ position: EditorPoint;
56
+ size: EditorDimensions;
57
+ rotation: number;
58
+ opacity: number;
59
+ scale: number;
60
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Domain - Editor Filter Types
3
+ */
4
+
5
+ export interface EditorFilter {
6
+ type: string;
7
+ intensity: number;
8
+ preview?: string;
9
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Domain - Editor Layer Types
3
+ */
4
+
5
+ import type { EditorStroke, EditorShape, EditorText, EditorSticker } from './EditorElementTypes';
6
+
7
+ export interface EditorLayer {
8
+ id: string;
9
+ name: string;
10
+ visible: boolean;
11
+ opacity: number;
12
+ locked: boolean;
13
+ elements: Array<{
14
+ type: 'stroke' | 'shape' | 'text' | 'sticker';
15
+ data: EditorStroke | EditorShape | EditorText | EditorSticker;
16
+ }>;
17
+ }
18
+
19
+ export interface EditorHistory {
20
+ id: string;
21
+ timestamp: Date;
22
+ layers: EditorLayer[];
23
+ thumbnail?: string;
24
+ }
25
+
26
+ export interface EditorSelection {
27
+ bounds: {
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ };
33
+ elements: string[];
34
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Domain - Editor State Types
3
+ */
4
+
5
+ import type { EditorTool } from './EditorToolTypes';
6
+ import type { EditorLayer } from './EditorLayerTypes';
7
+ import type { EditorSelection } from './EditorLayerTypes';
8
+ import type { EditorHistory } from './EditorLayerTypes';
9
+ import type { EditorPoint, EditorDimensions } from './EditorElementTypes';
10
+ import type { EditorFilter } from './EditorFilterTypes';
11
+
12
+ export interface EditorCropArea {
13
+ x: number;
14
+ y: number;
15
+ width: number;
16
+ height: number;
17
+ aspectRatio?: number;
18
+ }
19
+
20
+ export interface EditorState {
21
+ originalUri: string;
22
+ currentUri?: string;
23
+ tool: EditorTool;
24
+ selectedLayer?: string;
25
+ layers: EditorLayer[];
26
+ history: EditorHistory[];
27
+ historyIndex: number;
28
+ selection?: EditorSelection;
29
+ cropArea?: EditorCropArea;
30
+ activeFilter?: EditorFilter;
31
+ isDirty: boolean;
32
+ dimensions: EditorDimensions;
33
+ zoom: number;
34
+ pan: EditorPoint;
35
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Domain - Editor Tool Types
3
+ */
4
+
5
+ export enum EditorTool {
6
+ MOVE = 'move',
7
+ BRUSH = 'brush',
8
+ ERASER = 'eraser',
9
+ TEXT = 'text',
10
+ SHAPE = 'shape',
11
+ CROP = 'crop',
12
+ FILTER = 'filter',
13
+ STICKER = 'sticker',
14
+ SELECT = 'select',
15
+ }
16
+
17
+ export enum ShapeType {
18
+ RECTANGLE = 'rectangle',
19
+ CIRCLE = 'circle',
20
+ LINE = 'line',
21
+ ARROW = 'arrow',
22
+ TRIANGLE = 'triangle',
23
+ STAR = 'star',
24
+ HEART = 'heart',
25
+ }
26
+
27
+ export enum BrushStyle {
28
+ NORMAL = 'normal',
29
+ MARKER = 'marker',
30
+ SPRAY = 'spray',
31
+ PENCIL = 'pencil',
32
+ CALIGRAPHY = 'caligraphy',
33
+ }
@@ -1,14 +1,11 @@
1
1
  /**
2
2
  * Image Infrastructure - Batch Processing Service
3
- *
3
+ *
4
4
  * Handles processing multiple images concurrently with progress tracking
5
5
  */
6
6
 
7
7
  import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
8
- import { ImageTransformService } from './ImageTransformService';
9
- import { ImageConversionService } from './ImageConversionService';
10
- import { ImageValidator } from '../utils/ImageValidator';
11
- import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
8
+ import { BatchProcessor } from '../utils/BatchProcessor';
12
9
 
13
10
  export interface BatchProcessingOptions {
14
11
  concurrency?: number;
@@ -33,74 +30,11 @@ export interface BatchProcessingResult {
33
30
  export interface BatchOperation {
34
31
  uri: string;
35
32
  type: 'resize' | 'crop' | 'filter' | 'compress' | 'convert';
36
- params: any;
37
- options?: any;
33
+ params: Record<string, unknown>;
34
+ options?: Record<string, unknown>;
38
35
  }
39
36
 
40
37
  export class ImageBatchService {
41
- private static async processBatchItem(
42
- operation: BatchOperation,
43
- options: BatchProcessingOptions = {}
44
- ): Promise<{ uri: string; result: ImageManipulationResult | null; error?: Error }> {
45
- try {
46
- const uriValidation = ImageValidator.validateUri(operation.uri);
47
- if (!uriValidation.isValid) {
48
- throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'batchProcess');
49
- }
50
-
51
- let result: ImageManipulationResult;
52
-
53
- switch (operation.type) {
54
- case 'resize':
55
- result = await ImageTransformService.resize(
56
- operation.uri,
57
- operation.params.width,
58
- operation.params.height,
59
- operation.options
60
- );
61
- break;
62
-
63
- case 'crop':
64
- result = await ImageTransformService.crop(
65
- operation.uri,
66
- operation.params,
67
- operation.options
68
- );
69
- break;
70
-
71
- case 'compress':
72
- result = await ImageConversionService.compress(
73
- operation.uri,
74
- operation.params.quality
75
- );
76
- break;
77
-
78
- case 'convert':
79
- result = await ImageConversionService.convertFormat(
80
- operation.uri,
81
- operation.params.format,
82
- operation.params.quality
83
- );
84
- break;
85
-
86
- default:
87
- throw ImageErrorHandler.createError(
88
- `Unknown operation type: ${operation.type}`,
89
- IMAGE_ERROR_CODES.VALIDATION_ERROR,
90
- 'batchProcess'
91
- );
92
- }
93
-
94
- return { uri: operation.uri, result };
95
- } catch (error) {
96
- return {
97
- uri: operation.uri,
98
- result: null,
99
- error: error instanceof Error ? error : new Error('Unknown error')
100
- };
101
- }
102
- }
103
-
104
38
  static async processBatch(
105
39
  operations: BatchOperation[],
106
40
  options: BatchProcessingOptions = {}
@@ -115,15 +49,15 @@ export class ImageBatchService {
115
49
  // Process operations in chunks based on concurrency
116
50
  for (let i = 0; i < operations.length; i += concurrency) {
117
51
  const chunk = operations.slice(i, i + concurrency);
118
-
52
+
119
53
  const chunkResults = await Promise.all(
120
- chunk.map(operation => this.processBatchItem(operation, options))
54
+ chunk.map(operation => BatchProcessor.processBatchItem(operation, options))
121
55
  );
122
56
 
123
57
  // Process results
124
58
  for (const result of chunkResults) {
125
59
  completed++;
126
-
60
+
127
61
  options.onProgress?.(completed, total, result.uri);
128
62
 
129
63
  if (result.error) {
@@ -148,7 +82,7 @@ export class ImageBatchService {
148
82
  uris: string[],
149
83
  width?: number,
150
84
  height?: number,
151
- options: BatchProcessingOptions & { saveOptions?: any } = {}
85
+ options: BatchProcessingOptions & { saveOptions?: Record<string, unknown> } = {}
152
86
  ): Promise<BatchProcessingResult> {
153
87
  const operations: BatchOperation[] = uris.map(uri => ({
154
88
  uri,
@@ -162,7 +96,7 @@ export class ImageBatchService {
162
96
 
163
97
  static async compressBatch(
164
98
  uris: string[],
165
- quality: number = 0.8,
99
+ quality = 0.8,
166
100
  options: BatchProcessingOptions = {}
167
101
  ): Promise<BatchProcessingResult> {
168
102
  const operations: BatchOperation[] = uris.map(uri => ({
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Image Infrastructure - Metadata Service
3
- *
3
+ *
4
4
  * Extracts and manages image metadata including EXIF data
5
5
  */
6
6
 
7
7
  import type { ImageMetadataExtended } from '../../domain/entities/ImageFilterTypes';
8
8
  import { ImageValidator } from '../utils/ImageValidator';
9
9
  import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
10
+ import { MetadataExtractor } from '../utils/MetadataExtractor';
10
11
 
11
12
  export interface ImageMetadataExtractionOptions {
12
13
  includeExif?: boolean;
@@ -15,78 +16,6 @@ export interface ImageMetadataExtractionOptions {
15
16
  }
16
17
 
17
18
  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
19
 
91
20
  static async extractMetadata(
92
21
  uri: string,
@@ -105,23 +34,23 @@ export class ImageMetadataService {
105
34
  } = options;
106
35
 
107
36
  // Get basic image info
108
- const dimensions = await ImageMetadataService.getImageDimensions(uri);
109
- const size = await ImageMetadataService.getFileSize(uri);
110
- const format = ImageMetadataService.detectFormat(uri);
37
+ const dimensions = await MetadataExtractor.getImageDimensions(uri);
38
+ const size = await MetadataExtractor.getFileSize(uri);
39
+ const format = MetadataExtractor.detectFormat(uri);
111
40
 
112
41
  // Build metadata object
113
42
  const metadata: ImageMetadataExtended = {
114
43
  format,
115
44
  size,
116
45
  dimensions,
117
- colorSpace: 'sRGB', // Default assumption
46
+ colorSpace: 'sRGB',
118
47
  hasAlpha: format === 'PNG' || format === 'WebP',
119
48
  orientation: 1,
120
49
  };
121
50
 
122
51
  // Extract EXIF data if requested
123
52
  if (includeExif) {
124
- const exifData = await ImageMetadataService.extractExifData(uri);
53
+ const exifData = await MetadataExtractor.extractExifData(uri);
125
54
  if (exifData) {
126
55
  metadata.creationDate = exifData.DateTimeOriginal ? new Date(exifData.DateTimeOriginal) : undefined;
127
56
  metadata.modificationDate = new Date();
@@ -140,7 +69,7 @@ export class ImageMetadataService {
140
69
 
141
70
  // Extract GPS data if requested
142
71
  if (includeGPS) {
143
- const gps = await ImageMetadataService.extractGPSData(uri);
72
+ const gps = await MetadataExtractor.extractGPSData(uri);
144
73
  metadata.gps = gps || undefined;
145
74
  }
146
75
 
@@ -161,9 +90,9 @@ export class ImageMetadataService {
161
90
  throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'getBasicInfo');
162
91
  }
163
92
 
164
- const dimensions = await ImageMetadataService.getImageDimensions(uri);
165
- const size = await ImageMetadataService.getFileSize(uri);
166
- const format = ImageMetadataService.detectFormat(uri);
93
+ const dimensions = await MetadataExtractor.getImageDimensions(uri);
94
+ const size = await MetadataExtractor.getFileSize(uri);
95
+ const format = MetadataExtractor.detectFormat(uri);
167
96
 
168
97
  return {
169
98
  format,
@@ -177,8 +106,8 @@ export class ImageMetadataService {
177
106
 
178
107
  static async hasMetadata(uri: string): Promise<boolean> {
179
108
  try {
180
- const exifData = await ImageMetadataService.extractExifData(uri);
181
- const gpsData = await ImageMetadataService.extractGPSData(uri);
109
+ const exifData = await MetadataExtractor.extractExifData(uri);
110
+ const gpsData = await MetadataExtractor.extractGPSData(uri);
182
111
  return !!(exifData || gpsData);
183
112
  } catch {
184
113
  return false;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Image Infrastructure - Batch Processor Utilities
3
+ *
4
+ * Helper functions for batch processing operations
5
+ */
6
+
7
+ import type { ImageManipulationResult, ImageCropArea, SaveFormat } from '../../domain/entities/ImageTypes';
8
+ import type { BatchOperation, BatchProcessingOptions } from '../services/ImageBatchService';
9
+ import { ImageTransformService } from '../services/ImageTransformService';
10
+ import { ImageConversionService } from '../services/ImageConversionService';
11
+ import { ImageValidator } from './ImageValidator';
12
+ import { ImageErrorHandler, IMAGE_ERROR_CODES } from './ImageErrorHandler';
13
+
14
+ export class BatchProcessor {
15
+ static async processBatchItem(
16
+ operation: BatchOperation,
17
+ options: BatchProcessingOptions = {}
18
+ ): Promise<{ uri: string; result: ImageManipulationResult | null; error?: Error }> {
19
+ try {
20
+ const uriValidation = ImageValidator.validateUri(operation.uri);
21
+ if (!uriValidation.isValid) {
22
+ throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'batchProcess');
23
+ }
24
+
25
+ let result: ImageManipulationResult;
26
+
27
+ switch (operation.type) {
28
+ case 'resize':
29
+ result = await ImageTransformService.resize(
30
+ operation.uri,
31
+ operation.params.width as number | undefined,
32
+ operation.params.height as number | undefined,
33
+ operation.options
34
+ );
35
+ break;
36
+
37
+ case 'crop':
38
+ result = await ImageTransformService.crop(
39
+ operation.uri,
40
+ operation.params as unknown as ImageCropArea,
41
+ operation.options
42
+ );
43
+ break;
44
+
45
+ case 'compress':
46
+ result = await ImageConversionService.compress(
47
+ operation.uri,
48
+ operation.params.quality as number
49
+ );
50
+ break;
51
+
52
+ case 'convert':
53
+ result = await ImageConversionService.convertFormat(
54
+ operation.uri,
55
+ operation.params.format as SaveFormat,
56
+ operation.params.quality as number
57
+ );
58
+ break;
59
+
60
+ default:
61
+ throw ImageErrorHandler.createError(
62
+ `Unknown operation type: ${operation.type}`,
63
+ IMAGE_ERROR_CODES.VALIDATION_ERROR,
64
+ 'batchProcess'
65
+ );
66
+ }
67
+
68
+ return { uri: operation.uri, result };
69
+ } catch (error) {
70
+ return {
71
+ uri: operation.uri,
72
+ result: null,
73
+ error: error instanceof Error ? error : new Error('Unknown error')
74
+ };
75
+ }
76
+ }
77
+
78
+ static async processBatchInChunks<T>(
79
+ items: T[],
80
+ processor: (item: T) => Promise<unknown>,
81
+ concurrency: number,
82
+ onProgress?: (completed: number, total: number) => void
83
+ ): Promise<void> {
84
+ let completed = 0;
85
+ const total = items.length;
86
+
87
+ for (let i = 0; i < items.length; i += concurrency) {
88
+ const chunk = items.slice(i, i + concurrency);
89
+ await Promise.all(chunk.map(processor));
90
+ completed += chunk.length;
91
+ onProgress?.(completed, total);
92
+ }
93
+ }
94
+ }
95
+
@@ -1,152 +1,21 @@
1
1
  /**
2
2
  * Infrastructure - Filter Utils
3
- *
4
- * Low-level filter implementations for raw pixel data
3
+ *
4
+ * Legacy wrapper for backward compatibility. Use specific filter classes directly.
5
5
  */
6
6
 
7
- export class ImageFilterUtils {
8
- static applyBrightness(
9
- data: Uint8ClampedArray,
10
- brightness: number
11
- ): Uint8ClampedArray {
12
- const result = new Uint8ClampedArray(data);
13
- const adjustment = brightness * 2.55;
14
-
15
- for (let i = 0; i < result.length; i += 4) {
16
- result[i] = Math.min(255, Math.max(0, result[i] + adjustment));
17
- result[i + 1] = Math.min(255, Math.max(0, result[i + 1] + adjustment));
18
- result[i + 2] = Math.min(255, Math.max(0, result[i + 2] + adjustment));
19
- }
20
-
21
- return result;
22
- }
23
-
24
- static applyContrast(
25
- data: Uint8ClampedArray,
26
- contrast: number
27
- ): Uint8ClampedArray {
28
- const result = new Uint8ClampedArray(data);
29
- const factor = (259 * (contrast * 2.55 + 255)) / (255 * (259 - contrast * 2.55));
30
-
31
- for (let i = 0; i < result.length; i += 4) {
32
- result[i] = Math.min(255, Math.max(0, factor * (result[i] - 128) + 128));
33
- result[i + 1] = Math.min(255, Math.max(0, factor * (result[i + 1] - 128) + 128));
34
- result[i + 2] = Math.min(255, Math.max(0, factor * (result[i + 2] - 128) + 128));
35
- }
36
-
37
- return result;
38
- }
39
-
40
- static applySaturation(
41
- data: Uint8ClampedArray,
42
- saturation: number
43
- ): Uint8ClampedArray {
44
- const result = new Uint8ClampedArray(data);
45
- const adjustment = 1 + (saturation / 100);
46
-
47
- for (let i = 0; i < result.length; i += 4) {
48
- const r = result[i];
49
- const g = result[i + 1];
50
- const b = result[i + 2];
51
-
52
- const gray = 0.299 * r + 0.587 * g + 0.114 * b;
53
-
54
- result[i] = Math.min(255, Math.max(0, gray + adjustment * (r - gray)));
55
- result[i + 1] = Math.min(255, Math.max(0, gray + adjustment * (g - gray)));
56
- result[i + 2] = Math.min(255, Math.max(0, gray + adjustment * (b - gray)));
57
- }
58
-
59
- return result;
60
- }
61
-
62
- static applyVintage(
63
- data: Uint8ClampedArray,
64
- intensity: number,
65
- warmth: number
66
- ): Uint8ClampedArray {
67
- const result = new Uint8ClampedArray(data);
68
- const factor = intensity / 100;
69
- const warmFactor = warmth / 100;
70
-
71
- for (let i = 0; i < result.length; i += 4) {
72
- let r = result[i];
73
- let g = result[i + 1];
74
- let b = result[i + 2];
75
-
76
- const tr = 0.393 * r + 0.769 * g + 0.189 * b;
77
- const tg = 0.349 * r + 0.686 * g + 0.168 * b;
78
- const tb = 0.272 * r + 0.534 * g + 0.131 * b;
7
+ import { BasicFilters } from './filters/BasicFilters';
8
+ import { SpecialFilters } from './filters/SpecialFilters';
9
+ import { FilterHelpers } from './filters/FilterHelpers';
79
10
 
80
- r = r * (1 - factor) + tr * factor;
81
- g = g * (1 - factor) + tg * factor;
82
- b = b * (1 - factor) + tb * factor;
83
-
84
- if (warmFactor > 0) {
85
- result[i] = Math.min(255, r + warmFactor * 20);
86
- result[i + 1] = Math.min(255, g + warmFactor * 10);
87
- result[i + 2] = Math.min(255, b * (1 - warmFactor * 0.3));
88
- } else {
89
- result[i] = r;
90
- result[i + 1] = g;
91
- result[i + 2] = Math.min(255, b * (1 - Math.abs(warmFactor) * 0.3));
92
- }
93
- }
94
-
95
- return result;
96
- }
97
-
98
- static applyBlur(
99
- data: Uint8ClampedArray,
100
- radius: number,
101
- width: number,
102
- height: number
103
- ): Uint8ClampedArray {
104
- const result = new Uint8ClampedArray(data);
105
- const size = Math.floor(radius) || 1;
106
-
107
- for (let y = 0; y < height; y++) {
108
- for (let x = 0; x < width; x++) {
109
- let r = 0, g = 0, b = 0, a = 0;
110
- let count = 0;
111
-
112
- for (let dy = -size; dy <= size; dy++) {
113
- for (let dx = -size; dx <= size; dx++) {
114
- const ny = y + dy;
115
- const nx = x + dx;
116
-
117
- if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
118
- const idx = (ny * width + nx) * 4;
119
- r += data[idx];
120
- g += data[idx + 1];
121
- b += data[idx + 2];
122
- a += data[idx + 3];
123
- count++;
124
- }
125
- }
126
- }
127
-
128
- const idx = (y * width + x) * 4;
129
- result[idx] = r / count;
130
- result[idx + 1] = g / count;
131
- result[idx + 2] = b / count;
132
- result[idx + 3] = a / count;
133
- }
134
- }
135
-
136
- return result;
137
- }
138
-
139
- static applyIntensity(
140
- originalData: Uint8ClampedArray,
141
- processedData: Uint8ClampedArray,
142
- intensity: number
143
- ): Uint8ClampedArray {
144
- const result = new Uint8ClampedArray(originalData.length);
145
-
146
- for (let i = 0; i < originalData.length; i++) {
147
- result[i] = originalData[i] * (1 - intensity) + processedData[i] * intensity;
148
- }
149
-
150
- return result;
151
- }
11
+ export class ImageFilterUtils {
12
+ static applyBrightness = BasicFilters.applyBrightness;
13
+ static applyContrast = BasicFilters.applyContrast;
14
+ static applySaturation = BasicFilters.applySaturation;
15
+ static applyVintage = SpecialFilters.applyVintage;
16
+ static applyBlur = SpecialFilters.applyBlur;
17
+ static applyIntensity = FilterHelpers.applyIntensity;
152
18
  }
19
+
20
+ // Re-export for convenience
21
+ export { BasicFilters, SpecialFilters, FilterHelpers };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Image Infrastructure - Metadata Extractor Utilities
3
+ *
4
+ * Helper functions for extracting metadata from images
5
+ */
6
+
7
+ import type { ImageMetadataExtractionOptions } from '../../infrastructure/services/ImageMetadataService';
8
+ import { ImageErrorHandler } from './ImageErrorHandler';
9
+
10
+ export class MetadataExtractor {
11
+ static async getImageDimensions(uri: string): Promise<{ width: number; height: number }> {
12
+ try {
13
+ // In a real implementation, we would use:
14
+ // - expo-image-manipulator for basic dimensions
15
+ // - react-native-image-picker for metadata
16
+ // - react-native-exif-reader for EXIF data
17
+
18
+ // Mock implementation
19
+ return {
20
+ width: Math.floor(Math.random() * 2000) + 100,
21
+ height: Math.floor(Math.random() * 2000) + 100,
22
+ };
23
+ } catch (error) {
24
+ throw ImageErrorHandler.handleUnknownError(error, 'getDimensions');
25
+ }
26
+ }
27
+
28
+ static async getFileSize(uri: string): Promise<number> {
29
+ try {
30
+ // In real implementation, use expo-file-system or similar
31
+ return Math.floor(Math.random() * 5000000) + 10000; // Random size between 10KB-5MB
32
+ } catch (error) {
33
+ throw ImageErrorHandler.handleUnknownError(error, 'getFileSize');
34
+ }
35
+ }
36
+
37
+ static async extractExifData(uri: string): Promise<any> {
38
+ try {
39
+ // Mock EXIF data extraction
40
+ return {
41
+ DateTimeOriginal: new Date().toISOString(),
42
+ Make: 'Mock Camera',
43
+ Model: 'Mock Phone',
44
+ ISO: Math.floor(Math.random() * 1600) + 100,
45
+ FocalLength: Math.random() * 50 + 10,
46
+ Flash: Math.random() > 0.5,
47
+ ExposureTime: `1/${Math.floor(Math.random() * 1000) + 100}`,
48
+ FNumber: Math.random() * 8 + 1.4,
49
+ };
50
+ } catch (error) {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ static async extractGPSData(uri: string): Promise<{ latitude: number; longitude: number } | null> {
56
+ try {
57
+ // Mock GPS data extraction
58
+ return Math.random() > 0.7 ? {
59
+ latitude: Math.random() * 180 - 90,
60
+ longitude: Math.random() * 360 - 180,
61
+ } : null;
62
+ } catch (error) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ static detectFormat(uri: string): string {
68
+ const extension = uri.toLowerCase().split('.').pop();
69
+ switch (extension) {
70
+ case 'jpg':
71
+ case 'jpeg':
72
+ return 'JPEG';
73
+ case 'png':
74
+ return 'PNG';
75
+ case 'webp':
76
+ return 'WebP';
77
+ case 'gif':
78
+ return 'GIF';
79
+ default:
80
+ return 'Unknown';
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Infrastructure - Basic Image Filters
3
+ *
4
+ * Core filter operations: brightness, contrast, saturation
5
+ */
6
+
7
+ export class BasicFilters {
8
+ static applyBrightness(
9
+ data: Uint8ClampedArray,
10
+ brightness: number
11
+ ): Uint8ClampedArray {
12
+ const result = new Uint8ClampedArray(data);
13
+ const adjustment = brightness * 2.55;
14
+
15
+ for (let i = 0; i < result.length; i += 4) {
16
+ result[i] = Math.min(255, Math.max(0, result[i] + adjustment));
17
+ result[i + 1] = Math.min(255, Math.max(0, result[i + 1] + adjustment));
18
+ result[i + 2] = Math.min(255, Math.max(0, result[i + 2] + adjustment));
19
+ }
20
+
21
+ return result;
22
+ }
23
+
24
+ static applyContrast(
25
+ data: Uint8ClampedArray,
26
+ contrast: number
27
+ ): Uint8ClampedArray {
28
+ const result = new Uint8ClampedArray(data);
29
+ const factor = (259 * (contrast * 2.55 + 255)) / (255 * (259 - contrast * 2.55));
30
+
31
+ for (let i = 0; i < result.length; i += 4) {
32
+ result[i] = Math.min(255, Math.max(0, factor * (result[i] - 128) + 128));
33
+ result[i + 1] = Math.min(255, Math.max(0, factor * (result[i + 1] - 128) + 128));
34
+ result[i + 2] = Math.min(255, Math.max(0, factor * (result[i + 2] - 128) + 128));
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ static applySaturation(
41
+ data: Uint8ClampedArray,
42
+ saturation: number
43
+ ): Uint8ClampedArray {
44
+ const result = new Uint8ClampedArray(data);
45
+ const adjustment = 1 + (saturation / 100);
46
+
47
+ for (let i = 0; i < result.length; i += 4) {
48
+ const r = result[i];
49
+ const g = result[i + 1];
50
+ const b = result[i + 2];
51
+
52
+ const gray = 0.299 * r + 0.587 * g + 0.114 * b;
53
+
54
+ result[i] = Math.min(255, Math.max(0, gray + adjustment * (r - gray)));
55
+ result[i + 1] = Math.min(255, Math.max(0, gray + adjustment * (g - gray)));
56
+ result[i + 2] = Math.min(255, Math.max(0, gray + adjustment * (b - gray)));
57
+ }
58
+
59
+ return result;
60
+ }
61
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Infrastructure - Filter Helper Utilities
3
+ *
4
+ * Common helper functions for filter operations
5
+ */
6
+
7
+ export class FilterHelpers {
8
+ static applyIntensity(
9
+ originalData: Uint8ClampedArray,
10
+ processedData: Uint8ClampedArray,
11
+ intensity: number
12
+ ): Uint8ClampedArray {
13
+ const result = new Uint8ClampedArray(originalData.length);
14
+
15
+ for (let i = 0; i < originalData.length; i++) {
16
+ result[i] = originalData[i] * (1 - intensity) + processedData[i] * intensity;
17
+ }
18
+
19
+ return result;
20
+ }
21
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Infrastructure - Special Image Filters
3
+ *
4
+ * Advanced filter operations: vintage, blur
5
+ */
6
+
7
+ export class SpecialFilters {
8
+ static applyVintage(
9
+ data: Uint8ClampedArray,
10
+ intensity: number,
11
+ warmth: number
12
+ ): Uint8ClampedArray {
13
+ const result = new Uint8ClampedArray(data);
14
+ const factor = intensity / 100;
15
+ const warmFactor = warmth / 100;
16
+
17
+ for (let i = 0; i < result.length; i += 4) {
18
+ let r = result[i];
19
+ let g = result[i + 1];
20
+ let b = result[i + 2];
21
+
22
+ const tr = 0.393 * r + 0.769 * g + 0.189 * b;
23
+ const tg = 0.349 * r + 0.686 * g + 0.168 * b;
24
+ const tb = 0.272 * r + 0.534 * g + 0.131 * b;
25
+
26
+ r = r * (1 - factor) + tr * factor;
27
+ g = g * (1 - factor) + tg * factor;
28
+ b = b * (1 - factor) + tb * factor;
29
+
30
+ if (warmFactor > 0) {
31
+ result[i] = Math.min(255, r + warmFactor * 20);
32
+ result[i + 1] = Math.min(255, g + warmFactor * 10);
33
+ result[i + 2] = Math.min(255, b * (1 - warmFactor * 0.3));
34
+ } else {
35
+ result[i] = r;
36
+ result[i + 1] = g;
37
+ result[i + 2] = Math.min(255, b * (1 - Math.abs(warmFactor) * 0.3));
38
+ }
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ static applyBlur(
45
+ data: Uint8ClampedArray,
46
+ radius: number,
47
+ width: number,
48
+ height: number
49
+ ): Uint8ClampedArray {
50
+ const result = new Uint8ClampedArray(data);
51
+ const size = Math.floor(radius) || 1;
52
+
53
+ for (let y = 0; y < height; y++) {
54
+ for (let x = 0; x < width; x++) {
55
+ let r = 0, g = 0, b = 0, a = 0;
56
+ let count = 0;
57
+
58
+ for (let dy = -size; dy <= size; dy++) {
59
+ for (let dx = -size; dx <= size; dx++) {
60
+ const ny = y + dy;
61
+ const nx = x + dx;
62
+
63
+ if (ny >= 0 && ny < height && nx >= 0 && nx < width) {
64
+ const idx = (ny * width + nx) * 4;
65
+ r += data[idx];
66
+ g += data[idx + 1];
67
+ b += data[idx + 2];
68
+ a += data[idx + 3];
69
+ count++;
70
+ }
71
+ }
72
+ }
73
+
74
+ const idx = (y * width + x) * 4;
75
+ result[idx] = r / count;
76
+ result[idx + 1] = g / count;
77
+ result[idx + 2] = b / count;
78
+ result[idx + 3] = a / count;
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+ }