react-native-expo-cropper 1.2.39 → 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,7 +8,7 @@ exports["default"] = CustomCamera;
8
8
  var _react = _interopRequireWildcard(require("react"));
9
9
  var _reactNative = require("react-native");
10
10
  var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
11
- var _expoCamera = require("expo-camera");
11
+ var _reactNativeVisionCamera = require("react-native-vision-camera");
12
12
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t2 in e) "default" !== _t2 && {}.hasOwnProperty.call(e, _t2) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t2)) && (i.get || i.set) ? o(f, _t2, i) : f[_t2] = e[_t2]); return f; })(e, t); }
13
13
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
14
14
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -38,44 +38,31 @@ function CustomCamera(_ref) {
38
38
  _useState4 = _slicedToArray(_useState3, 2),
39
39
  loadingBeforeCapture = _useState4[0],
40
40
  setLoadingBeforeCapture = _useState4[1];
41
- var _useState5 = (0, _react.useState)(null),
42
- _useState6 = _slicedToArray(_useState5, 2),
43
- hasPermission = _useState6[0],
44
- setHasPermission = _useState6[1];
45
41
  var cameraRef = (0, _react.useRef)(null);
46
42
  var cameraWrapperRef = (0, _react.useRef)(null);
47
43
  var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
48
- var _useState7 = (0, _react.useState)({
44
+ var _useState5 = (0, _react.useState)({
49
45
  width: 0,
50
46
  height: 0,
51
47
  x: 0,
52
48
  y: 0
53
49
  }),
50
+ _useState6 = _slicedToArray(_useState5, 2),
51
+ cameraWrapperLayout = _useState6[0],
52
+ setCameraWrapperLayout = _useState6[1];
53
+ var _useState7 = (0, _react.useState)(null),
54
54
  _useState8 = _slicedToArray(_useState7, 2),
55
- cameraWrapperLayout = _useState8[0],
56
- setCameraWrapperLayout = _useState8[1];
57
- var _useState9 = (0, _react.useState)(null),
58
- _useState0 = _slicedToArray(_useState9, 2),
59
- greenFrame = _useState0[0],
60
- setGreenFrame = _useState0[1];
55
+ greenFrame = _useState8[0],
56
+ setGreenFrame = _useState8[1];
57
+ var _useCameraPermission = (0, _reactNativeVisionCamera.useCameraPermission)(),
58
+ hasPermission = _useCameraPermission.hasPermission,
59
+ requestPermission = _useCameraPermission.requestPermission;
60
+ var device = (0, _reactNativeVisionCamera.useCameraDevice)('back');
61
61
  (0, _react.useEffect)(function () {
62
- _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
63
- var _yield$Camera$request, status;
64
- return _regenerator().w(function (_context) {
65
- while (1) switch (_context.n) {
66
- case 0:
67
- _context.n = 1;
68
- return _expoCamera.Camera.requestCameraPermissionsAsync();
69
- case 1:
70
- _yield$Camera$request = _context.v;
71
- status = _yield$Camera$request.status;
72
- setHasPermission(status === 'granted');
73
- case 2:
74
- return _context.a(2);
75
- }
76
- }, _callee);
77
- }))();
78
- }, []);
62
+ if (!hasPermission) {
63
+ requestPermission();
64
+ }
65
+ }, [hasPermission, requestPermission]);
79
66
 
80
67
  // Helper function to wait for multiple render cycles (works on iOS)
81
68
  var waitForRender = function waitForRender() {
@@ -94,26 +81,19 @@ function CustomCamera(_ref) {
94
81
  });
95
82
  };
96
83
 
97
- // CRITICAL FIX: Calculate green frame coordinates relative to camera preview
98
- // The green frame should be calculated on the wrapper (as it's visually drawn there)
99
- // But we store it with wrapper dimensions so ImageCropper can map it correctly
100
- //
101
- // NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
102
- // This is handled in ImageCropper by using "cover" mode to match preview content
84
+ // Green frame coordinates relative to camera preview wrapper.
85
+ // Preview matches sensor output (VisionCamera) so framing matches capture.
103
86
  var calculateGreenFrameCoordinates = function calculateGreenFrameCoordinates() {
104
87
  var wrapperWidth = cameraWrapperLayout.width;
105
88
  var wrapperHeight = cameraWrapperLayout.height;
106
89
  if (wrapperWidth === 0 || wrapperHeight === 0) {
107
- console.warn("Camera wrapper layout not ready, cannot calculate green frame");
90
+ console.warn('Camera wrapper layout not ready, cannot calculate green frame');
108
91
  return null;
109
92
  }
110
-
111
- // Calculate green frame as percentage of WRAPPER
112
- var frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
113
- var frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
114
- var frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
115
- var frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
116
-
93
+ var frameWidth = wrapperWidth * 0.85;
94
+ var frameHeight = wrapperHeight * 0.7;
95
+ var frameX = (wrapperWidth - frameWidth) / 2;
96
+ var frameY = (wrapperHeight - frameHeight) / 2;
117
97
  var frameCoords = {
118
98
  x: frameX,
119
99
  y: frameY,
@@ -121,19 +101,14 @@ function CustomCamera(_ref) {
121
101
  height: frameHeight,
122
102
  wrapperWidth: wrapperWidth,
123
103
  wrapperHeight: wrapperHeight,
124
- // ✅ Store percentages for easier mapping later
125
104
  percentX: frameX / wrapperWidth * 100,
126
105
  percentY: frameY / wrapperHeight * 100,
127
106
  percentWidth: 85,
128
- // 85% of wrapper width
129
- percentHeight: 70 // 70% of wrapper height
107
+ percentHeight: 70
130
108
  };
131
- console.log("✅ Green frame coordinates calculated:", frameCoords);
109
+ console.log('Green frame coordinates calculated:', frameCoords);
132
110
  return frameCoords;
133
111
  };
134
-
135
- // 🔁 Keep green frame state in sync with wrapper layout so we can both render it
136
- // and send the exact same coordinates along with the captured photo.
137
112
  (0, _react.useEffect)(function () {
138
113
  var coords = calculateGreenFrameCoordinates();
139
114
  if (coords) {
@@ -141,86 +116,53 @@ function CustomCamera(_ref) {
141
116
  }
142
117
  }, [cameraWrapperLayout]);
143
118
  var takePicture = /*#__PURE__*/function () {
144
- var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
145
- var captureOptions, photo, capturedAspectRatio, greenFrameCoords, _t;
146
- return _regenerator().w(function (_context2) {
147
- while (1) switch (_context2.p = _context2.n) {
119
+ var _ref2 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
120
+ var photo, uri, capturedAspectRatio, greenFrameCoords, _t;
121
+ return _regenerator().w(function (_context) {
122
+ while (1) switch (_context.p = _context.n) {
148
123
  case 0:
149
- if (!cameraRef.current) {
150
- _context2.n = 7;
124
+ if (!(!cameraRef.current || !device)) {
125
+ _context.n = 1;
151
126
  break;
152
127
  }
153
- _context2.p = 1;
154
- // Show loading after a delay (using setImmediate for iOS compatibility)
128
+ return _context.a(2);
129
+ case 1:
130
+ _context.p = 1;
155
131
  waitForRender(5).then(function () {
156
132
  setLoadingBeforeCapture(true);
157
133
  });
158
-
159
- // Wait a bit before taking picture (works on iOS)
160
- _context2.n = 2;
134
+ _context.n = 2;
161
135
  return waitForRender(2);
162
136
  case 2:
163
- // OPTIMIZED: Capture with maximum quality and native camera ratio
164
- // Platform-specific optimizations for best quality
165
- captureOptions = _objectSpread(_objectSpread({
166
- // Maximum quality (0-1, 1 = best quality, no compression)
167
- quality: 1,
168
- // Disable shutter sound for better UX
169
- shutterSound: false,
170
- // ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
171
- // This ensures pixel-perfect quality and prevents premature resizing
172
- skipProcessing: true,
173
- // Include EXIF metadata (orientation, camera settings, etc.)
174
- exif: true
175
- }, _reactNative.Platform.OS === 'ios' && {
176
- // iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
177
- // No additional processing to preserve quality
178
- }), _reactNative.Platform.OS === 'android' && {
179
- // Android: Ensure maximum resolution capture
180
- // skipProcessing already handles this, but we can add Android-specific options if needed
181
- });
182
- console.log("📸 Capturing photo with maximum quality settings:", {
183
- platform: _reactNative.Platform.OS,
184
- options: captureOptions,
185
- wrapperSize: {
186
- width: cameraWrapperLayout.width,
187
- height: cameraWrapperLayout.height
188
- }
137
+ _context.n = 3;
138
+ return cameraRef.current.takePhoto({
139
+ enableShutterSound: false,
140
+ flash: 'off'
189
141
  });
190
- _context2.n = 3;
191
- return cameraRef.current.takePictureAsync(captureOptions);
192
142
  case 3:
193
- photo = _context2.v;
194
- if (!(!photo.width || !photo.height || photo.width === 0 || photo.height === 0)) {
195
- _context2.n = 4;
143
+ photo = _context.v;
144
+ if (!(!photo.path || !photo.width || !photo.height)) {
145
+ _context.n = 4;
196
146
  break;
197
147
  }
198
- throw new Error("Invalid photo dimensions received from camera");
148
+ throw new Error('Invalid photo received from camera');
199
149
  case 4:
150
+ uri = photo.path.startsWith('file://') ? photo.path : "file://".concat(photo.path);
200
151
  capturedAspectRatio = photo.width / photo.height;
201
- console.log("✅ Photo captured with maximum quality:", {
202
- uri: photo.uri,
152
+ console.log('Photo captured (VisionCamera):', {
153
+ uri: uri,
203
154
  width: photo.width,
204
155
  height: photo.height,
205
- aspectRatio: capturedAspectRatio.toFixed(3),
206
- expectedRatio: "~1.33 (4:3) or ~1.78 (16:9)",
207
- exif: photo.exif ? "present" : "missing",
208
- fileSize: photo.uri ? "available" : "unknown"
156
+ aspectRatio: capturedAspectRatio.toFixed(3)
209
157
  });
210
-
211
- // ✅ CRITICAL FIX: Use the same green frame coordinates that are used for rendering
212
- // Fallback to recalculation if, for some reason, state is not yet set
213
158
  greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
214
159
  if (greenFrameCoords) {
215
- _context2.n = 5;
160
+ _context.n = 5;
216
161
  break;
217
162
  }
218
- throw new Error("Green frame coordinates not available");
163
+ throw new Error('Green frame coordinates not available');
219
164
  case 5:
220
- // ✅ Send photo URI and frame data to ImageCropper
221
- // The photo maintains its native resolution and aspect ratio
222
- // ImageCropper will handle display and cropping while preserving quality
223
- onPhotoCaptured(photo.uri, {
165
+ onPhotoCaptured(uri, {
224
166
  greenFrame: greenFrameCoords,
225
167
  capturedImageSize: {
226
168
  width: photo.width,
@@ -229,23 +171,46 @@ function CustomCamera(_ref) {
229
171
  }
230
172
  });
231
173
  setLoadingBeforeCapture(false);
232
- _context2.n = 7;
174
+ _context.n = 7;
233
175
  break;
234
176
  case 6:
235
- _context2.p = 6;
236
- _t = _context2.v;
237
- console.error("❌ Error capturing photo:", _t);
177
+ _context.p = 6;
178
+ _t = _context.v;
179
+ console.error('Error capturing photo:', _t);
238
180
  setLoadingBeforeCapture(false);
239
- _reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
181
+ _reactNative.Alert.alert('Erreur', "Impossible de capturer la photo: ".concat(_t.message || 'Erreur inconnue', ". Veuillez r\xE9essayer."));
240
182
  case 7:
241
- return _context2.a(2);
183
+ return _context.a(2);
242
184
  }
243
- }, _callee2, null, [[1, 6]]);
185
+ }, _callee, null, [[1, 6]]);
244
186
  }));
245
187
  return function takePicture() {
246
- return _ref3.apply(this, arguments);
188
+ return _ref2.apply(this, arguments);
247
189
  };
248
190
  }();
191
+ if (!hasPermission) {
192
+ return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
193
+ style: styles.outerContainer
194
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
195
+ style: styles.permissionContainer
196
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
197
+ style: styles.text
198
+ }, "Acc\xE8s \xE0 la cam\xE9ra requis pour prendre des photos."), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
199
+ style: styles.button,
200
+ onPress: requestPermission
201
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
202
+ style: styles.buttonText
203
+ }, "Autoriser la cam\xE9ra"))));
204
+ }
205
+ if (!device) {
206
+ return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
207
+ style: styles.outerContainer
208
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
209
+ style: styles.permissionContainer
210
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
211
+ style: styles.text
212
+ }, "Aucune cam\xE9ra arri\xE8re disponible.")));
213
+ }
249
214
  return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
250
215
  style: styles.outerContainer
251
216
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
@@ -259,19 +224,18 @@ function CustomCamera(_ref) {
259
224
  x: layout.x,
260
225
  y: layout.y
261
226
  });
262
- console.log("Camera wrapper layout updated:", layout);
227
+ console.log('Camera wrapper layout updated:', layout);
263
228
  }
264
- }, /*#__PURE__*/_react["default"].createElement(_expoCamera.CameraView, {
265
- style: styles.camera,
266
- facing: "back",
229
+ }, /*#__PURE__*/_react["default"].createElement(_reactNativeVisionCamera.Camera, {
267
230
  ref: cameraRef,
268
- onCameraReady: function onCameraReady() {
231
+ style: _reactNative.StyleSheet.absoluteFill,
232
+ device: device,
233
+ isActive: true,
234
+ photo: true,
235
+ onInitialized: function onInitialized() {
269
236
  setIsReady(true);
270
- console.log("✅ Camera ready - Maximum quality capture enabled");
237
+ console.log('VisionCamera ready - preview matches capture');
271
238
  }
272
- // ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
273
- // The wrapper constrains the view, but capture uses full sensor resolution
274
- // This ensures preview matches what will be captured
275
239
  }), loadingBeforeCapture && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
276
240
  style: styles.loadingOverlay
277
241
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
@@ -318,7 +282,6 @@ var styles = _reactNative.StyleSheet.create({
318
282
  justifyContent: 'center',
319
283
  position: 'relative'
320
284
  },
321
- camera: _objectSpread({}, _reactNative.StyleSheet.absoluteFillObject),
322
285
  scanFrame: {
323
286
  position: 'absolute',
324
287
  borderWidth: 4,
@@ -340,14 +303,6 @@ var styles = _reactNative.StyleSheet.create({
340
303
  zIndex: 21,
341
304
  backgroundColor: 'transparent'
342
305
  }),
343
- cancelIcon: {
344
- position: 'absolute',
345
- top: 20,
346
- left: 20,
347
- backgroundColor: PRIMARY_GREEN,
348
- borderRadius: 5,
349
- padding: 8
350
- },
351
306
  buttonContainer: {
352
307
  position: 'absolute',
353
308
  bottom: 0,
@@ -369,16 +324,15 @@ var styles = _reactNative.StyleSheet.create({
369
324
  fontSize: 18,
370
325
  color: GLOW_WHITE
371
326
  },
372
- container: {
327
+ permissionContainer: {
373
328
  flex: 1,
374
- backgroundColor: DEEP_BLACK,
375
329
  justifyContent: 'center',
376
330
  alignItems: 'center',
377
331
  padding: 20
378
332
  },
379
- iconText: {
380
- fontSize: 18,
381
- color: GLOW_WHITE,
333
+ buttonText: {
334
+ fontSize: 16,
335
+ color: DEEP_BLACK,
382
336
  fontWeight: '600'
383
337
  }
384
338
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-expo-cropper",
3
- "version": "1.2.39",
3
+ "version": "1.2.40",
4
4
  "description": "Recadrage polygonal d'images.",
5
5
  "main": "index.js",
6
6
  "author": "PCS AGRI",
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@expo/vector-icons": "^15.0.2",
24
+ "@react-native-masked-view/masked-view": "^0.3.1",
24
25
  "expo": "54.0.0",
25
26
  "expo-camera": "~17.0.8",
26
27
  "expo-image-manipulator": "~14.0.7",
@@ -29,7 +30,7 @@
29
30
  "expo-screen-orientation": "~9.0.7",
30
31
  "react-native-svg": "15.12.1",
31
32
  "react-native-view-shot": "4.0.3",
32
- "@react-native-masked-view/masked-view": "^0.3.1"
33
+ "react-native-vision-camera": "^4.7.3"
33
34
  },
34
35
  "overrides": {
35
36
  "glob": "^10.5.0"
@@ -45,10 +46,10 @@
45
46
  "url": "https://github.com/pcsagri/react-native-expo-cropper/issues"
46
47
  },
47
48
  "homepage": "https://github.com/pcsagri/react-native-expo-cropper#readme",
48
- "scripts": {
49
- "test": "echo \"Error: no test specified\" && exit 1",
50
- "build": "npx --package=@babel/cli babel src --out-dir dist"
51
- },
49
+ "scripts": {
50
+ "test": "echo \"Error: no test specified\" && exit 1",
51
+ "build": "npx --package=@babel/cli babel src --out-dir dist"
52
+ },
52
53
  "devDependencies": {
53
54
  "@babel/cli": "^7.28.3",
54
55
  "@babel/core": "^7.28.5",
@@ -11,25 +11,36 @@ import {
11
11
  Platform,
12
12
  } from 'react-native';
13
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
14
- import { Camera, CameraView } from 'expo-camera';
14
+ import {
15
+ Camera,
16
+ useCameraDevice,
17
+ useCameraPermission,
18
+ } from 'react-native-vision-camera';
19
+
15
20
  const { width } = Dimensions.get('window');
16
21
 
17
22
  export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
18
23
  const [isReady, setIsReady] = useState(false);
19
24
  const [loadingBeforeCapture, setLoadingBeforeCapture] = useState(false);
20
- const [hasPermission, setHasPermission] = useState(null);
21
25
  const cameraRef = useRef(null);
22
26
  const cameraWrapperRef = useRef(null);
23
27
  const insets = useSafeAreaInsets();
24
- 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
+ });
25
34
  const [greenFrame, setGreenFrame] = useState(null);
26
35
 
36
+ const { hasPermission, requestPermission } = useCameraPermission();
37
+ const device = useCameraDevice('back');
38
+
27
39
  useEffect(() => {
28
- (async () => {
29
- const { status } = await Camera.requestCameraPermissionsAsync();
30
- setHasPermission(status === 'granted');
31
- })();
32
- }, []);
40
+ if (!hasPermission) {
41
+ requestPermission();
42
+ }
43
+ }, [hasPermission, requestPermission]);
33
44
 
34
45
  // Helper function to wait for multiple render cycles (works on iOS)
35
46
  const waitForRender = (cycles = 5) => {
@@ -47,29 +58,24 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
47
58
  });
48
59
  };
49
60
 
50
-
51
-
52
- // ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
53
- // The green frame should be calculated on the wrapper (as it's visually drawn there)
54
- // But we store it with wrapper dimensions so ImageCropper can map it correctly
55
- //
56
- // NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
57
- // This is handled in ImageCropper by using "cover" mode to match preview content
61
+ // Green frame coordinates relative to camera preview wrapper.
62
+ // Preview matches sensor output (VisionCamera) so framing matches capture.
58
63
  const calculateGreenFrameCoordinates = () => {
59
64
  const wrapperWidth = cameraWrapperLayout.width;
60
65
  const wrapperHeight = cameraWrapperLayout.height;
61
-
66
+
62
67
  if (wrapperWidth === 0 || wrapperHeight === 0) {
63
- 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
+ );
64
71
  return null;
65
72
  }
66
-
67
- // Calculate green frame as percentage of WRAPPER
68
- const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
69
- const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
70
- const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
71
- const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
72
-
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
+
73
79
  const frameCoords = {
74
80
  x: frameX,
75
81
  y: frameY,
@@ -77,19 +83,16 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
77
83
  height: frameHeight,
78
84
  wrapperWidth,
79
85
  wrapperHeight,
80
- // ✅ Store percentages for easier mapping later
81
86
  percentX: (frameX / wrapperWidth) * 100,
82
87
  percentY: (frameY / wrapperHeight) * 100,
83
- percentWidth: 85, // 85% of wrapper width
84
- percentHeight: 70 // 70% of wrapper height
88
+ percentWidth: 85,
89
+ percentHeight: 70,
85
90
  };
86
-
87
- console.log("✅ Green frame coordinates calculated:", frameCoords);
91
+
92
+ console.log('Green frame coordinates calculated:', frameCoords);
88
93
  return frameCoords;
89
94
  };
90
95
 
91
- // 🔁 Keep green frame state in sync with wrapper layout so we can both render it
92
- // and send the exact same coordinates along with the captured photo.
93
96
  useEffect(() => {
94
97
  const coords = calculateGreenFrameCoordinates();
95
98
  if (coords) {
@@ -98,103 +101,86 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
98
101
  }, [cameraWrapperLayout]);
99
102
 
100
103
  const takePicture = async () => {
101
- if (cameraRef.current) {
102
- try {
103
- // Show loading after a delay (using setImmediate for iOS compatibility)
104
- waitForRender(5).then(() => {
105
- setLoadingBeforeCapture(true);
106
- });
107
-
108
- // Wait a bit before taking picture (works on iOS)
109
- await waitForRender(2);
110
-
111
- // ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
112
- // Platform-specific optimizations for best quality
113
- const captureOptions = {
114
- // Maximum quality (0-1, 1 = best quality, no compression)
115
- quality: 1,
116
-
117
- // Disable shutter sound for better UX
118
- shutterSound: false,
119
-
120
- // ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
121
- // This ensures pixel-perfect quality and prevents premature resizing
122
- skipProcessing: true,
123
-
124
- // Include EXIF metadata (orientation, camera settings, etc.)
125
- exif: true,
126
-
127
- // ✅ Platform-specific optimizations
128
- ...(Platform.OS === 'ios' && {
129
- // iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
130
- // No additional processing to preserve quality
131
- }),
132
- ...(Platform.OS === 'android' && {
133
- // Android: Ensure maximum resolution capture
134
- // skipProcessing already handles this, but we can add Android-specific options if needed
135
- }),
136
- };
104
+ if (!cameraRef.current || !device) return;
105
+ try {
106
+ waitForRender(5).then(() => {
107
+ setLoadingBeforeCapture(true);
108
+ });
109
+ await waitForRender(2);
137
110
 
138
- console.log("📸 Capturing photo with maximum quality settings:", {
139
- platform: Platform.OS,
140
- options: captureOptions,
141
- wrapperSize: { width: cameraWrapperLayout.width, height: cameraWrapperLayout.height }
142
- });
111
+ const photo = await cameraRef.current.takePhoto({
112
+ enableShutterSound: false,
113
+ flash: 'off',
114
+ });
143
115
 
144
- const photo = await cameraRef.current.takePictureAsync(captureOptions);
116
+ if (!photo.path || !photo.width || !photo.height) {
117
+ throw new Error('Invalid photo received from camera');
118
+ }
145
119
 
146
- // Validate captured photo dimensions
147
- if (!photo.width || !photo.height || photo.width === 0 || photo.height === 0) {
148
- throw new Error("Invalid photo dimensions received from camera");
149
- }
120
+ const uri =
121
+ photo.path.startsWith('file://') ? photo.path : `file://${photo.path}`;
122
+ const capturedAspectRatio = photo.width / photo.height;
150
123
 
151
- const capturedAspectRatio = photo.width / photo.height;
152
- console.log("✅ Photo captured with maximum quality:", {
153
- uri: photo.uri,
154
- width: photo.width,
155
- height: photo.height,
156
- aspectRatio: capturedAspectRatio.toFixed(3),
157
- expectedRatio: "~1.33 (4:3) or ~1.78 (16:9)",
158
- exif: photo.exif ? "present" : "missing",
159
- fileSize: photo.uri ? "available" : "unknown"
160
- });
124
+ console.log('Photo captured (VisionCamera):', {
125
+ uri,
126
+ width: photo.width,
127
+ height: photo.height,
128
+ aspectRatio: capturedAspectRatio.toFixed(3),
129
+ });
161
130
 
162
- // CRITICAL FIX: Use the same green frame coordinates that are used for rendering
163
- // Fallback to recalculation if, for some reason, state is not yet set
164
- const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
165
-
166
- if (!greenFrameCoords) {
167
- throw new Error("Green frame coordinates not available");
168
- }
169
-
170
- // ✅ Send photo URI and frame data to ImageCropper
171
- // The photo maintains its native resolution and aspect ratio
172
- // ImageCropper will handle display and cropping while preserving quality
173
- onPhotoCaptured(photo.uri, {
174
- greenFrame: greenFrameCoords,
175
- capturedImageSize: {
176
- width: photo.width,
177
- height: photo.height,
178
- aspectRatio: capturedAspectRatio
179
- }
180
- });
181
-
182
- setLoadingBeforeCapture(false);
183
- } catch (error) {
184
- console.error("❌ Error capturing photo:", error);
185
- setLoadingBeforeCapture(false);
186
- Alert.alert(
187
- "Erreur",
188
- `Impossible de capturer la photo: ${error.message || "Erreur inconnue"}. Veuillez réessayer.`
189
- );
131
+ const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
132
+ if (!greenFrameCoords) {
133
+ throw new Error('Green frame coordinates not available');
190
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
+ );
191
153
  }
192
154
  };
193
-
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
+ }
194
180
 
195
181
  return (
196
182
  <SafeAreaView style={styles.outerContainer}>
197
- <View
183
+ <View
198
184
  style={styles.cameraWrapper}
199
185
  ref={cameraWrapperRef}
200
186
  onLayout={(e) => {
@@ -203,25 +189,23 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
203
189
  width: layout.width,
204
190
  height: layout.height,
205
191
  x: layout.x,
206
- y: layout.y
192
+ y: layout.y,
207
193
  });
208
- console.log("Camera wrapper layout updated:", layout);
194
+ console.log('Camera wrapper layout updated:', layout);
209
195
  }}
210
196
  >
211
- <CameraView
212
- style={styles.camera}
213
- facing="back"
197
+ <Camera
214
198
  ref={cameraRef}
215
- onCameraReady={() => {
199
+ style={StyleSheet.absoluteFill}
200
+ device={device}
201
+ isActive={true}
202
+ photo={true}
203
+ onInitialized={() => {
216
204
  setIsReady(true);
217
- console.log("✅ Camera ready - Maximum quality capture enabled");
205
+ console.log('VisionCamera ready - preview matches capture');
218
206
  }}
219
- // ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
220
- // The wrapper constrains the view, but capture uses full sensor resolution
221
- // This ensures preview matches what will be captured
222
207
  />
223
208
 
224
- {/* Loading overlay */}
225
209
  {loadingBeforeCapture && (
226
210
  <>
227
211
  <View style={styles.loadingOverlay}>
@@ -235,7 +219,6 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
235
219
  </>
236
220
  )}
237
221
 
238
- {/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
239
222
  {greenFrame && (
240
223
  <View
241
224
  style={[
@@ -251,7 +234,12 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
251
234
  )}
252
235
  </View>
253
236
 
254
- <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
+ >
255
243
  <TouchableOpacity
256
244
  style={styles.button}
257
245
  onPress={takePicture}
@@ -282,9 +270,6 @@ const styles = StyleSheet.create({
282
270
  justifyContent: 'center',
283
271
  position: 'relative',
284
272
  },
285
- camera: {
286
- ...StyleSheet.absoluteFillObject,
287
- },
288
273
  scanFrame: {
289
274
  position: 'absolute',
290
275
  borderWidth: 4,
@@ -308,14 +293,6 @@ const styles = StyleSheet.create({
308
293
  zIndex: 21,
309
294
  backgroundColor: 'transparent',
310
295
  },
311
- cancelIcon: {
312
- position: 'absolute',
313
- top: 20,
314
- left: 20,
315
- backgroundColor: PRIMARY_GREEN,
316
- borderRadius: 5,
317
- padding: 8,
318
- },
319
296
  buttonContainer: {
320
297
  position: 'absolute',
321
298
  bottom: 0,
@@ -337,16 +314,15 @@ const styles = StyleSheet.create({
337
314
  fontSize: 18,
338
315
  color: GLOW_WHITE,
339
316
  },
340
- container: {
317
+ permissionContainer: {
341
318
  flex: 1,
342
- backgroundColor: DEEP_BLACK,
343
319
  justifyContent: 'center',
344
320
  alignItems: 'center',
345
321
  padding: 20,
346
322
  },
347
- iconText: {
348
- fontSize: 18,
349
- color: GLOW_WHITE,
323
+ buttonText: {
324
+ fontSize: 16,
325
+ color: DEEP_BLACK,
350
326
  fontWeight: '600',
351
327
  },
352
- });
328
+ });