react-native-expo-cropper 1.2.47 → 1.2.48
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/README.MD +19 -0
- package/dist/CustomCamera.js +116 -52
- package/dist/ImageCropper.js +17 -23
- package/package.json +2 -2
- package/src/CustomCamera.js +125 -72
- package/src/ImageCropper.js +33 -43
package/README.MD
CHANGED
|
@@ -32,6 +32,25 @@ EXPO SDK 54 -------------------------------------------------------------
|
|
|
32
32
|
},
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
## ✅ IMPORTANT (APK / production build)
|
|
36
|
+
|
|
37
|
+
Expo Go already contains native camera modules/permissions. In a **standalone APK** you must ensure the native config is present.
|
|
38
|
+
|
|
39
|
+
Add this to your `app.json` / `app.config.js` (example):
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"expo": {
|
|
44
|
+
"plugins": ["expo-camera", "expo-image-picker", "expo-media-library"],
|
|
45
|
+
"android": {
|
|
46
|
+
"permissions": ["CAMERA"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then rebuild the APK (EAS / prebuild). If the camera crashes only in release, it’s almost always missing permissions/plugin config or a device-specific camera processing issue.
|
|
53
|
+
|
|
35
54
|
------------------------------ DO THIS IN YOUR APP SCREEN CODE !!!!!!!!!! :-------------------------------
|
|
36
55
|
|
|
37
56
|
import ImageCropper from 'react-native-expo-cropper/src/ImageCropper';
|
package/dist/CustomCamera.js
CHANGED
|
@@ -9,7 +9,7 @@ var _react = _interopRequireWildcard(require("react"));
|
|
|
9
9
|
var _reactNative = require("react-native");
|
|
10
10
|
var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
|
|
11
11
|
var _expoCamera = require("expo-camera");
|
|
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
|
|
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 _t3 in e) "default" !== _t3 && {}.hasOwnProperty.call(e, _t3) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t3)) && (i.get || i.set) ? o(f, _t3, i) : f[_t3] = e[_t3]); 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; }
|
|
15
15
|
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
|
|
@@ -45,28 +45,22 @@ function CustomCamera(_ref) {
|
|
|
45
45
|
_useState6 = _slicedToArray(_useState5, 2),
|
|
46
46
|
hasPermission = _useState6[0],
|
|
47
47
|
setHasPermission = _useState6[1];
|
|
48
|
-
// Force a fresh native CameraView instance per "open" to avoid stale/cached preview.
|
|
49
|
-
var _useState7 = (0, _react.useState)(function () {
|
|
50
|
-
return "".concat(Date.now(), "-").concat(Math.random().toString(16).slice(2));
|
|
51
|
-
}),
|
|
52
|
-
_useState8 = _slicedToArray(_useState7, 1),
|
|
53
|
-
cameraViewMountKey = _useState8[0];
|
|
54
48
|
var cameraRef = (0, _react.useRef)(null);
|
|
55
49
|
var cameraWrapperRef = (0, _react.useRef)(null);
|
|
56
50
|
var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
57
|
-
var
|
|
51
|
+
var _useState7 = (0, _react.useState)({
|
|
58
52
|
width: 0,
|
|
59
53
|
height: 0,
|
|
60
54
|
x: 0,
|
|
61
55
|
y: 0
|
|
62
56
|
}),
|
|
57
|
+
_useState8 = _slicedToArray(_useState7, 2),
|
|
58
|
+
cameraWrapperLayout = _useState8[0],
|
|
59
|
+
setCameraWrapperLayout = _useState8[1];
|
|
60
|
+
var _useState9 = (0, _react.useState)(null),
|
|
63
61
|
_useState0 = _slicedToArray(_useState9, 2),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
var _useState1 = (0, _react.useState)(null),
|
|
67
|
-
_useState10 = _slicedToArray(_useState1, 2),
|
|
68
|
-
greenFrame = _useState10[0],
|
|
69
|
-
setGreenFrame = _useState10[1];
|
|
62
|
+
greenFrame = _useState0[0],
|
|
63
|
+
setGreenFrame = _useState0[1];
|
|
70
64
|
(0, _react.useEffect)(function () {
|
|
71
65
|
_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
72
66
|
var _yield$Camera$request, status;
|
|
@@ -85,6 +79,34 @@ function CustomCamera(_ref) {
|
|
|
85
79
|
}, _callee);
|
|
86
80
|
}))();
|
|
87
81
|
}, []);
|
|
82
|
+
var requestPermissionAgain = /*#__PURE__*/function () {
|
|
83
|
+
var _ref3 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
84
|
+
var _yield$Camera$request2, status, _t;
|
|
85
|
+
return _regenerator().w(function (_context2) {
|
|
86
|
+
while (1) switch (_context2.p = _context2.n) {
|
|
87
|
+
case 0:
|
|
88
|
+
_context2.p = 0;
|
|
89
|
+
_context2.n = 1;
|
|
90
|
+
return _expoCamera.Camera.requestCameraPermissionsAsync();
|
|
91
|
+
case 1:
|
|
92
|
+
_yield$Camera$request2 = _context2.v;
|
|
93
|
+
status = _yield$Camera$request2.status;
|
|
94
|
+
setHasPermission(status === 'granted');
|
|
95
|
+
_context2.n = 3;
|
|
96
|
+
break;
|
|
97
|
+
case 2:
|
|
98
|
+
_context2.p = 2;
|
|
99
|
+
_t = _context2.v;
|
|
100
|
+
setHasPermission(false);
|
|
101
|
+
case 3:
|
|
102
|
+
return _context2.a(2);
|
|
103
|
+
}
|
|
104
|
+
}, _callee2, null, [[0, 2]]);
|
|
105
|
+
}));
|
|
106
|
+
return function requestPermissionAgain() {
|
|
107
|
+
return _ref3.apply(this, arguments);
|
|
108
|
+
};
|
|
109
|
+
}();
|
|
88
110
|
|
|
89
111
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
90
112
|
var waitForRender = function waitForRender() {
|
|
@@ -150,25 +172,32 @@ function CustomCamera(_ref) {
|
|
|
150
172
|
}
|
|
151
173
|
}, [cameraWrapperLayout]);
|
|
152
174
|
var takePicture = /*#__PURE__*/function () {
|
|
153
|
-
var
|
|
154
|
-
var captureOptions, photo, capturedAspectRatio, greenFrameCoords,
|
|
155
|
-
return _regenerator().w(function (
|
|
156
|
-
while (1) switch (
|
|
175
|
+
var _ref4 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
|
|
176
|
+
var captureOptions, photo, capturedAspectRatio, greenFrameCoords, _t2;
|
|
177
|
+
return _regenerator().w(function (_context3) {
|
|
178
|
+
while (1) switch (_context3.p = _context3.n) {
|
|
157
179
|
case 0:
|
|
180
|
+
if (hasPermission) {
|
|
181
|
+
_context3.n = 1;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
_reactNative.Alert.alert("Permission requise", "Veuillez autoriser l'accès à la caméra pour prendre une photo.");
|
|
185
|
+
return _context3.a(2);
|
|
186
|
+
case 1:
|
|
158
187
|
if (!cameraRef.current) {
|
|
159
|
-
|
|
188
|
+
_context3.n = 8;
|
|
160
189
|
break;
|
|
161
190
|
}
|
|
162
|
-
|
|
191
|
+
_context3.p = 2;
|
|
163
192
|
// Show loading after a delay (using setImmediate for iOS compatibility)
|
|
164
193
|
waitForRender(5).then(function () {
|
|
165
194
|
setLoadingBeforeCapture(true);
|
|
166
195
|
});
|
|
167
196
|
|
|
168
197
|
// Wait a bit before taking picture (works on iOS)
|
|
169
|
-
|
|
198
|
+
_context3.n = 3;
|
|
170
199
|
return waitForRender(2);
|
|
171
|
-
case
|
|
200
|
+
case 3:
|
|
172
201
|
// ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
|
|
173
202
|
// Platform-specific optimizations for best quality
|
|
174
203
|
captureOptions = _objectSpread(_objectSpread({
|
|
@@ -178,7 +207,8 @@ function CustomCamera(_ref) {
|
|
|
178
207
|
shutterSound: false,
|
|
179
208
|
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
180
209
|
// This ensures pixel-perfect quality and prevents premature resizing
|
|
181
|
-
|
|
210
|
+
// NOTE: Some Android devices are unstable with skipProcessing; keep it off there.
|
|
211
|
+
skipProcessing: _reactNative.Platform.OS === 'ios',
|
|
182
212
|
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
183
213
|
exif: true
|
|
184
214
|
}, _reactNative.Platform.OS === 'ios' && {
|
|
@@ -196,16 +226,16 @@ function CustomCamera(_ref) {
|
|
|
196
226
|
height: cameraWrapperLayout.height
|
|
197
227
|
}
|
|
198
228
|
});
|
|
199
|
-
|
|
229
|
+
_context3.n = 4;
|
|
200
230
|
return cameraRef.current.takePictureAsync(captureOptions);
|
|
201
|
-
case
|
|
202
|
-
photo =
|
|
231
|
+
case 4:
|
|
232
|
+
photo = _context3.v;
|
|
203
233
|
if (!(!photo.width || !photo.height || photo.width === 0 || photo.height === 0)) {
|
|
204
|
-
|
|
234
|
+
_context3.n = 5;
|
|
205
235
|
break;
|
|
206
236
|
}
|
|
207
237
|
throw new Error("Invalid photo dimensions received from camera");
|
|
208
|
-
case
|
|
238
|
+
case 5:
|
|
209
239
|
capturedAspectRatio = photo.width / photo.height;
|
|
210
240
|
console.log("✅ Photo captured with maximum quality:", {
|
|
211
241
|
uri: photo.uri,
|
|
@@ -221,11 +251,11 @@ function CustomCamera(_ref) {
|
|
|
221
251
|
// Fallback to recalculation if, for some reason, state is not yet set
|
|
222
252
|
greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
223
253
|
if (greenFrameCoords) {
|
|
224
|
-
|
|
254
|
+
_context3.n = 6;
|
|
225
255
|
break;
|
|
226
256
|
}
|
|
227
257
|
throw new Error("Green frame coordinates not available");
|
|
228
|
-
case
|
|
258
|
+
case 6:
|
|
229
259
|
// ✅ Send photo URI and frame data to ImageCropper
|
|
230
260
|
// The photo maintains its native resolution and aspect ratio
|
|
231
261
|
// ImageCropper will handle display and cropping while preserving quality
|
|
@@ -238,26 +268,42 @@ function CustomCamera(_ref) {
|
|
|
238
268
|
}
|
|
239
269
|
});
|
|
240
270
|
setLoadingBeforeCapture(false);
|
|
241
|
-
|
|
271
|
+
_context3.n = 8;
|
|
242
272
|
break;
|
|
243
|
-
case 6:
|
|
244
|
-
_context2.p = 6;
|
|
245
|
-
_t = _context2.v;
|
|
246
|
-
console.error("❌ Error capturing photo:", _t);
|
|
247
|
-
setLoadingBeforeCapture(false);
|
|
248
|
-
_reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
|
|
249
273
|
case 7:
|
|
250
|
-
|
|
274
|
+
_context3.p = 7;
|
|
275
|
+
_t2 = _context3.v;
|
|
276
|
+
console.error("❌ Error capturing photo:", _t2);
|
|
277
|
+
setLoadingBeforeCapture(false);
|
|
278
|
+
_reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t2.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
|
|
279
|
+
case 8:
|
|
280
|
+
return _context3.a(2);
|
|
251
281
|
}
|
|
252
|
-
},
|
|
282
|
+
}, _callee3, null, [[2, 7]]);
|
|
253
283
|
}));
|
|
254
284
|
return function takePicture() {
|
|
255
|
-
return
|
|
285
|
+
return _ref4.apply(this, arguments);
|
|
256
286
|
};
|
|
257
287
|
}();
|
|
258
288
|
return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
|
|
259
289
|
style: styles.outerContainer
|
|
260
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
290
|
+
}, hasPermission === null && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
291
|
+
style: styles.permissionContainer
|
|
292
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
293
|
+
size: "large",
|
|
294
|
+
color: PRIMARY_GREEN
|
|
295
|
+
}), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
296
|
+
style: styles.permissionText
|
|
297
|
+
}, "Demande d'autorisation cam\xE9ra...")), hasPermission === false && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
298
|
+
style: styles.permissionContainer
|
|
299
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
300
|
+
style: styles.permissionText
|
|
301
|
+
}, "Autorisation cam\xE9ra refus\xE9e. Activez-la dans les param\xE8tres pour continuer."), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
302
|
+
style: styles.permissionButton,
|
|
303
|
+
onPress: requestPermissionAgain
|
|
304
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
|
|
305
|
+
style: styles.permissionButtonText
|
|
306
|
+
}, "R\xE9essayer"))), hasPermission === true && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
261
307
|
style: [styles.cameraWrapper, {
|
|
262
308
|
width: cameraPreviewWidth
|
|
263
309
|
}],
|
|
@@ -273,7 +319,6 @@ function CustomCamera(_ref) {
|
|
|
273
319
|
console.log("Camera wrapper layout updated:", layout);
|
|
274
320
|
}
|
|
275
321
|
}, /*#__PURE__*/_react["default"].createElement(_expoCamera.CameraView, {
|
|
276
|
-
key: cameraViewMountKey,
|
|
277
322
|
style: styles.camera,
|
|
278
323
|
facing: "back",
|
|
279
324
|
ref: cameraRef,
|
|
@@ -286,10 +331,9 @@ function CustomCamera(_ref) {
|
|
|
286
331
|
// This ensures preview matches what will be captured
|
|
287
332
|
}), loadingBeforeCapture && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
288
333
|
style: styles.loadingOverlay
|
|
289
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
resizeMode: "contain"
|
|
334
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
335
|
+
size: "large",
|
|
336
|
+
color: PRIMARY_GREEN
|
|
293
337
|
})), /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
294
338
|
style: styles.touchBlocker,
|
|
295
339
|
pointerEvents: "auto"
|
|
@@ -308,8 +352,8 @@ function CustomCamera(_ref) {
|
|
|
308
352
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
309
353
|
style: styles.button,
|
|
310
354
|
onPress: takePicture,
|
|
311
|
-
disabled: !isReady || loadingBeforeCapture
|
|
312
|
-
})));
|
|
355
|
+
disabled: !isReady || loadingBeforeCapture || !hasPermission
|
|
356
|
+
}))));
|
|
313
357
|
}
|
|
314
358
|
var PRIMARY_GREEN = '#198754';
|
|
315
359
|
var DEEP_BLACK = '#0B0B0B';
|
|
@@ -321,6 +365,30 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
321
365
|
justifyContent: 'center',
|
|
322
366
|
alignItems: 'center'
|
|
323
367
|
},
|
|
368
|
+
permissionContainer: {
|
|
369
|
+
flex: 1,
|
|
370
|
+
width: '100%',
|
|
371
|
+
justifyContent: 'center',
|
|
372
|
+
alignItems: 'center',
|
|
373
|
+
paddingHorizontal: 24
|
|
374
|
+
},
|
|
375
|
+
permissionText: {
|
|
376
|
+
marginTop: 12,
|
|
377
|
+
fontSize: 14,
|
|
378
|
+
color: GLOW_WHITE,
|
|
379
|
+
textAlign: 'center'
|
|
380
|
+
},
|
|
381
|
+
permissionButton: {
|
|
382
|
+
marginTop: 16,
|
|
383
|
+
backgroundColor: PRIMARY_GREEN,
|
|
384
|
+
paddingHorizontal: 16,
|
|
385
|
+
paddingVertical: 10,
|
|
386
|
+
borderRadius: 8
|
|
387
|
+
},
|
|
388
|
+
permissionButtonText: {
|
|
389
|
+
color: 'white',
|
|
390
|
+
fontWeight: '600'
|
|
391
|
+
},
|
|
324
392
|
cameraWrapper: {
|
|
325
393
|
aspectRatio: 9 / 16,
|
|
326
394
|
borderRadius: 30,
|
|
@@ -343,10 +411,6 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
343
411
|
justifyContent: 'center',
|
|
344
412
|
alignItems: 'center'
|
|
345
413
|
}),
|
|
346
|
-
loadingGif: {
|
|
347
|
-
width: 100,
|
|
348
|
-
height: 100
|
|
349
|
-
},
|
|
350
414
|
touchBlocker: _objectSpread(_objectSpread({}, _reactNative.StyleSheet.absoluteFillObject), {}, {
|
|
351
415
|
zIndex: 21,
|
|
352
416
|
backgroundColor: 'transparent'
|
package/dist/ImageCropper.js
CHANGED
|
@@ -49,6 +49,15 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
49
49
|
var _useWindowDimensions = (0, _reactNative.useWindowDimensions)(),
|
|
50
50
|
windowWidth = _useWindowDimensions.width;
|
|
51
51
|
var cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
|
|
52
|
+
var guessExtensionFromUri = function guessExtensionFromUri(uri) {
|
|
53
|
+
if (!uri || typeof uri !== 'string') return null;
|
|
54
|
+
var match = uri.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
var ext = match[1].toLowerCase();
|
|
57
|
+
// Keep only common image extensions
|
|
58
|
+
if (['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'].includes(ext)) return ext;
|
|
59
|
+
return null;
|
|
60
|
+
};
|
|
52
61
|
var _useState = (0, _react.useState)(null),
|
|
53
62
|
_useState2 = _slicedToArray(_useState, 2),
|
|
54
63
|
image = _useState2[0],
|
|
@@ -253,7 +262,6 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
253
262
|
var rotationInProgressRef = (0, _react.useRef)(false); // block duplicate taps immediately
|
|
254
263
|
var lastValidPosition = (0, _react.useRef)(null);
|
|
255
264
|
var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
|
|
256
|
-
var openCameraTimeoutRef = (0, _react.useRef)(null);
|
|
257
265
|
|
|
258
266
|
// ✅ NEW ARCH: mobile does NOT export the final crop.
|
|
259
267
|
|
|
@@ -262,17 +270,8 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
262
270
|
var enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
|
|
263
271
|
|
|
264
272
|
(0, _react.useEffect)(function () {
|
|
265
|
-
// If a previous open was scheduled, cancel it.
|
|
266
|
-
if (openCameraTimeoutRef.current) {
|
|
267
|
-
clearTimeout(openCameraTimeoutRef.current);
|
|
268
|
-
openCameraTimeoutRef.current = null;
|
|
269
|
-
}
|
|
270
273
|
if (openCameraFirst) {
|
|
271
|
-
|
|
272
|
-
openCameraTimeoutRef.current = setTimeout(function () {
|
|
273
|
-
setShowCustomCamera(true);
|
|
274
|
-
openCameraTimeoutRef.current = null;
|
|
275
|
-
}, 800);
|
|
274
|
+
setShowCustomCamera(true);
|
|
276
275
|
} else if (initialImage) {
|
|
277
276
|
setImage(initialImage);
|
|
278
277
|
sourceImageUri.current = initialImage;
|
|
@@ -286,12 +285,6 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
286
285
|
hasInitializedCropBox.current = false;
|
|
287
286
|
imageSource.current = null;
|
|
288
287
|
}
|
|
289
|
-
return function () {
|
|
290
|
-
if (openCameraTimeoutRef.current) {
|
|
291
|
-
clearTimeout(openCameraTimeoutRef.current);
|
|
292
|
-
openCameraTimeoutRef.current = null;
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
288
|
}, [openCameraFirst, initialImage]);
|
|
296
289
|
|
|
297
290
|
// ✅ REFACTORISATION : Stocker uniquement les dimensions originales (pas de calcul théorique)
|
|
@@ -1494,7 +1487,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1494
1487
|
}, "Reset")), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1495
1488
|
style: _ImageCropperStyles["default"].button,
|
|
1496
1489
|
onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
1497
|
-
var _cameraFrameData$curr4, actualImageWidth, actualImageHeight, isCoverMode, captured, layout, contentRect, displayedWidth, displayedHeight, scale, coverOffsetX, coverOffsetY, scaledWidth, scaledHeight, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, name, _t2;
|
|
1490
|
+
var _cameraFrameData$curr4, actualImageWidth, actualImageHeight, isCoverMode, captured, layout, contentRect, displayedWidth, displayedHeight, scale, coverOffsetX, coverOffsetY, scaledWidth, scaledHeight, originalUri, cropMeta, imagePoints, minX, minY, maxX, maxY, cropX, cropY, cropEndX, cropEndY, cropWidth, cropHeight, bbox, polygon, ext, safeExt, name, _t2;
|
|
1498
1491
|
return _regenerator().w(function (_context2) {
|
|
1499
1492
|
while (1) switch (_context2.p = _context2.n) {
|
|
1500
1493
|
case 0:
|
|
@@ -1635,7 +1628,9 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1635
1628
|
console.error("Error computing crop meta:", cropError);
|
|
1636
1629
|
}
|
|
1637
1630
|
}
|
|
1638
|
-
|
|
1631
|
+
ext = guessExtensionFromUri(originalUri) || 'jpg';
|
|
1632
|
+
safeExt = ext === 'jpeg' ? 'jpg' : ext;
|
|
1633
|
+
name = "IMAGE_XTK_".concat(Date.now(), ".").concat(safeExt);
|
|
1639
1634
|
if (onConfirm) {
|
|
1640
1635
|
onConfirm(originalUri, name, cropMeta);
|
|
1641
1636
|
}
|
|
@@ -1688,10 +1683,9 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1688
1683
|
animationType: "fade"
|
|
1689
1684
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1690
1685
|
style: _ImageCropperStyles["default"].loadingOverlay
|
|
1691
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
resizeMode: "contain"
|
|
1686
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
1687
|
+
size: "large",
|
|
1688
|
+
color: PRIMARY_GREEN
|
|
1695
1689
|
}))));
|
|
1696
1690
|
};
|
|
1697
1691
|
var _default = exports["default"] = ImageCropper;
|
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.48",
|
|
4
4
|
"description": "Recadrage polygonal d'images.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "PCS AGRI",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"homepage": "https://github.com/pcsagri/react-native-expo-cropper#readme",
|
|
48
48
|
"scripts": {
|
|
49
49
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
50
|
-
"build": "
|
|
50
|
+
"build": "node ./node_modules/@babel/cli/bin/babel.js src --out-dir dist"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@babel/cli": "^7.28.3",
|
package/src/CustomCamera.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
TouchableOpacity,
|
|
7
7
|
Alert,
|
|
8
8
|
SafeAreaView,
|
|
9
|
-
Image,
|
|
10
9
|
Platform,
|
|
10
|
+
ActivityIndicator,
|
|
11
11
|
useWindowDimensions,
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
@@ -22,8 +22,6 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
22
22
|
const [isReady, setIsReady] = useState(false);
|
|
23
23
|
const [loadingBeforeCapture, setLoadingBeforeCapture] = useState(false);
|
|
24
24
|
const [hasPermission, setHasPermission] = useState(null);
|
|
25
|
-
// Force a fresh native CameraView instance per "open" to avoid stale/cached preview.
|
|
26
|
-
const [cameraViewMountKey] = useState(() => `${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
27
25
|
const cameraRef = useRef(null);
|
|
28
26
|
const cameraWrapperRef = useRef(null);
|
|
29
27
|
const insets = useSafeAreaInsets();
|
|
@@ -37,6 +35,15 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
37
35
|
})();
|
|
38
36
|
}, []);
|
|
39
37
|
|
|
38
|
+
const requestPermissionAgain = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const { status } = await Camera.requestCameraPermissionsAsync();
|
|
41
|
+
setHasPermission(status === 'granted');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
setHasPermission(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
40
47
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
41
48
|
const waitForRender = (cycles = 5) => {
|
|
42
49
|
return new Promise((resolve) => {
|
|
@@ -104,6 +111,14 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
104
111
|
}, [cameraWrapperLayout]);
|
|
105
112
|
|
|
106
113
|
const takePicture = async () => {
|
|
114
|
+
if (!hasPermission) {
|
|
115
|
+
Alert.alert(
|
|
116
|
+
"Permission requise",
|
|
117
|
+
"Veuillez autoriser l'accès à la caméra pour prendre une photo."
|
|
118
|
+
);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
if (cameraRef.current) {
|
|
108
123
|
try {
|
|
109
124
|
// Show loading after a delay (using setImmediate for iOS compatibility)
|
|
@@ -125,7 +140,8 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
125
140
|
|
|
126
141
|
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
127
142
|
// This ensures pixel-perfect quality and prevents premature resizing
|
|
128
|
-
|
|
143
|
+
// NOTE: Some Android devices are unstable with skipProcessing; keep it off there.
|
|
144
|
+
skipProcessing: Platform.OS === 'ios',
|
|
129
145
|
|
|
130
146
|
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
131
147
|
exif: true,
|
|
@@ -200,71 +216,88 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
200
216
|
|
|
201
217
|
return (
|
|
202
218
|
<SafeAreaView style={styles.outerContainer}>
|
|
203
|
-
|
|
204
|
-
style={
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
setIsReady(true);
|
|
224
|
-
console.log("✅ Camera ready - Maximum quality capture enabled");
|
|
225
|
-
}}
|
|
226
|
-
// ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
|
|
227
|
-
// The wrapper constrains the view, but capture uses full sensor resolution
|
|
228
|
-
// This ensures preview matches what will be captured
|
|
229
|
-
/>
|
|
230
|
-
|
|
231
|
-
{/* Loading overlay */}
|
|
232
|
-
{loadingBeforeCapture && (
|
|
233
|
-
<>
|
|
234
|
-
<View style={styles.loadingOverlay}>
|
|
235
|
-
<Image
|
|
236
|
-
source={require('../src/assets/loadingCamera.gif')}
|
|
237
|
-
style={styles.loadingGif}
|
|
238
|
-
resizeMode="contain"
|
|
239
|
-
/>
|
|
240
|
-
</View>
|
|
241
|
-
<View style={styles.touchBlocker} pointerEvents="auto" />
|
|
242
|
-
</>
|
|
243
|
-
)}
|
|
244
|
-
|
|
245
|
-
{/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
|
|
246
|
-
{greenFrame && (
|
|
219
|
+
{hasPermission === null && (
|
|
220
|
+
<View style={styles.permissionContainer}>
|
|
221
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
222
|
+
<Text style={styles.permissionText}>Demande d'autorisation caméra...</Text>
|
|
223
|
+
</View>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{hasPermission === false && (
|
|
227
|
+
<View style={styles.permissionContainer}>
|
|
228
|
+
<Text style={styles.permissionText}>
|
|
229
|
+
Autorisation caméra refusée. Activez-la dans les paramètres pour continuer.
|
|
230
|
+
</Text>
|
|
231
|
+
<TouchableOpacity style={styles.permissionButton} onPress={requestPermissionAgain}>
|
|
232
|
+
<Text style={styles.permissionButtonText}>Réessayer</Text>
|
|
233
|
+
</TouchableOpacity>
|
|
234
|
+
</View>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{hasPermission === true && (
|
|
238
|
+
<>
|
|
247
239
|
<View
|
|
248
|
-
style={[
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
width:
|
|
254
|
-
height:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
240
|
+
style={[styles.cameraWrapper, { width: cameraPreviewWidth }]}
|
|
241
|
+
ref={cameraWrapperRef}
|
|
242
|
+
onLayout={(e) => {
|
|
243
|
+
const layout = e.nativeEvent.layout;
|
|
244
|
+
setCameraWrapperLayout({
|
|
245
|
+
width: layout.width,
|
|
246
|
+
height: layout.height,
|
|
247
|
+
x: layout.x,
|
|
248
|
+
y: layout.y
|
|
249
|
+
});
|
|
250
|
+
console.log("Camera wrapper layout updated:", layout);
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<CameraView
|
|
254
|
+
style={styles.camera}
|
|
255
|
+
facing="back"
|
|
256
|
+
ref={cameraRef}
|
|
257
|
+
onCameraReady={() => {
|
|
258
|
+
setIsReady(true);
|
|
259
|
+
console.log("✅ Camera ready - Maximum quality capture enabled");
|
|
260
|
+
}}
|
|
261
|
+
// ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
|
|
262
|
+
// The wrapper constrains the view, but capture uses full sensor resolution
|
|
263
|
+
// This ensures preview matches what will be captured
|
|
264
|
+
/>
|
|
265
|
+
|
|
266
|
+
{/* Loading overlay */}
|
|
267
|
+
{loadingBeforeCapture && (
|
|
268
|
+
<>
|
|
269
|
+
<View style={styles.loadingOverlay}>
|
|
270
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
271
|
+
</View>
|
|
272
|
+
<View style={styles.touchBlocker} pointerEvents="auto" />
|
|
273
|
+
</>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
|
|
277
|
+
{greenFrame && (
|
|
278
|
+
<View
|
|
279
|
+
style={[
|
|
280
|
+
styles.scanFrame,
|
|
281
|
+
{
|
|
282
|
+
left: greenFrame.x,
|
|
283
|
+
top: greenFrame.y,
|
|
284
|
+
width: greenFrame.width,
|
|
285
|
+
height: greenFrame.height,
|
|
286
|
+
},
|
|
287
|
+
]}
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
290
|
+
</View>
|
|
291
|
+
|
|
292
|
+
<View style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
|
|
293
|
+
<TouchableOpacity
|
|
294
|
+
style={styles.button}
|
|
295
|
+
onPress={takePicture}
|
|
296
|
+
disabled={!isReady || loadingBeforeCapture || !hasPermission}
|
|
297
|
+
/>
|
|
298
|
+
</View>
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
268
301
|
</SafeAreaView>
|
|
269
302
|
);
|
|
270
303
|
}
|
|
@@ -280,6 +313,30 @@ const styles = StyleSheet.create({
|
|
|
280
313
|
justifyContent: 'center',
|
|
281
314
|
alignItems: 'center',
|
|
282
315
|
},
|
|
316
|
+
permissionContainer: {
|
|
317
|
+
flex: 1,
|
|
318
|
+
width: '100%',
|
|
319
|
+
justifyContent: 'center',
|
|
320
|
+
alignItems: 'center',
|
|
321
|
+
paddingHorizontal: 24,
|
|
322
|
+
},
|
|
323
|
+
permissionText: {
|
|
324
|
+
marginTop: 12,
|
|
325
|
+
fontSize: 14,
|
|
326
|
+
color: GLOW_WHITE,
|
|
327
|
+
textAlign: 'center',
|
|
328
|
+
},
|
|
329
|
+
permissionButton: {
|
|
330
|
+
marginTop: 16,
|
|
331
|
+
backgroundColor: PRIMARY_GREEN,
|
|
332
|
+
paddingHorizontal: 16,
|
|
333
|
+
paddingVertical: 10,
|
|
334
|
+
borderRadius: 8,
|
|
335
|
+
},
|
|
336
|
+
permissionButtonText: {
|
|
337
|
+
color: 'white',
|
|
338
|
+
fontWeight: '600',
|
|
339
|
+
},
|
|
283
340
|
cameraWrapper: {
|
|
284
341
|
aspectRatio: 9 / 16,
|
|
285
342
|
borderRadius: 30,
|
|
@@ -305,10 +362,6 @@ const styles = StyleSheet.create({
|
|
|
305
362
|
justifyContent: 'center',
|
|
306
363
|
alignItems: 'center',
|
|
307
364
|
},
|
|
308
|
-
loadingGif: {
|
|
309
|
-
width: 100,
|
|
310
|
-
height: 100,
|
|
311
|
-
},
|
|
312
365
|
touchBlocker: {
|
|
313
366
|
...StyleSheet.absoluteFillObject,
|
|
314
367
|
zIndex: 21,
|
package/src/ImageCropper.js
CHANGED
|
@@ -17,6 +17,16 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
17
17
|
const { width: windowWidth } = useWindowDimensions();
|
|
18
18
|
const cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
|
|
19
19
|
|
|
20
|
+
const guessExtensionFromUri = (uri) => {
|
|
21
|
+
if (!uri || typeof uri !== 'string') return null;
|
|
22
|
+
const match = uri.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const ext = match[1].toLowerCase();
|
|
25
|
+
// Keep only common image extensions
|
|
26
|
+
if (['jpg', 'jpeg', 'png', 'webp', 'heic', 'heif'].includes(ext)) return ext;
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
|
|
20
30
|
const [image, setImage] = useState(null);
|
|
21
31
|
const [points, setPoints] = useState([]);
|
|
22
32
|
const [showResult, setShowResult] = useState(false);
|
|
@@ -141,7 +151,6 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
141
151
|
const rotationInProgressRef = useRef(false); // block duplicate taps immediately
|
|
142
152
|
const lastValidPosition = useRef(null);
|
|
143
153
|
const insets = useSafeAreaInsets();
|
|
144
|
-
const openCameraTimeoutRef = useRef(null);
|
|
145
154
|
|
|
146
155
|
// ✅ NEW ARCH: mobile does NOT export the final crop.
|
|
147
156
|
|
|
@@ -154,39 +163,22 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
|
|
|
154
163
|
|
|
155
164
|
|
|
156
165
|
useEffect(() => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// This ensures the crop box will be automatically initialized
|
|
174
|
-
setPoints([]);
|
|
175
|
-
rotationAngle.current = 0;
|
|
176
|
-
// Clear camera frame data for gallery images
|
|
177
|
-
cameraFrameData.current = null;
|
|
178
|
-
// ✅ CRITICAL: Reset initialization guard for new image
|
|
179
|
-
hasInitializedCropBox.current = false;
|
|
180
|
-
imageSource.current = null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return () => {
|
|
184
|
-
if (openCameraTimeoutRef.current) {
|
|
185
|
-
clearTimeout(openCameraTimeoutRef.current);
|
|
186
|
-
openCameraTimeoutRef.current = null;
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
}, [openCameraFirst, initialImage]);
|
|
166
|
+
if (openCameraFirst) {
|
|
167
|
+
setShowCustomCamera(true);
|
|
168
|
+
} else if (initialImage) {
|
|
169
|
+
setImage(initialImage);
|
|
170
|
+
sourceImageUri.current = initialImage;
|
|
171
|
+
// ✅ CRITICAL: Reset points when loading a new image from gallery
|
|
172
|
+
// This ensures the crop box will be automatically initialized
|
|
173
|
+
setPoints([]);
|
|
174
|
+
rotationAngle.current = 0;
|
|
175
|
+
// Clear camera frame data for gallery images
|
|
176
|
+
cameraFrameData.current = null;
|
|
177
|
+
// ✅ CRITICAL: Reset initialization guard for new image
|
|
178
|
+
hasInitializedCropBox.current = false;
|
|
179
|
+
imageSource.current = null;
|
|
180
|
+
}
|
|
181
|
+
}, [openCameraFirst, initialImage]);
|
|
190
182
|
|
|
191
183
|
|
|
192
184
|
// ✅ REFACTORISATION : Stocker uniquement les dimensions originales (pas de calcul théorique)
|
|
@@ -1392,7 +1384,9 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1392
1384
|
}
|
|
1393
1385
|
}
|
|
1394
1386
|
|
|
1395
|
-
const
|
|
1387
|
+
const ext = guessExtensionFromUri(originalUri) || 'jpg';
|
|
1388
|
+
const safeExt = ext === 'jpeg' ? 'jpg' : ext;
|
|
1389
|
+
const name = `IMAGE_XTK_${Date.now()}.${safeExt}`;
|
|
1396
1390
|
if (onConfirm) {
|
|
1397
1391
|
onConfirm(originalUri, name, cropMeta);
|
|
1398
1392
|
}
|
|
@@ -1449,14 +1443,10 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1449
1443
|
)}
|
|
1450
1444
|
|
|
1451
1445
|
<Modal visible={isLoading} transparent animationType="fade">
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
resizeMode="contain"
|
|
1457
|
-
/>
|
|
1458
|
-
</View>
|
|
1459
|
-
</Modal>
|
|
1446
|
+
<View style={styles.loadingOverlay}>
|
|
1447
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
1448
|
+
</View>
|
|
1449
|
+
</Modal>
|
|
1460
1450
|
</View>
|
|
1461
1451
|
);
|
|
1462
1452
|
};
|