react-native-expo-cropper 1.2.38 → 1.2.40
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/dist/CustomCamera.js +107 -113
- package/dist/ImageCropper.js +132 -214
- package/dist/ImageCropperStyles.js +31 -5
- package/package.json +7 -6
- package/src/CustomCamera.js +130 -104
- package/src/ImageCropper.js +142 -221
- package/src/ImageCropperStyles.js +27 -4
package/src/CustomCamera.js
CHANGED
|
@@ -8,27 +8,39 @@ import {
|
|
|
8
8
|
SafeAreaView,
|
|
9
9
|
Dimensions,
|
|
10
10
|
Image,
|
|
11
|
+
Platform,
|
|
11
12
|
} from 'react-native';
|
|
12
13
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
Camera,
|
|
16
|
+
useCameraDevice,
|
|
17
|
+
useCameraPermission,
|
|
18
|
+
} from 'react-native-vision-camera';
|
|
19
|
+
|
|
14
20
|
const { width } = Dimensions.get('window');
|
|
15
21
|
|
|
16
22
|
export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
17
23
|
const [isReady, setIsReady] = useState(false);
|
|
18
24
|
const [loadingBeforeCapture, setLoadingBeforeCapture] = useState(false);
|
|
19
|
-
const [hasPermission, setHasPermission] = useState(null);
|
|
20
25
|
const cameraRef = useRef(null);
|
|
21
26
|
const cameraWrapperRef = useRef(null);
|
|
22
27
|
const insets = useSafeAreaInsets();
|
|
23
|
-
const [cameraWrapperLayout, setCameraWrapperLayout] = useState({
|
|
28
|
+
const [cameraWrapperLayout, setCameraWrapperLayout] = useState({
|
|
29
|
+
width: 0,
|
|
30
|
+
height: 0,
|
|
31
|
+
x: 0,
|
|
32
|
+
y: 0,
|
|
33
|
+
});
|
|
24
34
|
const [greenFrame, setGreenFrame] = useState(null);
|
|
25
35
|
|
|
36
|
+
const { hasPermission, requestPermission } = useCameraPermission();
|
|
37
|
+
const device = useCameraDevice('back');
|
|
38
|
+
|
|
26
39
|
useEffect(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}, []);
|
|
40
|
+
if (!hasPermission) {
|
|
41
|
+
requestPermission();
|
|
42
|
+
}
|
|
43
|
+
}, [hasPermission, requestPermission]);
|
|
32
44
|
|
|
33
45
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
34
46
|
const waitForRender = (cycles = 5) => {
|
|
@@ -46,26 +58,24 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
46
58
|
});
|
|
47
59
|
};
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
|
|
52
|
-
// The green frame should be calculated on the wrapper (as it's visually drawn there)
|
|
53
|
-
// But we store it with wrapper dimensions so ImageCropper can map it correctly
|
|
61
|
+
// Green frame coordinates relative to camera preview wrapper.
|
|
62
|
+
// Preview matches sensor output (VisionCamera) so framing matches capture.
|
|
54
63
|
const calculateGreenFrameCoordinates = () => {
|
|
55
64
|
const wrapperWidth = cameraWrapperLayout.width;
|
|
56
65
|
const wrapperHeight = cameraWrapperLayout.height;
|
|
57
|
-
|
|
66
|
+
|
|
58
67
|
if (wrapperWidth === 0 || wrapperHeight === 0) {
|
|
59
|
-
console.warn(
|
|
68
|
+
console.warn(
|
|
69
|
+
'Camera wrapper layout not ready, cannot calculate green frame'
|
|
70
|
+
);
|
|
60
71
|
return null;
|
|
61
72
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
|
|
74
|
+
const frameWidth = wrapperWidth * 0.85;
|
|
75
|
+
const frameHeight = wrapperHeight * 0.7;
|
|
76
|
+
const frameX = (wrapperWidth - frameWidth) / 2;
|
|
77
|
+
const frameY = (wrapperHeight - frameHeight) / 2;
|
|
78
|
+
|
|
69
79
|
const frameCoords = {
|
|
70
80
|
x: frameX,
|
|
71
81
|
y: frameY,
|
|
@@ -73,19 +83,16 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
73
83
|
height: frameHeight,
|
|
74
84
|
wrapperWidth,
|
|
75
85
|
wrapperHeight,
|
|
76
|
-
// ✅ Store percentages for easier mapping later
|
|
77
86
|
percentX: (frameX / wrapperWidth) * 100,
|
|
78
87
|
percentY: (frameY / wrapperHeight) * 100,
|
|
79
|
-
percentWidth: 85,
|
|
80
|
-
percentHeight: 70
|
|
88
|
+
percentWidth: 85,
|
|
89
|
+
percentHeight: 70,
|
|
81
90
|
};
|
|
82
|
-
|
|
83
|
-
console.log(
|
|
91
|
+
|
|
92
|
+
console.log('Green frame coordinates calculated:', frameCoords);
|
|
84
93
|
return frameCoords;
|
|
85
94
|
};
|
|
86
95
|
|
|
87
|
-
// 🔁 Keep green frame state in sync with wrapper layout so we can both render it
|
|
88
|
-
// and send the exact same coordinates along with the captured photo.
|
|
89
96
|
useEffect(() => {
|
|
90
97
|
const coords = calculateGreenFrameCoordinates();
|
|
91
98
|
if (coords) {
|
|
@@ -94,63 +101,86 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
94
101
|
}, [cameraWrapperLayout]);
|
|
95
102
|
|
|
96
103
|
const takePicture = async () => {
|
|
97
|
-
if (cameraRef.current)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Wait a bit before taking picture (works on iOS)
|
|
105
|
-
await waitForRender(2);
|
|
106
|
-
|
|
107
|
-
// ✅ REFACTORISATION : Capture avec qualité maximale et préservation des métadonnées
|
|
108
|
-
const photo = await cameraRef.current.takePictureAsync({
|
|
109
|
-
quality: 1, // Qualité maximale (0-1, 1 = meilleure qualité)
|
|
110
|
-
shutterSound: false,
|
|
111
|
-
// skipProcessing: true = Désactiver le traitement automatique pour préserver la qualité pixel-perfect
|
|
112
|
-
// IMPORTANT : Cela préserve la résolution originale et évite toute interpolation
|
|
113
|
-
skipProcessing: true,
|
|
114
|
-
// exif: true = Inclure les métadonnées EXIF (orientation, etc.)
|
|
115
|
-
// L'orientation sera gérée dans ImageCropper si nécessaire via la fonction de rotation
|
|
116
|
-
exif: true,
|
|
117
|
-
});
|
|
104
|
+
if (!cameraRef.current || !device) return;
|
|
105
|
+
try {
|
|
106
|
+
waitForRender(5).then(() => {
|
|
107
|
+
setLoadingBeforeCapture(true);
|
|
108
|
+
});
|
|
109
|
+
await waitForRender(2);
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
exif: photo.exif ? "present" : "missing"
|
|
124
|
-
});
|
|
111
|
+
const photo = await cameraRef.current.takePhoto({
|
|
112
|
+
enableShutterSound: false,
|
|
113
|
+
flash: 'off',
|
|
114
|
+
});
|
|
125
115
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
129
|
-
|
|
130
|
-
// ✅ REFACTORISATION : Utiliser directement l'URI de la photo
|
|
131
|
-
// L'orientation sera gérée dans ImageCropper si l'utilisateur utilise la fonction de rotation
|
|
132
|
-
// skipProcessing: true préserve la qualité mais peut laisser l'orientation EXIF non appliquée
|
|
133
|
-
// C'est acceptable car l'utilisateur peut corriger via la rotation dans ImageCropper
|
|
134
|
-
const fixedUri = photo.uri;
|
|
135
|
-
|
|
136
|
-
// Envoyer l'URI et les coordonnées du green frame à ImageCropper
|
|
137
|
-
onPhotoCaptured(fixedUri, {
|
|
138
|
-
greenFrame: greenFrameCoords,
|
|
139
|
-
capturedImageSize: { width: photo.width, height: photo.height }
|
|
140
|
-
});
|
|
141
|
-
setLoadingBeforeCapture(false);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
console.error("Error capturing photo:", error);
|
|
144
|
-
setLoadingBeforeCapture(false);
|
|
145
|
-
Alert.alert("Erreur", "Impossible de capturer la photo. Veuillez réessayer.");
|
|
116
|
+
if (!photo.path || !photo.width || !photo.height) {
|
|
117
|
+
throw new Error('Invalid photo received from camera');
|
|
146
118
|
}
|
|
119
|
+
|
|
120
|
+
const uri =
|
|
121
|
+
photo.path.startsWith('file://') ? photo.path : `file://${photo.path}`;
|
|
122
|
+
const capturedAspectRatio = photo.width / photo.height;
|
|
123
|
+
|
|
124
|
+
console.log('Photo captured (VisionCamera):', {
|
|
125
|
+
uri,
|
|
126
|
+
width: photo.width,
|
|
127
|
+
height: photo.height,
|
|
128
|
+
aspectRatio: capturedAspectRatio.toFixed(3),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
132
|
+
if (!greenFrameCoords) {
|
|
133
|
+
throw new Error('Green frame coordinates not available');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
onPhotoCaptured(uri, {
|
|
137
|
+
greenFrame: greenFrameCoords,
|
|
138
|
+
capturedImageSize: {
|
|
139
|
+
width: photo.width,
|
|
140
|
+
height: photo.height,
|
|
141
|
+
aspectRatio: capturedAspectRatio,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
setLoadingBeforeCapture(false);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error('Error capturing photo:', error);
|
|
148
|
+
setLoadingBeforeCapture(false);
|
|
149
|
+
Alert.alert(
|
|
150
|
+
'Erreur',
|
|
151
|
+
`Impossible de capturer la photo: ${error.message || 'Erreur inconnue'}. Veuillez réessayer.`
|
|
152
|
+
);
|
|
147
153
|
}
|
|
148
154
|
};
|
|
149
|
-
|
|
155
|
+
|
|
156
|
+
if (!hasPermission) {
|
|
157
|
+
return (
|
|
158
|
+
<SafeAreaView style={styles.outerContainer}>
|
|
159
|
+
<View style={styles.permissionContainer}>
|
|
160
|
+
<Text style={styles.text}>
|
|
161
|
+
Accès à la caméra requis pour prendre des photos.
|
|
162
|
+
</Text>
|
|
163
|
+
<TouchableOpacity style={styles.button} onPress={requestPermission}>
|
|
164
|
+
<Text style={styles.buttonText}>Autoriser la caméra</Text>
|
|
165
|
+
</TouchableOpacity>
|
|
166
|
+
</View>
|
|
167
|
+
</SafeAreaView>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!device) {
|
|
172
|
+
return (
|
|
173
|
+
<SafeAreaView style={styles.outerContainer}>
|
|
174
|
+
<View style={styles.permissionContainer}>
|
|
175
|
+
<Text style={styles.text}>Aucune caméra arrière disponible.</Text>
|
|
176
|
+
</View>
|
|
177
|
+
</SafeAreaView>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
150
180
|
|
|
151
181
|
return (
|
|
152
182
|
<SafeAreaView style={styles.outerContainer}>
|
|
153
|
-
<View
|
|
183
|
+
<View
|
|
154
184
|
style={styles.cameraWrapper}
|
|
155
185
|
ref={cameraWrapperRef}
|
|
156
186
|
onLayout={(e) => {
|
|
@@ -159,19 +189,23 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
159
189
|
width: layout.width,
|
|
160
190
|
height: layout.height,
|
|
161
191
|
x: layout.x,
|
|
162
|
-
y: layout.y
|
|
192
|
+
y: layout.y,
|
|
163
193
|
});
|
|
164
|
-
console.log(
|
|
194
|
+
console.log('Camera wrapper layout updated:', layout);
|
|
165
195
|
}}
|
|
166
196
|
>
|
|
167
|
-
<
|
|
168
|
-
style={styles.camera}
|
|
169
|
-
facing="back"
|
|
197
|
+
<Camera
|
|
170
198
|
ref={cameraRef}
|
|
171
|
-
|
|
199
|
+
style={StyleSheet.absoluteFill}
|
|
200
|
+
device={device}
|
|
201
|
+
isActive={true}
|
|
202
|
+
photo={true}
|
|
203
|
+
onInitialized={() => {
|
|
204
|
+
setIsReady(true);
|
|
205
|
+
console.log('VisionCamera ready - preview matches capture');
|
|
206
|
+
}}
|
|
172
207
|
/>
|
|
173
208
|
|
|
174
|
-
{/* Loading overlay */}
|
|
175
209
|
{loadingBeforeCapture && (
|
|
176
210
|
<>
|
|
177
211
|
<View style={styles.loadingOverlay}>
|
|
@@ -185,7 +219,6 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
185
219
|
</>
|
|
186
220
|
)}
|
|
187
221
|
|
|
188
|
-
{/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
|
|
189
222
|
{greenFrame && (
|
|
190
223
|
<View
|
|
191
224
|
style={[
|
|
@@ -201,7 +234,12 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
201
234
|
)}
|
|
202
235
|
</View>
|
|
203
236
|
|
|
204
|
-
<View
|
|
237
|
+
<View
|
|
238
|
+
style={[
|
|
239
|
+
styles.buttonContainer,
|
|
240
|
+
{ bottom: (insets?.bottom || 0) + 16, marginBottom: 0 },
|
|
241
|
+
]}
|
|
242
|
+
>
|
|
205
243
|
<TouchableOpacity
|
|
206
244
|
style={styles.button}
|
|
207
245
|
onPress={takePicture}
|
|
@@ -232,9 +270,6 @@ const styles = StyleSheet.create({
|
|
|
232
270
|
justifyContent: 'center',
|
|
233
271
|
position: 'relative',
|
|
234
272
|
},
|
|
235
|
-
camera: {
|
|
236
|
-
...StyleSheet.absoluteFillObject,
|
|
237
|
-
},
|
|
238
273
|
scanFrame: {
|
|
239
274
|
position: 'absolute',
|
|
240
275
|
borderWidth: 4,
|
|
@@ -258,14 +293,6 @@ const styles = StyleSheet.create({
|
|
|
258
293
|
zIndex: 21,
|
|
259
294
|
backgroundColor: 'transparent',
|
|
260
295
|
},
|
|
261
|
-
cancelIcon: {
|
|
262
|
-
position: 'absolute',
|
|
263
|
-
top: 20,
|
|
264
|
-
left: 20,
|
|
265
|
-
backgroundColor: PRIMARY_GREEN,
|
|
266
|
-
borderRadius: 5,
|
|
267
|
-
padding: 8,
|
|
268
|
-
},
|
|
269
296
|
buttonContainer: {
|
|
270
297
|
position: 'absolute',
|
|
271
298
|
bottom: 0,
|
|
@@ -287,16 +314,15 @@ const styles = StyleSheet.create({
|
|
|
287
314
|
fontSize: 18,
|
|
288
315
|
color: GLOW_WHITE,
|
|
289
316
|
},
|
|
290
|
-
|
|
317
|
+
permissionContainer: {
|
|
291
318
|
flex: 1,
|
|
292
|
-
backgroundColor: DEEP_BLACK,
|
|
293
319
|
justifyContent: 'center',
|
|
294
320
|
alignItems: 'center',
|
|
295
321
|
padding: 20,
|
|
296
322
|
},
|
|
297
|
-
|
|
298
|
-
fontSize:
|
|
299
|
-
color:
|
|
323
|
+
buttonText: {
|
|
324
|
+
fontSize: 16,
|
|
325
|
+
color: DEEP_BLACK,
|
|
300
326
|
fontWeight: '600',
|
|
301
327
|
},
|
|
302
|
-
});
|
|
328
|
+
});
|