@umituz/react-native-image 1.1.5 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +25 -10
  2. package/src/domain/entities/EditorTypes.ts +180 -0
  3. package/src/domain/entities/ImageFilterTypes.ts +70 -0
  4. package/src/index.ts +54 -0
  5. package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
  6. package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
  7. package/src/infrastructure/services/ImageBatchService.ts +199 -0
  8. package/src/infrastructure/services/ImageEditorService.ts +274 -0
  9. package/src/infrastructure/services/ImageFilterService.ts +168 -0
  10. package/src/infrastructure/services/ImageMetadataService.ts +187 -0
  11. package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +57 -0
  12. package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
  13. package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
  14. package/src/infrastructure/utils/CropTool.ts +260 -0
  15. package/src/infrastructure/utils/DrawingEngine.ts +210 -0
  16. package/src/infrastructure/utils/FilterEffects.ts +51 -0
  17. package/src/infrastructure/utils/FilterProcessor.ts +361 -0
  18. package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
  19. package/src/infrastructure/utils/LayerManager.ts +158 -0
  20. package/src/infrastructure/utils/ShapeRenderer.ts +168 -0
  21. package/src/infrastructure/utils/TextEditor.ts +273 -0
  22. package/src/presentation/components/CropComponent.tsx +183 -0
  23. package/src/presentation/components/Editor.tsx +261 -0
  24. package/src/presentation/components/EditorCanvas.tsx +135 -0
  25. package/src/presentation/components/EditorPanel.tsx +321 -0
  26. package/src/presentation/components/EditorToolbar.tsx +180 -0
  27. package/src/presentation/components/FilterSlider.tsx +123 -0
  28. package/src/presentation/components/GalleryHeader.tsx +87 -25
  29. package/src/presentation/components/ImageGallery.tsx +97 -48
  30. package/src/presentation/hooks/useEditorTools.ts +188 -0
  31. package/src/presentation/hooks/useImage.ts +33 -2
  32. package/src/presentation/hooks/useImageAIEnhancement.ts +33 -0
  33. package/src/presentation/hooks/useImageAnnotation.ts +32 -0
  34. package/src/presentation/hooks/useImageBatch.ts +33 -0
  35. package/src/presentation/hooks/useImageEditor.ts +165 -38
  36. package/src/presentation/hooks/useImageFilter.ts +38 -0
  37. package/src/presentation/hooks/useImageMetadata.ts +28 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Infrastructure - Shape Renderer
3
+ *
4
+ * Advanced shape drawing with different styles
5
+ */
6
+
7
+ export interface ShapeStyle {
8
+ fill?: string;
9
+ stroke?: string;
10
+ strokeWidth?: number;
11
+ dash?: number[];
12
+ opacity?: number;
13
+ }
14
+
15
+ export class ShapeRenderer {
16
+ static drawRoundedRect(
17
+ ctx: CanvasRenderingContext2D,
18
+ x: number,
19
+ y: number,
20
+ width: number,
21
+ height: number,
22
+ radius: number,
23
+ style: ShapeStyle
24
+ ): void {
25
+ ctx.save();
26
+
27
+ ctx.globalAlpha = style.opacity || 1;
28
+ ctx.strokeStyle = style.stroke || '#000000';
29
+ ctx.fillStyle = style.fill || 'transparent';
30
+ ctx.lineWidth = style.strokeWidth || 2;
31
+
32
+ if (style.dash) {
33
+ ctx.setLineDash(style.dash);
34
+ }
35
+
36
+ ctx.beginPath();
37
+ ctx.moveTo(x + radius, y);
38
+ ctx.lineTo(x + width - radius, y);
39
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
40
+ ctx.lineTo(x + width, y + height - radius);
41
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
42
+ ctx.lineTo(x + radius, y + height);
43
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
44
+ ctx.lineTo(x, y + radius);
45
+ ctx.quadraticCurveTo(x, y, x + radius, y);
46
+ ctx.closePath();
47
+
48
+ if (style.fill) ctx.fill();
49
+ if (style.stroke) ctx.stroke();
50
+
51
+ ctx.restore();
52
+ }
53
+
54
+ static drawStar(
55
+ ctx: CanvasRenderingContext2D,
56
+ cx: number,
57
+ cy: number,
58
+ outerRadius: number,
59
+ innerRadius: number,
60
+ points: number,
61
+ style: ShapeStyle
62
+ ): void {
63
+ ctx.save();
64
+
65
+ ctx.globalAlpha = style.opacity || 1;
66
+ ctx.strokeStyle = style.stroke || '#000000';
67
+ ctx.fillStyle = style.fill || 'transparent';
68
+ ctx.lineWidth = style.strokeWidth || 2;
69
+
70
+ ctx.beginPath();
71
+ for (let i = 0; i < points * 2; i++) {
72
+ const angle = (Math.PI * i) / points - Math.PI / 2;
73
+ const radius = i % 2 === 0 ? outerRadius : innerRadius;
74
+ const x = cx + Math.cos(angle) * radius;
75
+ const y = cy + Math.sin(angle) * radius;
76
+
77
+ if (i === 0) {
78
+ ctx.moveTo(x, y);
79
+ } else {
80
+ ctx.lineTo(x, y);
81
+ }
82
+ }
83
+ ctx.closePath();
84
+
85
+ if (style.fill) ctx.fill();
86
+ if (style.stroke) ctx.stroke();
87
+
88
+ ctx.restore();
89
+ }
90
+
91
+ static drawHeart(
92
+ ctx: CanvasRenderingContext2D,
93
+ x: number,
94
+ y: number,
95
+ size: number,
96
+ style: ShapeStyle
97
+ ): void {
98
+ ctx.save();
99
+
100
+ ctx.globalAlpha = style.opacity || 1;
101
+ ctx.strokeStyle = style.stroke || '#000000';
102
+ ctx.fillStyle = style.fill || 'transparent';
103
+ ctx.lineWidth = style.strokeWidth || 2;
104
+
105
+ ctx.beginPath();
106
+ const topCurveHeight = size * 0.3;
107
+ ctx.moveTo(x, y + topCurveHeight);
108
+
109
+ // Top left curve
110
+ ctx.bezierCurveTo(
111
+ x, y,
112
+ x - size / 2, y,
113
+ x - size / 2, y + topCurveHeight
114
+ );
115
+
116
+ // Bottom left curve
117
+ ctx.bezierCurveTo(
118
+ x - size / 2, y + (size + topCurveHeight) / 2,
119
+ x, y + (size + topCurveHeight) / 1.5,
120
+ x, y + size
121
+ );
122
+
123
+ // Bottom right curve
124
+ ctx.bezierCurveTo(
125
+ x, y + (size + topCurveHeight) / 1.5,
126
+ x + size / 2, y + (size + topCurveHeight) / 2,
127
+ x + size / 2, y + topCurveHeight
128
+ );
129
+
130
+ // Top right curve
131
+ ctx.bezierCurveTo(
132
+ x + size / 2, y,
133
+ x, y,
134
+ x, y + topCurveHeight
135
+ );
136
+
137
+ ctx.closePath();
138
+
139
+ if (style.fill) ctx.fill();
140
+ if (style.stroke) ctx.stroke();
141
+
142
+ ctx.restore();
143
+ }
144
+
145
+ static drawTriangle(
146
+ ctx: CanvasRenderingContext2D,
147
+ points: Array<{ x: number; y: number }>,
148
+ style: ShapeStyle
149
+ ): void {
150
+ ctx.save();
151
+
152
+ ctx.globalAlpha = style.opacity || 1;
153
+ ctx.strokeStyle = style.stroke || '#000000';
154
+ ctx.fillStyle = style.fill || 'transparent';
155
+ ctx.lineWidth = style.strokeWidth || 2;
156
+
157
+ ctx.beginPath();
158
+ ctx.moveTo(points[0].x, points[0].y);
159
+ ctx.lineTo(points[1].x, points[1].y);
160
+ ctx.lineTo(points[2].x, points[2].y);
161
+ ctx.closePath();
162
+
163
+ if (style.fill) ctx.fill();
164
+ if (style.stroke) ctx.stroke();
165
+
166
+ ctx.restore();
167
+ }
168
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Infrastructure - Text Editor
3
+ *
4
+ * Advanced text editing with rich formatting
5
+ */
6
+
7
+ export interface TextFormatting {
8
+ fontSize: number;
9
+ fontFamily: string;
10
+ fontWeight: 'normal' | 'bold';
11
+ fontStyle: 'normal' | 'italic';
12
+ textAlign: 'left' | 'center' | 'right';
13
+ color: string;
14
+ backgroundColor?: string;
15
+ opacity: number;
16
+ lineHeight?: number;
17
+ letterSpacing?: number;
18
+ textShadow?: {
19
+ color: string;
20
+ blur: number;
21
+ offsetX: number;
22
+ offsetY: number;
23
+ };
24
+ textStroke?: {
25
+ color: string;
26
+ width: number;
27
+ };
28
+ }
29
+
30
+ export interface TextMeasurement {
31
+ width: number;
32
+ height: number;
33
+ baseline: number;
34
+ }
35
+
36
+ export class TextEditor {
37
+ private static systemFonts = [
38
+ 'Arial',
39
+ 'Helvetica',
40
+ 'Times New Roman',
41
+ 'Georgia',
42
+ 'Courier New',
43
+ 'Verdana',
44
+ 'Comic Sans MS',
45
+ 'Impact',
46
+ 'Palatino',
47
+ 'Garamond',
48
+ 'Bookman',
49
+ 'Trebuchet MS',
50
+ ];
51
+
52
+ private static getFontFamily(fontFamily: string): string {
53
+ // Check if font is available, fallback to system font
54
+ return this.systemFonts.includes(fontFamily) ? fontFamily : 'Arial';
55
+ }
56
+
57
+ static measureText(
58
+ text: string,
59
+ formatting: TextFormatting,
60
+ ctx: CanvasRenderingContext2D
61
+ ): TextMeasurement {
62
+ const {
63
+ fontSize,
64
+ fontFamily,
65
+ fontWeight,
66
+ fontStyle,
67
+ lineHeight = 1.2,
68
+ } = formatting;
69
+
70
+ // Build font string
71
+ const fontString = `${fontStyle} ${fontWeight} ${fontSize}px ${this.getFontFamily(fontFamily)}`;
72
+ ctx.font = fontString;
73
+
74
+ // Measure text
75
+ const metrics = ctx.measureText(text);
76
+ const width = metrics.width;
77
+ const height = fontSize * lineHeight;
78
+ const baseline = fontSize * 0.8;
79
+
80
+ return { width, height, baseline };
81
+ }
82
+
83
+ static renderText(
84
+ ctx: CanvasRenderingContext2D,
85
+ text: string,
86
+ position: { x: number; y: number },
87
+ formatting: TextFormatting,
88
+ maxWidth?: number
89
+ ): TextMeasurement {
90
+ const measurement = this.measureText(text, formatting, ctx);
91
+
92
+ ctx.save();
93
+
94
+ // Set text properties
95
+ const fontString = `${formatting.fontStyle} ${formatting.fontWeight} ${formatting.fontSize}px ${this.getFontFamily(formatting.fontFamily)}`;
96
+ ctx.font = fontString;
97
+ ctx.fillStyle = formatting.color;
98
+ ctx.globalAlpha = formatting.opacity;
99
+ ctx.textAlign = formatting.textAlign;
100
+ ctx.textBaseline = 'alphabetic';
101
+
102
+ // Apply text shadow
103
+ if (formatting.textShadow) {
104
+ ctx.shadowColor = formatting.textShadow.color;
105
+ ctx.shadowBlur = formatting.textShadow.blur;
106
+ ctx.shadowOffsetX = formatting.textShadow.offsetX;
107
+ ctx.shadowOffsetY = formatting.textShadow.offsetY;
108
+ }
109
+
110
+ // Draw background if specified
111
+ if (formatting.backgroundColor) {
112
+ const padding = 4;
113
+ let x = position.x;
114
+ let y = position.y - measurement.baseline - padding;
115
+
116
+ if (formatting.textAlign === 'center') {
117
+ x -= measurement.width / 2;
118
+ } else if (formatting.textAlign === 'right') {
119
+ x -= measurement.width;
120
+ }
121
+
122
+ ctx.fillStyle = formatting.backgroundColor;
123
+ ctx.fillRect(
124
+ x - padding,
125
+ y - padding,
126
+ measurement.width + padding * 2,
127
+ measurement.height + padding * 2
128
+ );
129
+
130
+ // Reset fill style for text
131
+ ctx.fillStyle = formatting.color;
132
+ }
133
+
134
+ // Draw text
135
+ let drawX = position.x;
136
+ if (formatting.textAlign === 'center') {
137
+ drawX = position.x;
138
+ } else if (formatting.textAlign === 'right') {
139
+ drawX = position.x;
140
+ }
141
+
142
+ if (maxWidth && measurement.width > maxWidth) {
143
+ // Text wrapping
144
+ const words = text.split(' ');
145
+ let currentLine = '';
146
+ let currentY = position.y;
147
+
148
+ for (const word of words) {
149
+ const testLine = currentLine + (currentLine ? ' ' : '') + word;
150
+ const testMetrics = this.measureText(testLine, formatting, ctx);
151
+
152
+ if (testMetrics.width > maxWidth && currentLine) {
153
+ ctx.fillText(currentLine, drawX, currentY);
154
+ currentLine = word;
155
+ currentY += measurement.height;
156
+ } else {
157
+ currentLine = testLine;
158
+ }
159
+ }
160
+ ctx.fillText(currentLine, drawX, currentY);
161
+ } else {
162
+ ctx.fillText(text, drawX, position.y);
163
+ }
164
+
165
+ // Apply text stroke if specified
166
+ if (formatting.textStroke) {
167
+ ctx.strokeStyle = formatting.textStroke.color;
168
+ ctx.lineWidth = formatting.textStroke.width;
169
+ ctx.strokeText(text, drawX, position.y);
170
+ }
171
+
172
+ ctx.restore();
173
+
174
+ return measurement;
175
+ }
176
+
177
+ static wrapText(
178
+ text: string,
179
+ maxWidth: number,
180
+ formatting: TextFormatting,
181
+ ctx: CanvasRenderingContext2D
182
+ ): string[] {
183
+ const words = text.split(' ');
184
+ const lines: string[] = [];
185
+ let currentLine = '';
186
+
187
+ for (const word of words) {
188
+ const testLine = currentLine + (currentLine ? ' ' : '') + word;
189
+ const testMetrics = this.measureText(testLine, formatting, ctx);
190
+
191
+ if (testMetrics.width > maxWidth && currentLine) {
192
+ lines.push(currentLine);
193
+ currentLine = word;
194
+ } else {
195
+ currentLine = testLine;
196
+ }
197
+ }
198
+
199
+ if (currentLine) {
200
+ lines.push(currentLine);
201
+ }
202
+
203
+ return lines;
204
+ }
205
+
206
+ static getAvailableFonts(): string[] {
207
+ return [...this.systemFonts];
208
+ }
209
+
210
+ static calculateTextBounds(
211
+ text: string,
212
+ formatting: TextFormatting,
213
+ ctx: CanvasRenderingContext2D,
214
+ maxWidth?: number
215
+ ): {
216
+ x: number;
217
+ y: number;
218
+ width: number;
219
+ height: number;
220
+ } {
221
+ const measurement = this.measureText(text, formatting, ctx);
222
+
223
+ if (maxWidth && measurement.width > maxWidth) {
224
+ const lines = this.wrapText(text, maxWidth, formatting, ctx);
225
+ const lineHeight = measurement.height;
226
+ const totalHeight = lines.length * lineHeight;
227
+
228
+ return {
229
+ x: 0,
230
+ y: 0,
231
+ width: maxWidth,
232
+ height: totalHeight,
233
+ };
234
+ }
235
+
236
+ return {
237
+ x: 0,
238
+ y: 0,
239
+ width: measurement.width,
240
+ height: measurement.height,
241
+ };
242
+ }
243
+
244
+ static createTextPath(
245
+ text: string,
246
+ position: { x: number; y: number },
247
+ formatting: TextFormatting,
248
+ ctx: CanvasRenderingContext2D
249
+ ): Path2D | any {
250
+ const measurement = this.measureText(text, formatting, ctx);
251
+
252
+ ctx.save();
253
+ const fontString = `${formatting.fontStyle} ${formatting.fontWeight} ${formatting.fontSize}px ${this.getFontFamily(formatting.fontFamily)}`;
254
+ ctx.font = fontString;
255
+
256
+ let x = position.x;
257
+ if (formatting.textAlign === 'center') {
258
+ x = position.x;
259
+ } else if (formatting.textAlign === 'right') {
260
+ x = position.x;
261
+ }
262
+
263
+ const path = new Path2D();
264
+ path.moveTo(x, position.y);
265
+
266
+ // This is a simplified version - in a real implementation,
267
+ // you would use more sophisticated text-to-path conversion
268
+ path.rect(x - measurement.width/2, position.y - measurement.baseline, measurement.width, measurement.height);
269
+
270
+ ctx.restore();
271
+ return path;
272
+ }
273
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Presentation - Crop Component
3
+ *
4
+ * Advanced cropping interface with aspect ratios and grid
5
+ */
6
+
7
+ import React, { useState, useCallback } from 'react';
8
+ import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
9
+ import { CropTool, type CropConfig, type CropArea } from '../../infrastructure/utils/CropTool';
10
+
11
+ interface CropComponentProps {
12
+ imageWidth: number;
13
+ imageHeight: number;
14
+ onCropChange?: (area: CropArea) => void;
15
+ onCropComplete?: (area: CropArea) => void;
16
+ initialArea?: CropArea;
17
+ config?: CropConfig;
18
+ }
19
+
20
+ export function CropComponent({
21
+ imageWidth,
22
+ imageHeight,
23
+ onCropChange,
24
+ onCropComplete,
25
+ initialArea,
26
+ config = {},
27
+ }: CropComponentProps) {
28
+ const [cropArea, setCropArea] = useState<CropArea>(
29
+ initialArea || {
30
+ x: 0,
31
+ y: 0,
32
+ width: imageWidth,
33
+ height: imageHeight,
34
+ }
35
+ );
36
+ const [isDragging, setIsDragging] = useState(false);
37
+ const [dragHandle, setDragHandle] = useState<string | null>(null);
38
+
39
+ const presets = CropTool.getPresets();
40
+
41
+ const handleCropChange = useCallback((newArea: CropArea) => {
42
+ setCropArea(newArea);
43
+ onCropChange?.(newArea);
44
+ }, [onCropChange]);
45
+
46
+ const handlePresetSelect = useCallback((preset: any) => {
47
+ const constrainedArea = CropTool.constrainToAspectRatio(
48
+ cropArea.width,
49
+ cropArea.height,
50
+ preset.aspectRatio
51
+ );
52
+
53
+ const centeredArea = {
54
+ ...constrainedArea,
55
+ x: (imageWidth - constrainedArea.width) / 2,
56
+ y: (imageHeight - constrainedArea.height) / 2,
57
+ };
58
+
59
+ handleCropChange(centeredArea);
60
+ }, [cropArea, imageWidth, imageHeight, handleCropChange]);
61
+
62
+ const handleCenterCrop = useCallback(() => {
63
+ const centerCrop = CropTool.centerCrop(imageWidth, imageHeight);
64
+ handleCropChange(centerCrop);
65
+ }, [imageWidth, imageHeight, handleCropChange]);
66
+
67
+ const renderPresets = () => (
68
+ <View style={styles.presetsContainer}>
69
+ <Text style={styles.presetsLabel}>Aspect Ratio</Text>
70
+ <View style={styles.presetButtons}>
71
+ {presets.map((preset) => (
72
+ <TouchableOpacity
73
+ key={preset.name}
74
+ style={[
75
+ styles.presetButton,
76
+ config.aspectRatio === preset.aspectRatio && styles.selectedPreset,
77
+ ]}
78
+ onPress={() => handlePresetSelect(preset)}
79
+ >
80
+ <Text style={[
81
+ styles.presetText,
82
+ config.aspectRatio === preset.aspectRatio && styles.presetTextSelected
83
+ ]}>{preset.name}</Text>
84
+ </TouchableOpacity>
85
+ ))}
86
+ </View>
87
+ </View>
88
+ );
89
+
90
+ const renderActions = () => (
91
+ <View style={styles.actionsContainer}>
92
+ <TouchableOpacity
93
+ style={styles.actionButton}
94
+ onPress={handleCenterCrop}
95
+ >
96
+ <Text style={styles.actionButtonText}>Center Crop</Text>
97
+ </TouchableOpacity>
98
+
99
+ <TouchableOpacity
100
+ style={[styles.actionButton, styles.primaryButton]}
101
+ onPress={() => onCropComplete?.(cropArea)}
102
+ >
103
+ <Text style={styles.primaryButtonText}>Apply Crop</Text>
104
+ </TouchableOpacity>
105
+ </View>
106
+ );
107
+
108
+ return (
109
+ <View style={styles.container}>
110
+ {renderPresets()}
111
+ {renderActions()}
112
+ </View>
113
+ );
114
+ }
115
+
116
+ const styles = StyleSheet.create({
117
+ container: {
118
+ padding: 16,
119
+ gap: 16,
120
+ },
121
+ presetsContainer: {
122
+ gap: 8,
123
+ },
124
+ presetsLabel: {
125
+ fontSize: 16,
126
+ fontWeight: '600',
127
+ color: '#333333',
128
+ marginBottom: 8,
129
+ },
130
+ presetButtons: {
131
+ flexDirection: 'row',
132
+ flexWrap: 'wrap',
133
+ gap: 8,
134
+ },
135
+ presetButton: {
136
+ paddingHorizontal: 12,
137
+ paddingVertical: 8,
138
+ borderRadius: 6,
139
+ borderWidth: 1,
140
+ borderColor: '#d0d0d0',
141
+ backgroundColor: '#ffffff',
142
+ },
143
+ selectedPreset: {
144
+ backgroundColor: '#007bff',
145
+ borderColor: '#007bff',
146
+ },
147
+ presetText: {
148
+ fontSize: 14,
149
+ fontWeight: '500',
150
+ color: '#333333',
151
+ },
152
+ presetTextSelected: {
153
+ color: '#ffffff',
154
+ },
155
+ actionsContainer: {
156
+ flexDirection: 'row',
157
+ justifyContent: 'space-between',
158
+ gap: 12,
159
+ },
160
+ actionButton: {
161
+ flex: 1,
162
+ paddingVertical: 12,
163
+ borderRadius: 8,
164
+ borderWidth: 1,
165
+ borderColor: '#d0d0d0',
166
+ backgroundColor: '#ffffff',
167
+ alignItems: 'center',
168
+ },
169
+ primaryButton: {
170
+ backgroundColor: '#007bff',
171
+ borderColor: '#007bff',
172
+ },
173
+ actionButtonText: {
174
+ fontSize: 16,
175
+ fontWeight: '600',
176
+ color: '#333333',
177
+ },
178
+ primaryButtonText: {
179
+ fontSize: 16,
180
+ fontWeight: '600',
181
+ color: '#ffffff',
182
+ },
183
+ });