@umituz/react-native-image 1.3.12 → 1.3.14
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 +1 -1
- package/src/domain/entities/EditorTypes.ts +15 -172
- package/src/domain/entities/editor/EditorConfigTypes.ts +35 -0
- package/src/domain/entities/editor/EditorElementTypes.ts +60 -0
- package/src/domain/entities/editor/EditorFilterTypes.ts +9 -0
- package/src/domain/entities/editor/EditorLayerTypes.ts +34 -0
- package/src/domain/entities/editor/EditorStateTypes.ts +35 -0
- package/src/domain/entities/editor/EditorToolTypes.ts +33 -0
- package/src/infrastructure/services/ImageBatchService.ts +9 -75
- package/src/infrastructure/services/ImageMetadataService.ts +13 -84
- package/src/infrastructure/utils/BatchProcessor.ts +95 -0
- package/src/infrastructure/utils/ImageFilterUtils.ts +15 -146
- package/src/infrastructure/utils/MetadataExtractor.ts +83 -0
- package/src/infrastructure/utils/filters/BasicFilters.ts +61 -0
- package/src/infrastructure/utils/filters/FilterHelpers.ts +21 -0
- package/src/infrastructure/utils/filters/SpecialFilters.ts +84 -0
- package/src/presentation/components/editor/FilterPickerSheet.tsx +1 -1
- package/src/presentation/components/editor/StickerPickerSheet.tsx +1 -1
- package/src/presentation/components/editor/TextEditorTabs.tsx +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-image",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.14",
|
|
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 -
|
|
2
|
+
* Domain - Editor Types (Main Export)
|
|
3
|
+
*
|
|
4
|
+
* Central export point for all editor-related types
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
MARKER = 'marker',
|
|
30
|
-
SPRAY = 'spray',
|
|
31
|
-
PENCIL = 'pencil',
|
|
32
|
-
CALIGRAPHY = 'caligraphy',
|
|
33
|
-
}
|
|
13
|
+
// Layers & History
|
|
14
|
+
export * from './editor/EditorLayerTypes';
|
|
34
15
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
y: number;
|
|
38
|
-
}
|
|
16
|
+
// State
|
|
17
|
+
export * from './editor/EditorStateTypes';
|
|
39
18
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
height: number;
|
|
43
|
-
}
|
|
19
|
+
// Configuration
|
|
20
|
+
export * from './editor/EditorConfigTypes';
|
|
44
21
|
|
|
45
|
-
|
|
46
|
-
|
|
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,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 {
|
|
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:
|
|
37
|
-
options?:
|
|
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 =>
|
|
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?:
|
|
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
|
|
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
|
|
109
|
-
const size = await
|
|
110
|
-
const format =
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
165
|
-
const size = await
|
|
166
|
-
const format =
|
|
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
|
|
181
|
-
const gpsData = await
|
|
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
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Legacy wrapper for backward compatibility. Use specific filter classes directly.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
@@ -43,7 +43,7 @@ export const FilterPickerSheet = forwardRef<BottomSheetModalRef, FilterPickerShe
|
|
|
43
43
|
width: 100,
|
|
44
44
|
height: 120,
|
|
45
45
|
backgroundColor: activeFilterId === preset.id ? tokens.colors.primaryContainer : tokens.colors.surfaceVariant,
|
|
46
|
-
borderRadius: tokens.
|
|
46
|
+
borderRadius: tokens.radius.lg,
|
|
47
47
|
alignItems: 'center',
|
|
48
48
|
justifyContent: 'center',
|
|
49
49
|
borderWidth: 2,
|
|
@@ -42,7 +42,7 @@ export const StickerPickerSheet = forwardRef<BottomSheetModalRef, StickerPickerS
|
|
|
42
42
|
width: (SCREEN_WIDTH - 64) / 3,
|
|
43
43
|
aspectRatio: 1,
|
|
44
44
|
backgroundColor: tokens.colors.surfaceVariant,
|
|
45
|
-
borderRadius: tokens.
|
|
45
|
+
borderRadius: tokens.radius.md,
|
|
46
46
|
padding: tokens.spacing.sm,
|
|
47
47
|
alignItems: 'center',
|
|
48
48
|
justifyContent: 'center',
|
|
@@ -23,7 +23,7 @@ export const TextContentTab: React.FC<TabProps & { text: string; onTextChange: (
|
|
|
23
23
|
...tokens.typography.bodyLarge,
|
|
24
24
|
borderWidth: 1,
|
|
25
25
|
borderColor: tokens.colors.border,
|
|
26
|
-
borderRadius: tokens.
|
|
26
|
+
borderRadius: tokens.radius.md,
|
|
27
27
|
padding: tokens.spacing.md,
|
|
28
28
|
minHeight: 120,
|
|
29
29
|
textAlignVertical: 'top',
|
|
@@ -50,7 +50,7 @@ export const TextStyleTab: React.FC<TabProps & {
|
|
|
50
50
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ gap: tokens.spacing.sm }}>
|
|
51
51
|
{fonts.map(f => (
|
|
52
52
|
<TouchableOpacity key={f} onPress={() => setFontFamily(f)} style={{
|
|
53
|
-
paddingHorizontal: tokens.spacing.md, paddingVertical: tokens.spacing.xs, borderRadius: tokens.
|
|
53
|
+
paddingHorizontal: tokens.spacing.md, paddingVertical: tokens.spacing.xs, borderRadius: tokens.radius.full,
|
|
54
54
|
borderWidth: 1, borderColor: fontFamily === f ? tokens.colors.primary : tokens.colors.border,
|
|
55
55
|
backgroundColor: fontFamily === f ? tokens.colors.primaryContainer : tokens.colors.surface
|
|
56
56
|
}}>
|
|
@@ -100,7 +100,7 @@ export const TextTransformTab: React.FC<TabProps & {
|
|
|
100
100
|
{onDelete && (
|
|
101
101
|
<TouchableOpacity onPress={onDelete} style={{
|
|
102
102
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: tokens.spacing.sm,
|
|
103
|
-
padding: tokens.spacing.md, borderRadius: tokens.
|
|
103
|
+
padding: tokens.spacing.md, borderRadius: tokens.radius.md, borderWidth: 1, borderColor: tokens.colors.error
|
|
104
104
|
}}>
|
|
105
105
|
<AtomicIcon name="trash" size={20} color="error" />
|
|
106
106
|
<AtomicText style={{ ...tokens.typography.labelMedium, color: tokens.colors.error }}>Delete Layer</AtomicText>
|