@umituz/react-native-image 1.1.4 → 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 +2 -2
- package/src/domain/entities/ImageConstants.ts +32 -15
- package/src/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/domain/entities/ImageTypes.ts +22 -7
- package/src/domain/utils/ImageUtils.ts +6 -6
- package/src/index.ts +47 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAdvancedTransformService.ts +106 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageConversionService.ts +51 -18
- 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/services/ImageStorageService.ts +22 -7
- package/src/infrastructure/services/ImageTransformService.ts +68 -101
- package/src/infrastructure/services/ImageViewerService.ts +3 -28
- 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/ImageErrorHandler.ts +40 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/infrastructure/utils/ImageValidator.ts +59 -0
- package/src/presentation/components/GalleryHeader.tsx +3 -4
- package/src/presentation/components/ImageGallery.tsx +7 -20
- package/src/presentation/hooks/useImage.ts +35 -5
- 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/useImageConversion.ts +6 -3
- package/src/presentation/hooks/useImageEditor.ts +5 -11
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageGallery.ts +1 -60
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
- package/src/presentation/hooks/useImageOperation.ts +14 -10
- package/src/presentation/hooks/useImageTransform.ts +13 -7
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - AI Image Analysis
|
|
3
|
+
*
|
|
4
|
+
* Advanced image analysis utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class AIImageAnalysisUtils {
|
|
8
|
+
static calculateColorBalance(imageData: Uint8ClampedArray): {
|
|
9
|
+
redBalance: number;
|
|
10
|
+
greenBalance: number;
|
|
11
|
+
blueBalance: number;
|
|
12
|
+
} {
|
|
13
|
+
let redSum = 0, greenSum = 0, blueSum = 0;
|
|
14
|
+
const pixelCount = imageData.length / 4;
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
17
|
+
redSum += imageData[i];
|
|
18
|
+
greenSum += imageData[i + 1];
|
|
19
|
+
blueSum += imageData[i + 2];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const redMean = redSum / pixelCount;
|
|
23
|
+
const greenMean = greenSum / pixelCount;
|
|
24
|
+
const blueMean = blueSum / pixelCount;
|
|
25
|
+
const grayMean = (redMean + greenMean + blueMean) / 3;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
redBalance: (grayMean - redMean) / 255,
|
|
29
|
+
greenBalance: (grayMean - greenMean) / 255,
|
|
30
|
+
blueBalance: (grayMean - blueMean) / 255,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static detectNoise(imageData: Uint8ClampedArray): number {
|
|
35
|
+
let noiseLevel = 0;
|
|
36
|
+
let sampleCount = 0;
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < imageData.length - 40; i += 40) {
|
|
39
|
+
const r1 = imageData[i];
|
|
40
|
+
const g1 = imageData[i + 1];
|
|
41
|
+
const b1 = imageData[i + 2];
|
|
42
|
+
|
|
43
|
+
const r2 = imageData[i + 4];
|
|
44
|
+
const g2 = imageData[i + 5];
|
|
45
|
+
const b2 = imageData[i + 6];
|
|
46
|
+
|
|
47
|
+
const diff = Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2);
|
|
48
|
+
noiseLevel += diff;
|
|
49
|
+
sampleCount++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return noiseLevel / (sampleCount * 3 * 255);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static applySharpening(imageData: Uint8ClampedArray): Uint8ClampedArray {
|
|
56
|
+
const result = new Uint8ClampedArray(imageData.length);
|
|
57
|
+
const width = Math.sqrt(imageData.length / 4);
|
|
58
|
+
const kernel = [
|
|
59
|
+
0, -1, 0,
|
|
60
|
+
-1, 5, -1,
|
|
61
|
+
0, -1, 0
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
65
|
+
const pixelIndex = i / 4;
|
|
66
|
+
const x = pixelIndex % width;
|
|
67
|
+
const y = Math.floor(pixelIndex / width);
|
|
68
|
+
|
|
69
|
+
for (let c = 0; c < 3; c++) {
|
|
70
|
+
let sum = 0;
|
|
71
|
+
for (let ky = -1; ky <= 1; ky++) {
|
|
72
|
+
for (let kx = -1; kx <= 1; kx++) {
|
|
73
|
+
const nx = x + kx;
|
|
74
|
+
const ny = y + ky;
|
|
75
|
+
|
|
76
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < width) {
|
|
77
|
+
const neighborIndex = (ny * width + nx) * 4 + c;
|
|
78
|
+
sum += imageData[neighborIndex] * kernel[(ky + 1) * 3 + (kx + 1)];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
result[i + c] = Math.min(255, Math.max(0, sum));
|
|
83
|
+
}
|
|
84
|
+
result[i + 3] = imageData[i + 3];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static applyNoiseReduction(imageData: Uint8ClampedArray): Uint8ClampedArray {
|
|
91
|
+
const result = new Uint8ClampedArray(imageData.length);
|
|
92
|
+
const width = Math.sqrt(imageData.length / 4);
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < imageData.length; i += 4) {
|
|
95
|
+
const pixelIndex = i / 4;
|
|
96
|
+
const x = pixelIndex % width;
|
|
97
|
+
const y = Math.floor(pixelIndex / width);
|
|
98
|
+
|
|
99
|
+
for (let c = 0; c < 3; c++) {
|
|
100
|
+
const values = [];
|
|
101
|
+
|
|
102
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
103
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
104
|
+
const nx = x + dx;
|
|
105
|
+
const ny = y + dy;
|
|
106
|
+
|
|
107
|
+
if (nx >= 0 && nx < width && ny >= 0 && ny < width) {
|
|
108
|
+
const neighborIndex = (ny * width + nx) * 4 + c;
|
|
109
|
+
values.push(imageData[neighborIndex]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
values.sort((a, b) => a - b);
|
|
115
|
+
result[i + c] = values[Math.floor(values.length / 2)];
|
|
116
|
+
}
|
|
117
|
+
result[i + 3] = imageData[i + 3];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Canvas Rendering Service
|
|
3
|
+
*
|
|
4
|
+
* Canvas-based rendering utilities for annotations and filters
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class CanvasRenderingService {
|
|
8
|
+
static renderTextOnCanvas(
|
|
9
|
+
ctx: CanvasRenderingContext2D,
|
|
10
|
+
overlay: import('../services/ImageAnnotationService').TextOverlay
|
|
11
|
+
): void {
|
|
12
|
+
ctx.save();
|
|
13
|
+
|
|
14
|
+
ctx.font = `${overlay.fontSize || 16}px ${overlay.fontFamily || 'Arial'}`;
|
|
15
|
+
ctx.fillStyle = overlay.color || '#000000';
|
|
16
|
+
|
|
17
|
+
if (overlay.backgroundColor) {
|
|
18
|
+
const metrics = ctx.measureText(overlay.text);
|
|
19
|
+
const padding = 4;
|
|
20
|
+
ctx.fillStyle = overlay.backgroundColor;
|
|
21
|
+
ctx.fillRect(
|
|
22
|
+
overlay.x - padding,
|
|
23
|
+
overlay.y - (overlay.fontSize || 16) - padding,
|
|
24
|
+
metrics.width + padding * 2,
|
|
25
|
+
(overlay.fontSize || 16) + padding * 2
|
|
26
|
+
);
|
|
27
|
+
ctx.fillStyle = overlay.color || '#000000';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (overlay.rotation) {
|
|
31
|
+
ctx.translate(overlay.x, overlay.y);
|
|
32
|
+
ctx.rotate((overlay.rotation * Math.PI) / 180);
|
|
33
|
+
ctx.fillText(overlay.text, 0, 0);
|
|
34
|
+
} else {
|
|
35
|
+
ctx.fillText(overlay.text, overlay.x, overlay.y);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ctx.restore();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static renderDrawingOnCanvas(
|
|
42
|
+
ctx: CanvasRenderingContext2D,
|
|
43
|
+
drawing: import('../services/ImageAnnotationService').DrawingElement
|
|
44
|
+
): void {
|
|
45
|
+
ctx.save();
|
|
46
|
+
ctx.strokeStyle = drawing.color || '#000000';
|
|
47
|
+
ctx.lineWidth = drawing.strokeWidth || 2;
|
|
48
|
+
|
|
49
|
+
if (drawing.fillColor) {
|
|
50
|
+
ctx.fillStyle = drawing.fillColor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (drawing.type) {
|
|
54
|
+
case 'line':
|
|
55
|
+
if (drawing.points.length >= 2) {
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
ctx.moveTo(drawing.points[0].x, drawing.points[0].y);
|
|
58
|
+
for (let i = 1; i < drawing.points.length; i++) {
|
|
59
|
+
ctx.lineTo(drawing.points[i].x, drawing.points[i].y);
|
|
60
|
+
}
|
|
61
|
+
ctx.stroke();
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
|
|
65
|
+
case 'rectangle':
|
|
66
|
+
if (drawing.points.length >= 2) {
|
|
67
|
+
const width = drawing.points[1].x - drawing.points[0].x;
|
|
68
|
+
const height = drawing.points[1].y - drawing.points[0].y;
|
|
69
|
+
if (drawing.fillColor) {
|
|
70
|
+
ctx.fillRect(drawing.points[0].x, drawing.points[0].y, width, height);
|
|
71
|
+
}
|
|
72
|
+
ctx.strokeRect(drawing.points[0].x, drawing.points[0].y, width, height);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case 'circle':
|
|
77
|
+
if (drawing.points.length >= 2) {
|
|
78
|
+
const radius = Math.sqrt(
|
|
79
|
+
Math.pow(drawing.points[1].x - drawing.points[0].x, 2) +
|
|
80
|
+
Math.pow(drawing.points[1].y - drawing.points[0].y, 2)
|
|
81
|
+
);
|
|
82
|
+
ctx.beginPath();
|
|
83
|
+
ctx.arc(drawing.points[0].x, drawing.points[0].y, radius, 0, 2 * Math.PI);
|
|
84
|
+
if (drawing.fillColor) {
|
|
85
|
+
ctx.fill();
|
|
86
|
+
}
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'arrow':
|
|
92
|
+
if (drawing.points.length >= 2) {
|
|
93
|
+
// Draw line
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.moveTo(drawing.points[0].x, drawing.points[0].y);
|
|
96
|
+
ctx.lineTo(drawing.points[1].x, drawing.points[1].y);
|
|
97
|
+
ctx.stroke();
|
|
98
|
+
|
|
99
|
+
// Draw arrowhead
|
|
100
|
+
const angle = Math.atan2(
|
|
101
|
+
drawing.points[1].y - drawing.points[0].y,
|
|
102
|
+
drawing.points[1].x - drawing.points[0].x
|
|
103
|
+
);
|
|
104
|
+
const arrowLength = 10;
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.moveTo(drawing.points[1].x, drawing.points[1].y);
|
|
107
|
+
ctx.lineTo(
|
|
108
|
+
drawing.points[1].x - arrowLength * Math.cos(angle - Math.PI / 6),
|
|
109
|
+
drawing.points[1].y - arrowLength * Math.sin(angle - Math.PI / 6)
|
|
110
|
+
);
|
|
111
|
+
ctx.moveTo(drawing.points[1].x, drawing.points[1].y);
|
|
112
|
+
ctx.lineTo(
|
|
113
|
+
drawing.points[1].x - arrowLength * Math.cos(angle + Math.PI / 6),
|
|
114
|
+
drawing.points[1].y - arrowLength * Math.sin(angle + Math.PI / 6)
|
|
115
|
+
);
|
|
116
|
+
ctx.stroke();
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'freehand':
|
|
121
|
+
if (drawing.points.length >= 2) {
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.moveTo(drawing.points[0].x, drawing.points[0].y);
|
|
124
|
+
for (let i = 1; i < drawing.points.length; i++) {
|
|
125
|
+
ctx.lineTo(drawing.points[i].x, drawing.points[i].y);
|
|
126
|
+
}
|
|
127
|
+
ctx.stroke();
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
ctx.restore();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Filter Effects
|
|
3
|
+
*
|
|
4
|
+
* Advanced filter effects and color processing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class FilterEffects {
|
|
8
|
+
static applyVintage(imageData: ImageData): ImageData {
|
|
9
|
+
let data = FilterEffects.applySepia(imageData, 0.8);
|
|
10
|
+
const { width, height } = imageData;
|
|
11
|
+
const centerX = width / 2;
|
|
12
|
+
const centerY = height / 2;
|
|
13
|
+
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
|
|
14
|
+
|
|
15
|
+
for (let y = 0; y < height; y++) {
|
|
16
|
+
for (let x = 0; x < width; x++) {
|
|
17
|
+
const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
|
|
18
|
+
const vignette = 1 - (distance / maxDistance) * 0.7;
|
|
19
|
+
const i = (y * width + x) * 4;
|
|
20
|
+
|
|
21
|
+
data.data[i] *= vignette;
|
|
22
|
+
data.data[i + 1] *= vignette;
|
|
23
|
+
data.data[i + 2] *= vignette;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static applySepia(imageData: ImageData, intensity: number = 1): ImageData {
|
|
31
|
+
const data = new Uint8ClampedArray(imageData.data);
|
|
32
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
33
|
+
const r = data[i];
|
|
34
|
+
const g = data[i + 1];
|
|
35
|
+
const b = data[i + 2];
|
|
36
|
+
|
|
37
|
+
data[i] = Math.min(255, (r * (1 - (0.607 * intensity))) + (g * (0.769 * intensity)) + (b * (0.189 * intensity)));
|
|
38
|
+
data[i + 1] = Math.min(255, (r * (0.349 * intensity)) + (g * (1 - (0.314 * intensity))) + (b * (0.168 * intensity)));
|
|
39
|
+
data[i + 2] = Math.min(255, (r * (0.272 * intensity)) + (g * (0.534 * intensity)) + (b * (1 - (0.869 * intensity))));
|
|
40
|
+
}
|
|
41
|
+
return FilterEffects.createCanvasImageData(imageData.width, imageData.height, data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static createCanvasImageData(
|
|
45
|
+
width: number,
|
|
46
|
+
height: number,
|
|
47
|
+
data: Uint8ClampedArray
|
|
48
|
+
): ImageData {
|
|
49
|
+
return { data, width, height } as ImageData;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Error Handler
|
|
3
|
+
*/
|
|
4
|
+
export class ImageError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
message: string,
|
|
7
|
+
public readonly code: string,
|
|
8
|
+
public readonly operation?: string
|
|
9
|
+
) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'ImageError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const IMAGE_ERROR_CODES = {
|
|
16
|
+
INVALID_URI: 'INVALID_URI',
|
|
17
|
+
INVALID_DIMENSIONS: 'INVALID_DIMENSIONS',
|
|
18
|
+
INVALID_QUALITY: 'INVALID_QUALITY',
|
|
19
|
+
MANIPULATION_FAILED: 'MANIPULATION_FAILED',
|
|
20
|
+
CONVERSION_FAILED: 'CONVERSION_FAILED',
|
|
21
|
+
STORAGE_FAILED: 'STORAGE_FAILED',
|
|
22
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export type ImageErrorCode = typeof IMAGE_ERROR_CODES[keyof typeof IMAGE_ERROR_CODES];
|
|
26
|
+
|
|
27
|
+
export class ImageErrorHandler {
|
|
28
|
+
static createError(message: string, code: ImageErrorCode, operation?: string): ImageError {
|
|
29
|
+
return new ImageError(message, code, operation);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static handleUnknownError(error: unknown, operation?: string): ImageError {
|
|
33
|
+
if (error instanceof ImageError) {
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
38
|
+
return new ImageError(message, IMAGE_ERROR_CODES.MANIPULATION_FAILED, operation);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Quality Presets
|
|
3
|
+
*
|
|
4
|
+
* Predefined quality settings for different use cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SaveFormat } from '../../domain/entities/ImageTypes';
|
|
8
|
+
import { IMAGE_CONSTANTS } from '../../domain/entities/ImageConstants';
|
|
9
|
+
|
|
10
|
+
export interface QualityPreset {
|
|
11
|
+
format: SaveFormat;
|
|
12
|
+
quality: number;
|
|
13
|
+
maxWidth?: number;
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface QualityPresets {
|
|
19
|
+
web: QualityPreset;
|
|
20
|
+
mobile: QualityPreset;
|
|
21
|
+
print: QualityPreset;
|
|
22
|
+
thumbnail: QualityPreset;
|
|
23
|
+
preview: QualityPreset;
|
|
24
|
+
archive: QualityPreset;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const IMAGE_QUALITY_PRESETS: QualityPresets = {
|
|
28
|
+
web: {
|
|
29
|
+
format: 'jpeg',
|
|
30
|
+
quality: 0.8,
|
|
31
|
+
maxWidth: 1920,
|
|
32
|
+
maxHeight: 1080,
|
|
33
|
+
description: 'Optimized for web use with good balance of quality and size',
|
|
34
|
+
},
|
|
35
|
+
mobile: {
|
|
36
|
+
format: 'jpeg',
|
|
37
|
+
quality: 0.7,
|
|
38
|
+
maxWidth: 1080,
|
|
39
|
+
maxHeight: 1920,
|
|
40
|
+
description: 'Optimized for mobile devices with smaller file size',
|
|
41
|
+
},
|
|
42
|
+
print: {
|
|
43
|
+
format: 'png',
|
|
44
|
+
quality: 1.0,
|
|
45
|
+
maxWidth: 3000,
|
|
46
|
+
maxHeight: 3000,
|
|
47
|
+
description: 'High quality for printing with maximum detail',
|
|
48
|
+
},
|
|
49
|
+
thumbnail: {
|
|
50
|
+
format: 'jpeg',
|
|
51
|
+
quality: 0.6,
|
|
52
|
+
maxWidth: IMAGE_CONSTANTS.thumbnailSize,
|
|
53
|
+
maxHeight: IMAGE_CONSTANTS.thumbnailSize,
|
|
54
|
+
description: 'Small thumbnail for preview use',
|
|
55
|
+
},
|
|
56
|
+
preview: {
|
|
57
|
+
format: 'jpeg',
|
|
58
|
+
quality: 0.5,
|
|
59
|
+
maxWidth: 800,
|
|
60
|
+
maxHeight: 600,
|
|
61
|
+
description: 'Quick preview with very small file size',
|
|
62
|
+
},
|
|
63
|
+
archive: {
|
|
64
|
+
format: 'png',
|
|
65
|
+
quality: 0.9,
|
|
66
|
+
description: 'High quality archival storage with lossless compression',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export class ImageQualityPresetService {
|
|
71
|
+
static getPreset(name: keyof QualityPresets): QualityPreset {
|
|
72
|
+
return IMAGE_QUALITY_PRESETS[name];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static getAllPresets(): QualityPresets {
|
|
76
|
+
return IMAGE_QUALITY_PRESETS;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static getCustomPreset(options: {
|
|
80
|
+
format?: SaveFormat;
|
|
81
|
+
quality?: number;
|
|
82
|
+
maxWidth?: number;
|
|
83
|
+
maxHeight?: number;
|
|
84
|
+
}): QualityPreset {
|
|
85
|
+
return {
|
|
86
|
+
format: options.format || 'jpeg',
|
|
87
|
+
quality: options.quality || IMAGE_CONSTANTS.defaultQuality,
|
|
88
|
+
maxWidth: options.maxWidth,
|
|
89
|
+
maxHeight: options.maxHeight,
|
|
90
|
+
description: 'Custom quality preset',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static optimizeForUseCase(
|
|
95
|
+
useCase: 'web' | 'mobile' | 'print' | 'thumbnail' | 'preview' | 'archive',
|
|
96
|
+
customOptions?: Partial<QualityPreset>
|
|
97
|
+
): QualityPreset {
|
|
98
|
+
const preset = IMAGE_QUALITY_PRESETS[useCase];
|
|
99
|
+
|
|
100
|
+
if (!customOptions) {
|
|
101
|
+
return preset;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...preset,
|
|
106
|
+
...customOptions,
|
|
107
|
+
description: `${preset.description} (modified)`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Infrastructure - Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
import { ImageDimensions } from '../../domain/entities/ImageTypes';
|
|
5
|
+
|
|
6
|
+
export interface ValidationResult {
|
|
7
|
+
isValid: boolean;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ImageValidator {
|
|
12
|
+
static validateUri(uri: string): ValidationResult {
|
|
13
|
+
if (!uri || typeof uri !== 'string') {
|
|
14
|
+
return { isValid: false, error: 'URI is required and must be a string' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!uri.startsWith('file://') &&
|
|
18
|
+
!uri.startsWith('content://') &&
|
|
19
|
+
!uri.startsWith('http://') &&
|
|
20
|
+
!uri.startsWith('https://') &&
|
|
21
|
+
!uri.startsWith('data:image/')) {
|
|
22
|
+
return { isValid: false, error: 'Invalid URI format' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { isValid: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static validateDimensions(dimensions: Partial<ImageDimensions>): ValidationResult {
|
|
29
|
+
if (dimensions.width !== undefined) {
|
|
30
|
+
if (typeof dimensions.width !== 'number' || dimensions.width <= 0) {
|
|
31
|
+
return { isValid: false, error: 'Width must be a positive number' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (dimensions.height !== undefined) {
|
|
36
|
+
if (typeof dimensions.height !== 'number' || dimensions.height <= 0) {
|
|
37
|
+
return { isValid: false, error: 'Height must be a positive number' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { isValid: true };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static validateQuality(quality: number): ValidationResult {
|
|
45
|
+
if (typeof quality !== 'number' || quality < 0 || quality > 1) {
|
|
46
|
+
return { isValid: false, error: 'Quality must be a number between 0 and 1' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { isValid: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static validateRotation(degrees: number): ValidationResult {
|
|
53
|
+
if (typeof degrees !== 'number') {
|
|
54
|
+
return { isValid: false, error: 'Degrees must be a number' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { isValid: true };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Header for ImageGallery with optional edit button and close button
|
|
2
|
+
* Presentation - Gallery Header Component
|
|
4
3
|
*
|
|
5
|
-
* This component should be implemented by
|
|
6
|
-
* using their
|
|
4
|
+
* NOTE: This component should be implemented by consumer app
|
|
5
|
+
* using their design system and safe area handling
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import React from 'react';
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Image Gallery Component
|
|
2
|
+
* Presentation - Image Gallery Component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* theme integration, standard configuration, and optional editing.
|
|
4
|
+
* Wrapper around react-native-image-viewing with theme integration
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import React, { useCallback } from 'react';
|
|
9
8
|
import ImageViewing from 'react-native-image-viewing';
|
|
10
|
-
import
|
|
11
|
-
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
9
|
+
// import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
12
10
|
import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
|
|
13
11
|
import { GalleryHeader } from './GalleryHeader';
|
|
14
12
|
|
|
@@ -33,14 +31,14 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
|
33
31
|
onImageChange,
|
|
34
32
|
enableEditing = false,
|
|
35
33
|
}) => {
|
|
36
|
-
const tokens = useAppDesignTokens();
|
|
34
|
+
// const tokens = useAppDesignTokens();
|
|
37
35
|
const [currentIndex, setCurrentIndex] = React.useState(index);
|
|
38
36
|
|
|
39
37
|
React.useEffect(() => {
|
|
40
38
|
setCurrentIndex(index);
|
|
41
39
|
}, [index]);
|
|
42
40
|
|
|
43
|
-
const bg = backgroundColor ||
|
|
41
|
+
const bg = backgroundColor || '#000000';
|
|
44
42
|
|
|
45
43
|
const viewerImages = React.useMemo(
|
|
46
44
|
() => images.map((img) => ({ uri: img.uri })),
|
|
@@ -52,20 +50,9 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
|
52
50
|
if (!currentImage || !onImageChange) return;
|
|
53
51
|
|
|
54
52
|
try {
|
|
55
|
-
|
|
56
|
-
currentImage.uri,
|
|
57
|
-
[],
|
|
58
|
-
{
|
|
59
|
-
compress: 1,
|
|
60
|
-
format: ImageManipulator.SaveFormat.JPEG,
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
if (result.uri) {
|
|
65
|
-
await onImageChange(result.uri, currentIndex);
|
|
66
|
-
}
|
|
53
|
+
await onImageChange(currentImage.uri, currentIndex);
|
|
67
54
|
} catch (error) {
|
|
68
|
-
//
|
|
55
|
+
// Consumer should handle editing logic
|
|
69
56
|
}
|
|
70
57
|
}, [images, currentIndex, onImageChange]);
|
|
71
58
|
|
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Presentation - Image Hook
|
|
3
3
|
*
|
|
4
|
-
* Aggregator hook
|
|
5
|
-
* Kept simple and modular.
|
|
4
|
+
* Aggregator hook combining transformation and conversion capabilities
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import { useImageTransform } from './useImageTransform';
|
|
9
8
|
import { useImageConversion } from './useImageConversion';
|
|
9
|
+
import { useImageFilter } from './useImageFilter';
|
|
10
|
+
import { useImageBatch } from './useImageBatch';
|
|
11
|
+
import { useImageAIEnhancement } from './useImageAIEnhancement';
|
|
12
|
+
import { useImageAnnotation } from './useImageAnnotation';
|
|
13
|
+
import { useImageMetadata } from './useImageMetadata';
|
|
10
14
|
|
|
11
15
|
export const useImage = () => {
|
|
12
16
|
const transform = useImageTransform();
|
|
13
17
|
const conversion = useImageConversion();
|
|
18
|
+
const filter = useImageFilter();
|
|
19
|
+
const batch = useImageBatch();
|
|
20
|
+
const aiEnhancement = useImageAIEnhancement();
|
|
21
|
+
const annotation = useImageAnnotation();
|
|
22
|
+
const metadata = useImageMetadata();
|
|
14
23
|
|
|
15
24
|
return {
|
|
25
|
+
// Basic operations
|
|
16
26
|
...transform,
|
|
17
27
|
...conversion,
|
|
28
|
+
// Advanced operations
|
|
29
|
+
...filter,
|
|
30
|
+
...batch,
|
|
31
|
+
...aiEnhancement,
|
|
32
|
+
...annotation,
|
|
33
|
+
...metadata,
|
|
18
34
|
// Combined state
|
|
19
|
-
isProcessing:
|
|
20
|
-
|
|
35
|
+
isProcessing:
|
|
36
|
+
transform.isTransforming ||
|
|
37
|
+
conversion.isConverting ||
|
|
38
|
+
filter.isFiltering ||
|
|
39
|
+
batch.isBatchProcessing ||
|
|
40
|
+
aiEnhancement.isEnhancing ||
|
|
41
|
+
annotation.isAnnotating ||
|
|
42
|
+
metadata.isExtracting,
|
|
43
|
+
error:
|
|
44
|
+
transform.transformError ||
|
|
45
|
+
conversion.conversionError ||
|
|
46
|
+
filter.filterError ||
|
|
47
|
+
batch.batchError ||
|
|
48
|
+
aiEnhancement.enhancementError ||
|
|
49
|
+
annotation.annotationError ||
|
|
50
|
+
metadata.metadataError,
|
|
21
51
|
};
|
|
22
52
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Image AI Enhancement Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { useImageOperation } from './useImageOperation';
|
|
7
|
+
import { ImageAIEnhancementService, type AutoEnhancementOptions, type EnhancementResult } from '../../infrastructure/services/ImageAIEnhancementService';
|
|
8
|
+
import { ImageSpecializedEnhancementService } from '../../infrastructure/services/ImageSpecializedEnhancementService';
|
|
9
|
+
|
|
10
|
+
export const useImageAIEnhancement = () => {
|
|
11
|
+
const { isProcessing, error, execute } = useImageOperation();
|
|
12
|
+
|
|
13
|
+
const autoEnhance = useCallback((uri: string, options?: AutoEnhancementOptions) =>
|
|
14
|
+
execute(() => ImageAIEnhancementService.autoEnhance(uri, options), 'Failed to auto enhance'), [execute]);
|
|
15
|
+
|
|
16
|
+
const enhancePortrait = useCallback((uri: string) =>
|
|
17
|
+
execute(() => ImageSpecializedEnhancementService.enhancePortrait(uri), 'Failed to enhance portrait'), [execute]);
|
|
18
|
+
|
|
19
|
+
const enhanceLandscape = useCallback((uri: string) =>
|
|
20
|
+
execute(() => ImageSpecializedEnhancementService.enhanceLandscape(uri), 'Failed to enhance landscape'), [execute]);
|
|
21
|
+
|
|
22
|
+
const analyzeImage = useCallback((uri: string) =>
|
|
23
|
+
execute(() => ImageAIEnhancementService.analyzeImage(uri), 'Failed to analyze image'), [execute]);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
autoEnhance,
|
|
27
|
+
enhancePortrait,
|
|
28
|
+
enhanceLandscape,
|
|
29
|
+
analyzeImage,
|
|
30
|
+
isEnhancing: isProcessing,
|
|
31
|
+
enhancementError: error,
|
|
32
|
+
};
|
|
33
|
+
};
|