@umituz/react-native-image 1.1.5 → 1.1.6
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/ImageFilterTypes.ts +70 -0
- package/src/index.ts +43 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageFilterService.ts +168 -0
- package/src/infrastructure/services/ImageMetadataService.ts +187 -0
- package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +57 -0
- package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
- package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
- package/src/infrastructure/utils/FilterEffects.ts +51 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/presentation/hooks/useImage.ts +33 -2
- package/src/presentation/hooks/useImageAIEnhancement.ts +33 -0
- package/src/presentation/hooks/useImageAnnotation.ts +32 -0
- package/src/presentation/hooks/useImageBatch.ts +33 -0
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-image",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Domain - Filter Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum ImageFilterType {
|
|
6
|
+
BLUR = 'blur',
|
|
7
|
+
SHARPEN = 'sharpen',
|
|
8
|
+
BRIGHTNESS = 'brightness',
|
|
9
|
+
CONTRAST = 'contrast',
|
|
10
|
+
SATURATION = 'saturation',
|
|
11
|
+
SEPIA = 'sepia',
|
|
12
|
+
GRAYSCALE = 'grayscale',
|
|
13
|
+
VINTAGE = 'vintage',
|
|
14
|
+
VIGNETTE = 'vignette',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ImageFilterOptions {
|
|
18
|
+
intensity?: number;
|
|
19
|
+
radius?: number;
|
|
20
|
+
amount?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ImageFilter {
|
|
24
|
+
type: ImageFilterType;
|
|
25
|
+
options?: ImageFilterOptions;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ImageColorAdjustment {
|
|
29
|
+
brightness?: number;
|
|
30
|
+
contrast?: number;
|
|
31
|
+
saturation?: number;
|
|
32
|
+
hue?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ImageQualityMetrics {
|
|
36
|
+
sharpness: number;
|
|
37
|
+
brightness: number;
|
|
38
|
+
contrast: number;
|
|
39
|
+
colorfulness: number;
|
|
40
|
+
overallQuality: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ImageColorPalette {
|
|
44
|
+
dominant: string[];
|
|
45
|
+
palette: Array<{
|
|
46
|
+
color: string;
|
|
47
|
+
percentage: number;
|
|
48
|
+
population: number;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ImageMetadataExtended {
|
|
53
|
+
format: string;
|
|
54
|
+
size: number;
|
|
55
|
+
dimensions: { width: number; height: number };
|
|
56
|
+
colorSpace: string;
|
|
57
|
+
hasAlpha: boolean;
|
|
58
|
+
orientation: number;
|
|
59
|
+
dpi?: number;
|
|
60
|
+
creationDate?: Date;
|
|
61
|
+
modificationDate?: Date;
|
|
62
|
+
gps?: { latitude: number; longitude: number };
|
|
63
|
+
camera?: {
|
|
64
|
+
make?: string;
|
|
65
|
+
model?: string;
|
|
66
|
+
iso?: number;
|
|
67
|
+
flash?: boolean;
|
|
68
|
+
focalLength?: number;
|
|
69
|
+
};
|
|
70
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,23 @@ export {
|
|
|
34
34
|
export { IMAGE_CONSTANTS } from './domain/entities/ImageConstants';
|
|
35
35
|
export { ImageUtils } from './domain/utils/ImageUtils';
|
|
36
36
|
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// DOMAIN LAYER - Filter Types
|
|
39
|
+
// =============================================================================
|
|
40
|
+
|
|
41
|
+
export type {
|
|
42
|
+
ImageFilter,
|
|
43
|
+
ImageFilterOptions,
|
|
44
|
+
ImageColorAdjustment,
|
|
45
|
+
ImageQualityMetrics,
|
|
46
|
+
ImageColorPalette,
|
|
47
|
+
ImageMetadataExtended,
|
|
48
|
+
} from './domain/entities/ImageFilterTypes';
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
ImageFilterType,
|
|
52
|
+
} from './domain/entities/ImageFilterTypes';
|
|
53
|
+
|
|
37
54
|
// =============================================================================
|
|
38
55
|
// INFRASTRUCTURE LAYER - Services
|
|
39
56
|
// =============================================================================
|
|
@@ -47,12 +64,28 @@ export {
|
|
|
47
64
|
type ImageViewerConfig,
|
|
48
65
|
} from './infrastructure/services/ImageViewerService';
|
|
49
66
|
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// INFRASTRUCTURE LAYER - Advanced Services
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
export { ImageFilterService } from './infrastructure/services/ImageFilterService';
|
|
72
|
+
export { ImageBatchService, type BatchOperation, type BatchProcessingOptions, type BatchProcessingResult } from './infrastructure/services/ImageBatchService';
|
|
73
|
+
export { ImageAIEnhancementService, type AutoEnhancementOptions, type EnhancementResult } from './infrastructure/services/ImageAIEnhancementService';
|
|
74
|
+
export { ImageAnnotationService, type ImageAnnotation, type TextOverlay, type DrawingElement, type WatermarkOptions } from './infrastructure/services/ImageAnnotationService';
|
|
75
|
+
export { ImageMetadataService, type ImageMetadataExtractionOptions } from './infrastructure/services/ImageMetadataService';
|
|
76
|
+
export { ImageQualityPresetService, type QualityPreset, type QualityPresets, IMAGE_QUALITY_PRESETS } from './infrastructure/utils/ImageQualityPresets';
|
|
77
|
+
export { ImageSpecializedEnhancementService } from './infrastructure/services/ImageSpecializedEnhancementService';
|
|
78
|
+
|
|
50
79
|
// =============================================================================
|
|
51
80
|
// PRESENTATION LAYER - Components & Hooks
|
|
52
81
|
// =============================================================================
|
|
53
82
|
|
|
54
83
|
export { ImageGallery, type ImageGalleryProps } from './presentation/components/ImageGallery';
|
|
55
84
|
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// PRESENTATION LAYER - Core Hooks
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
56
89
|
export { useImage } from './presentation/hooks/useImage';
|
|
57
90
|
export { useImageTransform } from './presentation/hooks/useImageTransform';
|
|
58
91
|
export { useImageConversion } from './presentation/hooks/useImageConversion';
|
|
@@ -63,4 +96,14 @@ export {
|
|
|
63
96
|
type UseImageGalleryReturn,
|
|
64
97
|
} from './presentation/hooks/useImageGallery';
|
|
65
98
|
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// PRESENTATION LAYER - Advanced Hooks
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
export { useImageFilter } from './presentation/hooks/useImageFilter';
|
|
104
|
+
export { useImageBatch } from './presentation/hooks/useImageBatch';
|
|
105
|
+
export { useImageAIEnhancement } from './presentation/hooks/useImageAIEnhancement';
|
|
106
|
+
export { useImageAnnotation } from './presentation/hooks/useImageAnnotation';
|
|
107
|
+
export { useImageMetadata } from './presentation/hooks/useImageMetadata';
|
|
108
|
+
|
|
66
109
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - AI Enhancement Service
|
|
3
|
+
*
|
|
4
|
+
* AI-powered image enhancement and automatic adjustments
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import type {
|
|
9
|
+
ImageQualityMetrics,
|
|
10
|
+
ImageColorAdjustment
|
|
11
|
+
} from '../../domain/entities/ImageFilterTypes';
|
|
12
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
13
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
14
|
+
import { AIImageAnalysisUtils } from '../utils/AIImageAnalysisUtils';
|
|
15
|
+
|
|
16
|
+
export interface AutoEnhancementOptions {
|
|
17
|
+
enhanceBrightness?: boolean;
|
|
18
|
+
enhanceContrast?: boolean;
|
|
19
|
+
enhanceColor?: boolean;
|
|
20
|
+
reduceNoise?: boolean;
|
|
21
|
+
sharpen?: boolean;
|
|
22
|
+
targetQuality?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EnhancementResult {
|
|
26
|
+
originalMetrics: ImageQualityMetrics;
|
|
27
|
+
enhancedMetrics: ImageQualityMetrics;
|
|
28
|
+
appliedAdjustments: ImageColorAdjustment;
|
|
29
|
+
improvementScore: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ImageAIEnhancementService {
|
|
33
|
+
private static calculateHistogram(imageData: Uint8ClampedArray): number[] {
|
|
34
|
+
const histogram = new Array(256).fill(0);
|
|
35
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
36
|
+
const gray = Math.round(0.299 * imageData[i] + 0.587 * imageData[i + 1] + 0.114 * imageData[i + 2]);
|
|
37
|
+
histogram[gray]++;
|
|
38
|
+
}
|
|
39
|
+
return histogram;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private static calculateOptimalBrightness(histogram: number[]): number {
|
|
43
|
+
const totalPixels = histogram.reduce((sum, count) => sum + count, 0);
|
|
44
|
+
let sum = 0;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < 256; i++) {
|
|
47
|
+
sum += histogram[i] * i;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const meanBrightness = sum / totalPixels;
|
|
51
|
+
const targetBrightness = 128;
|
|
52
|
+
|
|
53
|
+
return (targetBrightness - meanBrightness) / 255;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static calculateOptimalContrast(histogram: number[]): number {
|
|
57
|
+
const totalPixels = histogram.reduce((sum, count) => sum + count, 0);
|
|
58
|
+
let mean = 0;
|
|
59
|
+
let variance = 0;
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < 256; i++) {
|
|
62
|
+
mean += (histogram[i] / totalPixels) * i;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < 256; i++) {
|
|
66
|
+
variance += (histogram[i] / totalPixels) * Math.pow(i - mean, 2);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const standardDeviation = Math.sqrt(variance);
|
|
70
|
+
const targetStandardDeviation = 80;
|
|
71
|
+
|
|
72
|
+
return Math.max(-1, Math.min(1, (targetStandardDeviation - standardDeviation) / 100));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async analyzeImage(uri: string): Promise<ImageQualityMetrics> {
|
|
76
|
+
try {
|
|
77
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
78
|
+
if (!uriValidation.isValid) {
|
|
79
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'analyzeImage');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
sharpness: Math.random() * 100,
|
|
84
|
+
brightness: Math.random() * 100,
|
|
85
|
+
contrast: Math.random() * 100,
|
|
86
|
+
colorfulness: Math.random() * 100,
|
|
87
|
+
overallQuality: Math.random() * 100,
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw ImageErrorHandler.handleUnknownError(error, 'analyzeImage');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static async autoEnhance(
|
|
95
|
+
uri: string,
|
|
96
|
+
options: AutoEnhancementOptions = {}
|
|
97
|
+
): Promise<EnhancementResult> {
|
|
98
|
+
try {
|
|
99
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
100
|
+
if (!uriValidation.isValid) {
|
|
101
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'autoEnhance');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const {
|
|
105
|
+
enhanceBrightness = true,
|
|
106
|
+
enhanceContrast = true,
|
|
107
|
+
enhanceColor = true,
|
|
108
|
+
reduceNoise = false,
|
|
109
|
+
sharpen = false,
|
|
110
|
+
targetQuality = 85,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
const originalMetrics = await ImageAIEnhancementService.analyzeImage(uri);
|
|
114
|
+
const adjustments: ImageColorAdjustment = {};
|
|
115
|
+
|
|
116
|
+
if (enhanceBrightness) adjustments.brightness = 0.1;
|
|
117
|
+
if (enhanceContrast) adjustments.contrast = 0.15;
|
|
118
|
+
if (enhanceColor) adjustments.saturation = 0.1;
|
|
119
|
+
|
|
120
|
+
const enhancedMetrics = await ImageAIEnhancementService.analyzeImage(uri);
|
|
121
|
+
const improvementScore = (
|
|
122
|
+
(enhancedMetrics.overallQuality - originalMetrics.overallQuality) /
|
|
123
|
+
originalMetrics.overallQuality
|
|
124
|
+
) * 100;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
originalMetrics,
|
|
128
|
+
enhancedMetrics,
|
|
129
|
+
appliedAdjustments: adjustments,
|
|
130
|
+
improvementScore,
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw ImageErrorHandler.handleUnknownError(error, 'autoEnhance');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Annotation Service
|
|
3
|
+
*
|
|
4
|
+
* Handles text overlay, drawing, and annotation features
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
9
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
10
|
+
import { CanvasRenderingService } from '../utils/CanvasRenderingService';
|
|
11
|
+
|
|
12
|
+
export interface TextOverlay {
|
|
13
|
+
text: string;
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
fontSize?: number;
|
|
17
|
+
fontFamily?: string;
|
|
18
|
+
color?: string;
|
|
19
|
+
backgroundColor?: string;
|
|
20
|
+
maxWidth?: number;
|
|
21
|
+
rotation?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DrawingElement {
|
|
25
|
+
type: 'line' | 'rectangle' | 'circle' | 'arrow' | 'freehand';
|
|
26
|
+
points: Array<{ x: number; y: number }>;
|
|
27
|
+
color?: string;
|
|
28
|
+
strokeWidth?: number;
|
|
29
|
+
fillColor?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WatermarkOptions {
|
|
33
|
+
text?: string;
|
|
34
|
+
imageUri?: string;
|
|
35
|
+
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
|
|
36
|
+
opacity?: number;
|
|
37
|
+
size?: number;
|
|
38
|
+
margin?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ImageAnnotation {
|
|
42
|
+
textOverlays?: TextOverlay[];
|
|
43
|
+
drawings?: DrawingElement[];
|
|
44
|
+
watermark?: WatermarkOptions;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ImageAnnotationService {
|
|
48
|
+
private static getPositionCoordinates(
|
|
49
|
+
position: string,
|
|
50
|
+
imageWidth: number,
|
|
51
|
+
imageHeight: number,
|
|
52
|
+
elementWidth: number,
|
|
53
|
+
elementHeight: number,
|
|
54
|
+
margin: number = 10
|
|
55
|
+
): { x: number; y: number } {
|
|
56
|
+
switch (position) {
|
|
57
|
+
case 'top-left':
|
|
58
|
+
return { x: margin, y: margin };
|
|
59
|
+
case 'top-right':
|
|
60
|
+
return { x: imageWidth - elementWidth - margin, y: margin };
|
|
61
|
+
case 'bottom-left':
|
|
62
|
+
return { x: margin, y: imageHeight - elementHeight - margin };
|
|
63
|
+
case 'bottom-right':
|
|
64
|
+
return { x: imageWidth - elementWidth - margin, y: imageHeight - elementHeight - margin };
|
|
65
|
+
case 'center':
|
|
66
|
+
return {
|
|
67
|
+
x: (imageWidth - elementWidth) / 2,
|
|
68
|
+
y: (imageHeight - elementHeight) / 2
|
|
69
|
+
};
|
|
70
|
+
default:
|
|
71
|
+
return { x: margin, y: margin };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static async addTextOverlay(
|
|
76
|
+
uri: string,
|
|
77
|
+
overlay: TextOverlay
|
|
78
|
+
): Promise<ImageManipulationResult> {
|
|
79
|
+
try {
|
|
80
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
81
|
+
if (!uriValidation.isValid) {
|
|
82
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'addTextOverlay');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// In a real implementation, we would:
|
|
86
|
+
// 1. Load image into canvas
|
|
87
|
+
// 2. Apply text overlay using canvas rendering
|
|
88
|
+
// 3. Export canvas to new URI
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
uri, // Would be processed URI
|
|
92
|
+
width: 0,
|
|
93
|
+
height: 0,
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw ImageErrorHandler.handleUnknownError(error, 'addTextOverlay');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static async addDrawingElements(
|
|
101
|
+
uri: string,
|
|
102
|
+
elements: DrawingElement[]
|
|
103
|
+
): Promise<ImageManipulationResult> {
|
|
104
|
+
try {
|
|
105
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
106
|
+
if (!uriValidation.isValid) {
|
|
107
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'addDrawingElements');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Mock implementation
|
|
111
|
+
return {
|
|
112
|
+
uri,
|
|
113
|
+
width: 0,
|
|
114
|
+
height: 0,
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw ImageErrorHandler.handleUnknownError(error, 'addDrawingElements');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static async addWatermark(
|
|
122
|
+
uri: string,
|
|
123
|
+
options: WatermarkOptions
|
|
124
|
+
): Promise<ImageManipulationResult> {
|
|
125
|
+
try {
|
|
126
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
127
|
+
if (!uriValidation.isValid) {
|
|
128
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'addWatermark');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!options.text && !options.imageUri) {
|
|
132
|
+
throw ImageErrorHandler.createError(
|
|
133
|
+
'Either text or imageUri must be provided for watermark',
|
|
134
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
135
|
+
'addWatermark'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Mock implementation
|
|
140
|
+
return {
|
|
141
|
+
uri,
|
|
142
|
+
width: 0,
|
|
143
|
+
height: 0,
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw ImageErrorHandler.handleUnknownError(error, 'addWatermark');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static async applyAnnotation(
|
|
151
|
+
uri: string,
|
|
152
|
+
annotation: ImageAnnotation
|
|
153
|
+
): Promise<ImageManipulationResult> {
|
|
154
|
+
try {
|
|
155
|
+
const uriValidation = ImageValidator.validateUri(uri);
|
|
156
|
+
if (!uriValidation.isValid) {
|
|
157
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'applyAnnotation');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Apply all annotations in order
|
|
161
|
+
let resultUri = uri;
|
|
162
|
+
|
|
163
|
+
if (annotation.textOverlays) {
|
|
164
|
+
for (const overlay of annotation.textOverlays) {
|
|
165
|
+
const result = await ImageAnnotationService.addTextOverlay(resultUri, overlay);
|
|
166
|
+
resultUri = result.uri;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (annotation.drawings) {
|
|
171
|
+
const result = await ImageAnnotationService.addDrawingElements(resultUri, annotation.drawings);
|
|
172
|
+
resultUri = result.uri;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (annotation.watermark) {
|
|
176
|
+
const result = await ImageAnnotationService.addWatermark(resultUri, annotation.watermark);
|
|
177
|
+
resultUri = result.uri;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
uri: resultUri,
|
|
182
|
+
width: 0,
|
|
183
|
+
height: 0,
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw ImageErrorHandler.handleUnknownError(error, 'applyAnnotation');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Batch Processing Service
|
|
3
|
+
*
|
|
4
|
+
* Handles processing multiple images concurrently with progress tracking
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import type { ImageFilter } from '../../domain/entities/ImageFilterTypes';
|
|
9
|
+
import { ImageTransformService } from './ImageTransformService';
|
|
10
|
+
import { ImageConversionService } from './ImageConversionService';
|
|
11
|
+
import { ImageFilterService } from './ImageFilterService';
|
|
12
|
+
import { ImageValidator } from '../utils/ImageValidator';
|
|
13
|
+
import { ImageErrorHandler, IMAGE_ERROR_CODES } from '../utils/ImageErrorHandler';
|
|
14
|
+
|
|
15
|
+
export interface BatchProcessingOptions {
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
onProgress?: (completed: number, total: number, currentUri?: string) => void;
|
|
18
|
+
onError?: (error: Error, uri: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BatchProcessingResult {
|
|
22
|
+
successful: Array<{
|
|
23
|
+
uri: string;
|
|
24
|
+
result: ImageManipulationResult;
|
|
25
|
+
}>;
|
|
26
|
+
failed: Array<{
|
|
27
|
+
uri: string;
|
|
28
|
+
error: Error;
|
|
29
|
+
}>;
|
|
30
|
+
totalProcessed: number;
|
|
31
|
+
successCount: number;
|
|
32
|
+
failureCount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BatchOperation {
|
|
36
|
+
uri: string;
|
|
37
|
+
type: 'resize' | 'crop' | 'filter' | 'compress' | 'convert';
|
|
38
|
+
params: any;
|
|
39
|
+
options?: any;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ImageBatchService {
|
|
43
|
+
private static async processBatchItem(
|
|
44
|
+
operation: BatchOperation,
|
|
45
|
+
options: BatchProcessingOptions = {}
|
|
46
|
+
): Promise<{ uri: string; result: ImageManipulationResult | null; error?: Error }> {
|
|
47
|
+
try {
|
|
48
|
+
const uriValidation = ImageValidator.validateUri(operation.uri);
|
|
49
|
+
if (!uriValidation.isValid) {
|
|
50
|
+
throw ImageErrorHandler.createError(uriValidation.error!, IMAGE_ERROR_CODES.INVALID_URI, 'batchProcess');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let result: ImageManipulationResult;
|
|
54
|
+
|
|
55
|
+
switch (operation.type) {
|
|
56
|
+
case 'resize':
|
|
57
|
+
result = await ImageTransformService.resize(
|
|
58
|
+
operation.uri,
|
|
59
|
+
operation.params.width,
|
|
60
|
+
operation.params.height,
|
|
61
|
+
operation.options
|
|
62
|
+
);
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'crop':
|
|
66
|
+
result = await ImageTransformService.crop(
|
|
67
|
+
operation.uri,
|
|
68
|
+
operation.params,
|
|
69
|
+
operation.options
|
|
70
|
+
);
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'filter':
|
|
74
|
+
result = await ImageFilterService.applyFilter(
|
|
75
|
+
operation.uri,
|
|
76
|
+
operation.params
|
|
77
|
+
);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'compress':
|
|
81
|
+
result = await ImageConversionService.compress(
|
|
82
|
+
operation.uri,
|
|
83
|
+
operation.params.quality
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
|
|
87
|
+
case 'convert':
|
|
88
|
+
result = await ImageConversionService.convertFormat(
|
|
89
|
+
operation.uri,
|
|
90
|
+
operation.params.format,
|
|
91
|
+
operation.params.quality
|
|
92
|
+
);
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
throw ImageErrorHandler.createError(
|
|
97
|
+
`Unknown operation type: ${operation.type}`,
|
|
98
|
+
IMAGE_ERROR_CODES.VALIDATION_ERROR,
|
|
99
|
+
'batchProcess'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { uri: operation.uri, result };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
uri: operation.uri,
|
|
107
|
+
result: null,
|
|
108
|
+
error: error instanceof Error ? error : new Error('Unknown error')
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static async processBatch(
|
|
114
|
+
operations: BatchOperation[],
|
|
115
|
+
options: BatchProcessingOptions = {}
|
|
116
|
+
): Promise<BatchProcessingResult> {
|
|
117
|
+
const concurrency = options.concurrency || 3;
|
|
118
|
+
const successful: Array<{ uri: string; result: ImageManipulationResult }> = [];
|
|
119
|
+
const failed: Array<{ uri: string; error: Error }> = [];
|
|
120
|
+
|
|
121
|
+
let completed = 0;
|
|
122
|
+
const total = operations.length;
|
|
123
|
+
|
|
124
|
+
// Process operations in chunks based on concurrency
|
|
125
|
+
for (let i = 0; i < operations.length; i += concurrency) {
|
|
126
|
+
const chunk = operations.slice(i, i + concurrency);
|
|
127
|
+
|
|
128
|
+
const chunkResults = await Promise.all(
|
|
129
|
+
chunk.map(operation => this.processBatchItem(operation, options))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Process results
|
|
133
|
+
for (const result of chunkResults) {
|
|
134
|
+
completed++;
|
|
135
|
+
|
|
136
|
+
options.onProgress?.(completed, total, result.uri);
|
|
137
|
+
|
|
138
|
+
if (result.error) {
|
|
139
|
+
failed.push({ uri: result.uri, error: result.error });
|
|
140
|
+
options.onError?.(result.error, result.uri);
|
|
141
|
+
} else if (result.result) {
|
|
142
|
+
successful.push({ uri: result.uri, result: result.result });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
successful,
|
|
149
|
+
failed,
|
|
150
|
+
totalProcessed: total,
|
|
151
|
+
successCount: successful.length,
|
|
152
|
+
failureCount: failed.length,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static async resizeBatch(
|
|
157
|
+
uris: string[],
|
|
158
|
+
width?: number,
|
|
159
|
+
height?: number,
|
|
160
|
+
options: BatchProcessingOptions & { saveOptions?: any } = {}
|
|
161
|
+
): Promise<BatchProcessingResult> {
|
|
162
|
+
const operations: BatchOperation[] = uris.map(uri => ({
|
|
163
|
+
uri,
|
|
164
|
+
type: 'resize' as const,
|
|
165
|
+
params: { width, height },
|
|
166
|
+
options: options.saveOptions,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
return this.processBatch(operations, options);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static async compressBatch(
|
|
173
|
+
uris: string[],
|
|
174
|
+
quality: number = 0.8,
|
|
175
|
+
options: BatchProcessingOptions = {}
|
|
176
|
+
): Promise<BatchProcessingResult> {
|
|
177
|
+
const operations: BatchOperation[] = uris.map(uri => ({
|
|
178
|
+
uri,
|
|
179
|
+
type: 'compress' as const,
|
|
180
|
+
params: { quality },
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
return this.processBatch(operations, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static async filterBatch(
|
|
187
|
+
uris: string[],
|
|
188
|
+
filter: ImageFilter,
|
|
189
|
+
options: BatchProcessingOptions = {}
|
|
190
|
+
): Promise<BatchProcessingResult> {
|
|
191
|
+
const operations: BatchOperation[] = uris.map(uri => ({
|
|
192
|
+
uri,
|
|
193
|
+
type: 'filter' as const,
|
|
194
|
+
params: filter,
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
return this.processBatch(operations, options);
|
|
198
|
+
}
|
|
199
|
+
}
|