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.
- package/dist/CustomCamera.js +93 -139
- package/package.json +7 -6
- package/src/CustomCamera.js +124 -148
package/dist/CustomCamera.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
var
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
//
|
|
98
|
-
//
|
|
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(
|
|
90
|
+
console.warn('Camera wrapper layout not ready, cannot calculate green frame');
|
|
108
91
|
return null;
|
|
109
92
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
var
|
|
113
|
-
var
|
|
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
|
-
|
|
129
|
-
percentHeight: 70 // 70% of wrapper height
|
|
107
|
+
percentHeight: 70
|
|
130
108
|
};
|
|
131
|
-
console.log(
|
|
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
|
|
145
|
-
var
|
|
146
|
-
return _regenerator().w(function (
|
|
147
|
-
while (1) switch (
|
|
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
|
-
|
|
124
|
+
if (!(!cameraRef.current || !device)) {
|
|
125
|
+
_context.n = 1;
|
|
151
126
|
break;
|
|
152
127
|
}
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 =
|
|
194
|
-
if (!(!photo.
|
|
195
|
-
|
|
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(
|
|
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(
|
|
202
|
-
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
|
-
|
|
160
|
+
_context.n = 5;
|
|
216
161
|
break;
|
|
217
162
|
}
|
|
218
|
-
throw new Error(
|
|
163
|
+
throw new Error('Green frame coordinates not available');
|
|
219
164
|
case 5:
|
|
220
|
-
|
|
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
|
-
|
|
174
|
+
_context.n = 7;
|
|
233
175
|
break;
|
|
234
176
|
case 6:
|
|
235
|
-
|
|
236
|
-
_t =
|
|
237
|
-
console.error(
|
|
177
|
+
_context.p = 6;
|
|
178
|
+
_t = _context.v;
|
|
179
|
+
console.error('Error capturing photo:', _t);
|
|
238
180
|
setLoadingBeforeCapture(false);
|
|
239
|
-
_reactNative.Alert.alert(
|
|
181
|
+
_reactNative.Alert.alert('Erreur', "Impossible de capturer la photo: ".concat(_t.message || 'Erreur inconnue', ". Veuillez r\xE9essayer."));
|
|
240
182
|
case 7:
|
|
241
|
-
return
|
|
183
|
+
return _context.a(2);
|
|
242
184
|
}
|
|
243
|
-
},
|
|
185
|
+
}, _callee, null, [[1, 6]]);
|
|
244
186
|
}));
|
|
245
187
|
return function takePicture() {
|
|
246
|
-
return
|
|
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(
|
|
227
|
+
console.log('Camera wrapper layout updated:', layout);
|
|
263
228
|
}
|
|
264
|
-
}, /*#__PURE__*/_react["default"].createElement(
|
|
265
|
-
style: styles.camera,
|
|
266
|
-
facing: "back",
|
|
229
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNativeVisionCamera.Camera, {
|
|
267
230
|
ref: cameraRef,
|
|
268
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
fontSize:
|
|
381
|
-
color:
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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",
|
package/src/CustomCamera.js
CHANGED
|
@@ -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 {
|
|
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({
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
68
|
+
console.warn(
|
|
69
|
+
'Camera wrapper layout not ready, cannot calculate green frame'
|
|
70
|
+
);
|
|
64
71
|
return null;
|
|
65
72
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
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,
|
|
84
|
-
percentHeight: 70
|
|
88
|
+
percentWidth: 85,
|
|
89
|
+
percentHeight: 70,
|
|
85
90
|
};
|
|
86
|
-
|
|
87
|
-
console.log(
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
111
|
+
const photo = await cameraRef.current.takePhoto({
|
|
112
|
+
enableShutterSound: false,
|
|
113
|
+
flash: 'off',
|
|
114
|
+
});
|
|
143
115
|
|
|
144
|
-
|
|
116
|
+
if (!photo.path || !photo.width || !photo.height) {
|
|
117
|
+
throw new Error('Invalid photo received from camera');
|
|
118
|
+
}
|
|
145
119
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
120
|
+
const uri =
|
|
121
|
+
photo.path.startsWith('file://') ? photo.path : `file://${photo.path}`;
|
|
122
|
+
const capturedAspectRatio = photo.width / photo.height;
|
|
150
123
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
194
|
+
console.log('Camera wrapper layout updated:', layout);
|
|
209
195
|
}}
|
|
210
196
|
>
|
|
211
|
-
<
|
|
212
|
-
style={styles.camera}
|
|
213
|
-
facing="back"
|
|
197
|
+
<Camera
|
|
214
198
|
ref={cameraRef}
|
|
215
|
-
|
|
199
|
+
style={StyleSheet.absoluteFill}
|
|
200
|
+
device={device}
|
|
201
|
+
isActive={true}
|
|
202
|
+
photo={true}
|
|
203
|
+
onInitialized={() => {
|
|
216
204
|
setIsReady(true);
|
|
217
|
-
console.log(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
348
|
-
fontSize:
|
|
349
|
-
color:
|
|
323
|
+
buttonText: {
|
|
324
|
+
fontSize: 16,
|
|
325
|
+
color: DEEP_BLACK,
|
|
350
326
|
fontWeight: '600',
|
|
351
327
|
},
|
|
352
|
-
});
|
|
328
|
+
});
|