@umituz/react-native-image 1.2.5 → 1.3.2

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.
@@ -1,273 +0,0 @@
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
- }
@@ -1,183 +0,0 @@
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
- });
@@ -1,261 +0,0 @@
1
- /**
2
- * Presentation - Editor Component
3
- *
4
- * Main image editor interface
5
- */
6
-
7
- import React, { useState, useCallback } from 'react';
8
- import { View, StyleSheet, Alert, Text, TouchableOpacity } from 'react-native';
9
- import { EditorCanvas } from './EditorCanvas';
10
- import { EditorToolbar } from './EditorToolbar';
11
- import { EditorPanel } from './EditorPanel';
12
- import { useImageEditor } from '../hooks/useImageEditor';
13
- import { EditorTool, EditorExportOptions } from '../../domain/entities/EditorTypes';
14
- import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
15
-
16
- interface EditorProps {
17
- imageUri: string;
18
- width?: number;
19
- height?: number;
20
- onSave?: (result: ImageManipulationResult) => void | Promise<void>;
21
- onCancel?: () => void;
22
- backgroundColor?: string;
23
- toolbarBackgroundColor?: string;
24
- panelBackgroundColor?: string;
25
- exportOptions?: EditorExportOptions;
26
- }
27
-
28
- export function Editor({
29
- imageUri,
30
- width = 800,
31
- height = 600,
32
- onSave,
33
- onCancel,
34
- backgroundColor = '#ffffff',
35
- toolbarBackgroundColor = '#f8f9fa',
36
- panelBackgroundColor = '#f8f9fa',
37
- exportOptions,
38
- }: EditorProps) {
39
- const [selectedTool, setSelectedTool] = useState<EditorTool>(EditorTool.MOVE);
40
- const [toolConfig, setToolConfig] = useState<any>({});
41
- const [canvas, setCanvas] = useState<any>(null);
42
-
43
- const {
44
- editorState,
45
- isProcessing,
46
- error,
47
- canUndo,
48
- canRedo,
49
- initializeEditor,
50
- setTool,
51
- addLayer,
52
- removeLayer,
53
- undo,
54
- redo,
55
- exportImage,
56
- cancel,
57
- } = useImageEditor({
58
- onSave,
59
- onCancel,
60
- });
61
-
62
- const handleCanvasReady = useCallback((canvasElement: any) => {
63
- setCanvas(canvasElement);
64
- if (imageUri && !editorState) {
65
- initializeEditor(imageUri);
66
- }
67
- }, [imageUri, editorState, initializeEditor]);
68
-
69
- const handleToolSelect = useCallback((tool: EditorTool) => {
70
- setSelectedTool(tool);
71
- setTool(tool);
72
- setToolConfig({});
73
- }, [setTool]);
74
-
75
- const handleToolConfigChange = useCallback((config: any) => {
76
- setToolConfig(config);
77
- }, []);
78
-
79
- const handleSave = useCallback(async () => {
80
- if (isProcessing) return;
81
-
82
- try {
83
- await exportImage(exportOptions);
84
- } catch (err) {
85
- Alert.alert('Error', 'Failed to save image');
86
- }
87
- }, [isProcessing, exportImage, exportImage]);
88
-
89
- const handleCancel = useCallback(() => {
90
- cancel();
91
- }, [cancel]);
92
-
93
- const handleUndo = useCallback(() => {
94
- undo();
95
- }, [undo]);
96
-
97
- const handleRedo = useCallback(() => {
98
- redo();
99
- }, [redo]);
100
-
101
- return (
102
- <View style={[styles.container, { backgroundColor }]}>
103
- {/* Toolbar */}
104
- <EditorToolbar
105
- selectedTool={selectedTool}
106
- onToolSelect={handleToolSelect}
107
- backgroundColor={toolbarBackgroundColor}
108
- />
109
-
110
- {/* Main Editor Area */}
111
- <View style={styles.editorArea}>
112
- <EditorCanvas
113
- width={width}
114
- height={height}
115
- onCanvasReady={handleCanvasReady}
116
- onToolChange={handleToolSelect}
117
- onStateChange={handleToolConfigChange}
118
- backgroundColor={backgroundColor}
119
- />
120
-
121
- {/* Error Display */}
122
- {error && (
123
- <View style={styles.errorContainer}>
124
- <Text style={styles.errorText}>{error}</Text>
125
- </View>
126
- )}
127
- </View>
128
-
129
- {/* Configuration Panel */}
130
- {(selectedTool === EditorTool.BRUSH ||
131
- selectedTool === EditorTool.ERASER ||
132
- selectedTool === EditorTool.TEXT ||
133
- selectedTool === EditorTool.CROP) && (
134
- <EditorPanel
135
- selectedTool={selectedTool}
136
- onToolConfigChange={handleToolConfigChange}
137
- backgroundColor={panelBackgroundColor}
138
- />
139
- )}
140
-
141
- {/* Action Buttons */}
142
- <View style={styles.actionBar}>
143
- <View style={styles.undoRedoContainer}>
144
- <TouchableOpacity
145
- style={[styles.actionButton, !canUndo && styles.disabledButton]}
146
- onPress={handleUndo}
147
- disabled={!canUndo || isProcessing}
148
- >
149
- <Text style={styles.actionButtonText}>↶</Text>
150
- </TouchableOpacity>
151
-
152
- <TouchableOpacity
153
- style={[styles.actionButton, !canRedo && styles.disabledButton]}
154
- onPress={handleRedo}
155
- disabled={!canRedo || isProcessing}
156
- >
157
- <Text style={styles.actionButtonText}>↷</Text>
158
- </TouchableOpacity>
159
- </View>
160
-
161
- <View style={styles.saveCancelContainer}>
162
- <TouchableOpacity
163
- style={[styles.actionButton, styles.cancelButton]}
164
- onPress={handleCancel}
165
- disabled={isProcessing}
166
- >
167
- <Text style={styles.cancelButtonText}>Cancel</Text>
168
- </TouchableOpacity>
169
-
170
- <TouchableOpacity
171
- style={[styles.actionButton, styles.saveButton, isProcessing && styles.processingButton]}
172
- onPress={handleSave}
173
- disabled={isProcessing}
174
- >
175
- <Text style={styles.saveButtonText}>
176
- {isProcessing ? 'Saving...' : 'Save'}
177
- </Text>
178
- </TouchableOpacity>
179
- </View>
180
- </View>
181
- </View>
182
- );
183
- }
184
-
185
- const styles = StyleSheet.create({
186
- container: {
187
- flex: 1,
188
- backgroundColor: '#ffffff',
189
- },
190
- editorArea: {
191
- flex: 1,
192
- position: 'relative',
193
- },
194
- errorContainer: {
195
- position: 'absolute',
196
- top: 16,
197
- left: 16,
198
- right: 16,
199
- backgroundColor: '#ff4444',
200
- padding: 12,
201
- borderRadius: 8,
202
- },
203
- errorText: {
204
- color: '#ffffff',
205
- fontSize: 14,
206
- fontWeight: '500',
207
- },
208
- actionBar: {
209
- flexDirection: 'row',
210
- justifyContent: 'space-between',
211
- alignItems: 'center',
212
- padding: 16,
213
- borderTopWidth: 1,
214
- borderTopColor: '#e0e0e0',
215
- backgroundColor: '#f8f9fa',
216
- },
217
- undoRedoContainer: {
218
- flexDirection: 'row',
219
- gap: 8,
220
- },
221
- saveCancelContainer: {
222
- flexDirection: 'row',
223
- gap: 8,
224
- },
225
- actionButton: {
226
- paddingHorizontal: 16,
227
- paddingVertical: 8,
228
- borderRadius: 6,
229
- borderWidth: 1,
230
- borderColor: '#d0d0d0',
231
- backgroundColor: '#ffffff',
232
- },
233
- disabledButton: {
234
- opacity: 0.5,
235
- },
236
- cancelButton: {
237
- borderColor: '#6c757d',
238
- },
239
- saveButton: {
240
- backgroundColor: '#28a745',
241
- borderColor: '#28a745',
242
- },
243
- processingButton: {
244
- opacity: 0.7,
245
- },
246
- actionButtonText: {
247
- fontSize: 16,
248
- fontWeight: '600',
249
- color: '#333333',
250
- },
251
- cancelButtonText: {
252
- fontSize: 16,
253
- fontWeight: '600',
254
- color: '#6c757d',
255
- },
256
- saveButtonText: {
257
- fontSize: 16,
258
- fontWeight: '600',
259
- color: '#ffffff',
260
- },
261
- });