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.
@@ -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 { Camera, CameraView } from 'expo-camera';
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({ width: 0, height: 0, x: 0, y: 0 });
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
- (async () => {
28
- const { status } = await Camera.requestCameraPermissionsAsync();
29
- setHasPermission(status === 'granted');
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("Camera wrapper layout not ready, cannot calculate green frame");
68
+ console.warn(
69
+ 'Camera wrapper layout not ready, cannot calculate green frame'
70
+ );
60
71
  return null;
61
72
  }
62
-
63
- // Calculate green frame as percentage of WRAPPER
64
- const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
65
- const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
66
- const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
67
- const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
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, // 85% of wrapper width
80
- percentHeight: 70 // 70% of wrapper height
88
+ percentWidth: 85,
89
+ percentHeight: 70,
81
90
  };
82
-
83
- console.log("✅ Green frame coordinates calculated:", frameCoords);
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
- try {
99
- // Show loading after a delay (using setImmediate for iOS compatibility)
100
- waitForRender(5).then(() => {
101
- setLoadingBeforeCapture(true);
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
- console.log("Photo captured with maximum quality:", {
120
- uri: photo.uri,
121
- width: photo.width,
122
- height: photo.height,
123
- exif: photo.exif ? "present" : "missing"
124
- });
111
+ const photo = await cameraRef.current.takePhoto({
112
+ enableShutterSound: false,
113
+ flash: 'off',
114
+ });
125
115
 
126
- // CRITICAL FIX: Use the same green frame coordinates that are used for rendering
127
- // Fallback to recalculation if, for some reason, state is not yet set
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("Camera wrapper layout updated:", layout);
194
+ console.log('Camera wrapper layout updated:', layout);
165
195
  }}
166
196
  >
167
- <CameraView
168
- style={styles.camera}
169
- facing="back"
197
+ <Camera
170
198
  ref={cameraRef}
171
- onCameraReady={() => setIsReady(true)}
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 style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
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
- container: {
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
- iconText: {
298
- fontSize: 18,
299
- color: GLOW_WHITE,
323
+ buttonText: {
324
+ fontSize: 16,
325
+ color: DEEP_BLACK,
300
326
  fontWeight: '600',
301
327
  },
302
- });
328
+ });