react-native-expo-cropper 1.2.46 → 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 +109 -38
- package/dist/ImageCropper.js +16 -32
- package/package.json +2 -2
- package/src/CustomCamera.js +125 -69
- package/src/ImageCropper.js +17 -17
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; }
|
|
@@ -79,6 +79,34 @@ function CustomCamera(_ref) {
|
|
|
79
79
|
}, _callee);
|
|
80
80
|
}))();
|
|
81
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
|
+
}();
|
|
82
110
|
|
|
83
111
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
84
112
|
var waitForRender = function waitForRender() {
|
|
@@ -144,25 +172,32 @@ function CustomCamera(_ref) {
|
|
|
144
172
|
}
|
|
145
173
|
}, [cameraWrapperLayout]);
|
|
146
174
|
var takePicture = /*#__PURE__*/function () {
|
|
147
|
-
var
|
|
148
|
-
var captureOptions, photo, capturedAspectRatio, greenFrameCoords,
|
|
149
|
-
return _regenerator().w(function (
|
|
150
|
-
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) {
|
|
151
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:
|
|
152
187
|
if (!cameraRef.current) {
|
|
153
|
-
|
|
188
|
+
_context3.n = 8;
|
|
154
189
|
break;
|
|
155
190
|
}
|
|
156
|
-
|
|
191
|
+
_context3.p = 2;
|
|
157
192
|
// Show loading after a delay (using setImmediate for iOS compatibility)
|
|
158
193
|
waitForRender(5).then(function () {
|
|
159
194
|
setLoadingBeforeCapture(true);
|
|
160
195
|
});
|
|
161
196
|
|
|
162
197
|
// Wait a bit before taking picture (works on iOS)
|
|
163
|
-
|
|
198
|
+
_context3.n = 3;
|
|
164
199
|
return waitForRender(2);
|
|
165
|
-
case
|
|
200
|
+
case 3:
|
|
166
201
|
// ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
|
|
167
202
|
// Platform-specific optimizations for best quality
|
|
168
203
|
captureOptions = _objectSpread(_objectSpread({
|
|
@@ -172,7 +207,8 @@ function CustomCamera(_ref) {
|
|
|
172
207
|
shutterSound: false,
|
|
173
208
|
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
174
209
|
// This ensures pixel-perfect quality and prevents premature resizing
|
|
175
|
-
|
|
210
|
+
// NOTE: Some Android devices are unstable with skipProcessing; keep it off there.
|
|
211
|
+
skipProcessing: _reactNative.Platform.OS === 'ios',
|
|
176
212
|
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
177
213
|
exif: true
|
|
178
214
|
}, _reactNative.Platform.OS === 'ios' && {
|
|
@@ -190,16 +226,16 @@ function CustomCamera(_ref) {
|
|
|
190
226
|
height: cameraWrapperLayout.height
|
|
191
227
|
}
|
|
192
228
|
});
|
|
193
|
-
|
|
229
|
+
_context3.n = 4;
|
|
194
230
|
return cameraRef.current.takePictureAsync(captureOptions);
|
|
195
|
-
case
|
|
196
|
-
photo =
|
|
231
|
+
case 4:
|
|
232
|
+
photo = _context3.v;
|
|
197
233
|
if (!(!photo.width || !photo.height || photo.width === 0 || photo.height === 0)) {
|
|
198
|
-
|
|
234
|
+
_context3.n = 5;
|
|
199
235
|
break;
|
|
200
236
|
}
|
|
201
237
|
throw new Error("Invalid photo dimensions received from camera");
|
|
202
|
-
case
|
|
238
|
+
case 5:
|
|
203
239
|
capturedAspectRatio = photo.width / photo.height;
|
|
204
240
|
console.log("✅ Photo captured with maximum quality:", {
|
|
205
241
|
uri: photo.uri,
|
|
@@ -215,11 +251,11 @@ function CustomCamera(_ref) {
|
|
|
215
251
|
// Fallback to recalculation if, for some reason, state is not yet set
|
|
216
252
|
greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
|
|
217
253
|
if (greenFrameCoords) {
|
|
218
|
-
|
|
254
|
+
_context3.n = 6;
|
|
219
255
|
break;
|
|
220
256
|
}
|
|
221
257
|
throw new Error("Green frame coordinates not available");
|
|
222
|
-
case
|
|
258
|
+
case 6:
|
|
223
259
|
// ✅ Send photo URI and frame data to ImageCropper
|
|
224
260
|
// The photo maintains its native resolution and aspect ratio
|
|
225
261
|
// ImageCropper will handle display and cropping while preserving quality
|
|
@@ -232,26 +268,42 @@ function CustomCamera(_ref) {
|
|
|
232
268
|
}
|
|
233
269
|
});
|
|
234
270
|
setLoadingBeforeCapture(false);
|
|
235
|
-
|
|
271
|
+
_context3.n = 8;
|
|
236
272
|
break;
|
|
237
|
-
case 6:
|
|
238
|
-
_context2.p = 6;
|
|
239
|
-
_t = _context2.v;
|
|
240
|
-
console.error("❌ Error capturing photo:", _t);
|
|
241
|
-
setLoadingBeforeCapture(false);
|
|
242
|
-
_reactNative.Alert.alert("Erreur", "Impossible de capturer la photo: ".concat(_t.message || "Erreur inconnue", ". Veuillez r\xE9essayer."));
|
|
243
273
|
case 7:
|
|
244
|
-
|
|
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);
|
|
245
281
|
}
|
|
246
|
-
},
|
|
282
|
+
}, _callee3, null, [[2, 7]]);
|
|
247
283
|
}));
|
|
248
284
|
return function takePicture() {
|
|
249
|
-
return
|
|
285
|
+
return _ref4.apply(this, arguments);
|
|
250
286
|
};
|
|
251
287
|
}();
|
|
252
288
|
return /*#__PURE__*/_react["default"].createElement(_reactNative.SafeAreaView, {
|
|
253
289
|
style: styles.outerContainer
|
|
254
|
-
}, /*#__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, {
|
|
255
307
|
style: [styles.cameraWrapper, {
|
|
256
308
|
width: cameraPreviewWidth
|
|
257
309
|
}],
|
|
@@ -279,10 +331,9 @@ function CustomCamera(_ref) {
|
|
|
279
331
|
// This ensures preview matches what will be captured
|
|
280
332
|
}), loadingBeforeCapture && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
281
333
|
style: styles.loadingOverlay
|
|
282
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
resizeMode: "contain"
|
|
334
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
335
|
+
size: "large",
|
|
336
|
+
color: PRIMARY_GREEN
|
|
286
337
|
})), /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
287
338
|
style: styles.touchBlocker,
|
|
288
339
|
pointerEvents: "auto"
|
|
@@ -301,8 +352,8 @@ function CustomCamera(_ref) {
|
|
|
301
352
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
302
353
|
style: styles.button,
|
|
303
354
|
onPress: takePicture,
|
|
304
|
-
disabled: !isReady || loadingBeforeCapture
|
|
305
|
-
})));
|
|
355
|
+
disabled: !isReady || loadingBeforeCapture || !hasPermission
|
|
356
|
+
}))));
|
|
306
357
|
}
|
|
307
358
|
var PRIMARY_GREEN = '#198754';
|
|
308
359
|
var DEEP_BLACK = '#0B0B0B';
|
|
@@ -314,6 +365,30 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
314
365
|
justifyContent: 'center',
|
|
315
366
|
alignItems: 'center'
|
|
316
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
|
+
},
|
|
317
392
|
cameraWrapper: {
|
|
318
393
|
aspectRatio: 9 / 16,
|
|
319
394
|
borderRadius: 30,
|
|
@@ -336,10 +411,6 @@ var styles = _reactNative.StyleSheet.create({
|
|
|
336
411
|
justifyContent: 'center',
|
|
337
412
|
alignItems: 'center'
|
|
338
413
|
}),
|
|
339
|
-
loadingGif: {
|
|
340
|
-
width: 100,
|
|
341
|
-
height: 100
|
|
342
|
-
},
|
|
343
414
|
touchBlocker: _objectSpread(_objectSpread({}, _reactNative.StyleSheet.absoluteFillObject), {}, {
|
|
344
415
|
zIndex: 21,
|
|
345
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],
|
|
@@ -1305,31 +1314,6 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1305
1314
|
style: _ImageCropperStyles["default"].container
|
|
1306
1315
|
}, showCustomCamera ? /*#__PURE__*/_react["default"].createElement(_CustomCamera["default"], {
|
|
1307
1316
|
onPhotoCaptured: function onPhotoCaptured(uri, frameData) {
|
|
1308
|
-
// ✅ Reset refs for new image so second (and later) photos don't use first image's layout (fixes white screen on some devices)
|
|
1309
|
-
originalImageDimensions.current = {
|
|
1310
|
-
width: 0,
|
|
1311
|
-
height: 0
|
|
1312
|
-
};
|
|
1313
|
-
imageDisplayRect.current = {
|
|
1314
|
-
x: 0,
|
|
1315
|
-
y: 0,
|
|
1316
|
-
width: 0,
|
|
1317
|
-
height: 0
|
|
1318
|
-
};
|
|
1319
|
-
displayedImageLayout.current = {
|
|
1320
|
-
x: 0,
|
|
1321
|
-
y: 0,
|
|
1322
|
-
width: 0,
|
|
1323
|
-
height: 0
|
|
1324
|
-
};
|
|
1325
|
-
imageMeasure.current = {
|
|
1326
|
-
x: 0,
|
|
1327
|
-
y: 0,
|
|
1328
|
-
width: 0,
|
|
1329
|
-
height: 0
|
|
1330
|
-
};
|
|
1331
|
-
sourceImageUri.current = uri;
|
|
1332
|
-
|
|
1333
1317
|
// ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
|
|
1334
1318
|
if (frameData && frameData.greenFrame) {
|
|
1335
1319
|
cameraFrameData.current = {
|
|
@@ -1425,7 +1409,6 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1425
1409
|
return hasMovement;
|
|
1426
1410
|
}
|
|
1427
1411
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.Image, {
|
|
1428
|
-
key: image,
|
|
1429
1412
|
source: {
|
|
1430
1413
|
uri: image
|
|
1431
1414
|
},
|
|
@@ -1504,7 +1487,7 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1504
1487
|
}, "Reset")), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
|
|
1505
1488
|
style: _ImageCropperStyles["default"].button,
|
|
1506
1489
|
onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
1507
|
-
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;
|
|
1508
1491
|
return _regenerator().w(function (_context2) {
|
|
1509
1492
|
while (1) switch (_context2.p = _context2.n) {
|
|
1510
1493
|
case 0:
|
|
@@ -1645,7 +1628,9 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1645
1628
|
console.error("Error computing crop meta:", cropError);
|
|
1646
1629
|
}
|
|
1647
1630
|
}
|
|
1648
|
-
|
|
1631
|
+
ext = guessExtensionFromUri(originalUri) || 'jpg';
|
|
1632
|
+
safeExt = ext === 'jpeg' ? 'jpg' : ext;
|
|
1633
|
+
name = "IMAGE_XTK_".concat(Date.now(), ".").concat(safeExt);
|
|
1649
1634
|
if (onConfirm) {
|
|
1650
1635
|
onConfirm(originalUri, name, cropMeta);
|
|
1651
1636
|
}
|
|
@@ -1698,10 +1683,9 @@ var ImageCropper = function ImageCropper(_ref) {
|
|
|
1698
1683
|
animationType: "fade"
|
|
1699
1684
|
}, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
|
|
1700
1685
|
style: _ImageCropperStyles["default"].loadingOverlay
|
|
1701
|
-
}, /*#__PURE__*/_react["default"].createElement(_reactNative.
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
resizeMode: "contain"
|
|
1686
|
+
}, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
|
|
1687
|
+
size: "large",
|
|
1688
|
+
color: PRIMARY_GREEN
|
|
1705
1689
|
}))));
|
|
1706
1690
|
};
|
|
1707
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';
|
|
@@ -35,6 +35,15 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
35
35
|
})();
|
|
36
36
|
}, []);
|
|
37
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
|
+
|
|
38
47
|
// Helper function to wait for multiple render cycles (works on iOS)
|
|
39
48
|
const waitForRender = (cycles = 5) => {
|
|
40
49
|
return new Promise((resolve) => {
|
|
@@ -102,6 +111,14 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
102
111
|
}, [cameraWrapperLayout]);
|
|
103
112
|
|
|
104
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
|
+
|
|
105
122
|
if (cameraRef.current) {
|
|
106
123
|
try {
|
|
107
124
|
// Show loading after a delay (using setImmediate for iOS compatibility)
|
|
@@ -123,7 +140,8 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
123
140
|
|
|
124
141
|
// ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
|
|
125
142
|
// This ensures pixel-perfect quality and prevents premature resizing
|
|
126
|
-
|
|
143
|
+
// NOTE: Some Android devices are unstable with skipProcessing; keep it off there.
|
|
144
|
+
skipProcessing: Platform.OS === 'ios',
|
|
127
145
|
|
|
128
146
|
// Include EXIF metadata (orientation, camera settings, etc.)
|
|
129
147
|
exif: true,
|
|
@@ -198,70 +216,88 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
|
|
|
198
216
|
|
|
199
217
|
return (
|
|
200
218
|
<SafeAreaView style={styles.outerContainer}>
|
|
201
|
-
|
|
202
|
-
style={
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.log("✅ Camera ready - Maximum quality capture enabled");
|
|
222
|
-
}}
|
|
223
|
-
// ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
|
|
224
|
-
// The wrapper constrains the view, but capture uses full sensor resolution
|
|
225
|
-
// This ensures preview matches what will be captured
|
|
226
|
-
/>
|
|
227
|
-
|
|
228
|
-
{/* Loading overlay */}
|
|
229
|
-
{loadingBeforeCapture && (
|
|
230
|
-
<>
|
|
231
|
-
<View style={styles.loadingOverlay}>
|
|
232
|
-
<Image
|
|
233
|
-
source={require('../src/assets/loadingCamera.gif')}
|
|
234
|
-
style={styles.loadingGif}
|
|
235
|
-
resizeMode="contain"
|
|
236
|
-
/>
|
|
237
|
-
</View>
|
|
238
|
-
<View style={styles.touchBlocker} pointerEvents="auto" />
|
|
239
|
-
</>
|
|
240
|
-
)}
|
|
241
|
-
|
|
242
|
-
{/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
|
|
243
|
-
{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
|
+
<>
|
|
244
239
|
<View
|
|
245
|
-
style={[
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
width:
|
|
251
|
-
height:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
)}
|
|
265
301
|
</SafeAreaView>
|
|
266
302
|
);
|
|
267
303
|
}
|
|
@@ -277,6 +313,30 @@ const styles = StyleSheet.create({
|
|
|
277
313
|
justifyContent: 'center',
|
|
278
314
|
alignItems: 'center',
|
|
279
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
|
+
},
|
|
280
340
|
cameraWrapper: {
|
|
281
341
|
aspectRatio: 9 / 16,
|
|
282
342
|
borderRadius: 30,
|
|
@@ -302,10 +362,6 @@ const styles = StyleSheet.create({
|
|
|
302
362
|
justifyContent: 'center',
|
|
303
363
|
alignItems: 'center',
|
|
304
364
|
},
|
|
305
|
-
loadingGif: {
|
|
306
|
-
width: 100,
|
|
307
|
-
height: 100,
|
|
308
|
-
},
|
|
309
365
|
touchBlocker: {
|
|
310
366
|
...StyleSheet.absoluteFillObject,
|
|
311
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);
|
|
@@ -1100,13 +1110,6 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1100
1110
|
{showCustomCamera ? (
|
|
1101
1111
|
<CustomCamera
|
|
1102
1112
|
onPhotoCaptured={(uri, frameData) => {
|
|
1103
|
-
// ✅ Reset refs for new image so second (and later) photos don't use first image's layout (fixes white screen on some devices)
|
|
1104
|
-
originalImageDimensions.current = { width: 0, height: 0 };
|
|
1105
|
-
imageDisplayRect.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
1106
|
-
displayedImageLayout.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
1107
|
-
imageMeasure.current = { x: 0, y: 0, width: 0, height: 0 };
|
|
1108
|
-
sourceImageUri.current = uri;
|
|
1109
|
-
|
|
1110
1113
|
// ✅ CRITICAL FIX: Store green frame coordinates for coordinate conversion
|
|
1111
1114
|
if (frameData && frameData.greenFrame) {
|
|
1112
1115
|
cameraFrameData.current = {
|
|
@@ -1204,7 +1207,6 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1204
1207
|
}}
|
|
1205
1208
|
>
|
|
1206
1209
|
<Image
|
|
1207
|
-
key={image}
|
|
1208
1210
|
source={{ uri: image }}
|
|
1209
1211
|
style={styles.image}
|
|
1210
1212
|
resizeMode={cameraFrameData.current?.greenFrame ? 'cover' : 'contain'}
|
|
@@ -1382,7 +1384,9 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1382
1384
|
}
|
|
1383
1385
|
}
|
|
1384
1386
|
|
|
1385
|
-
const
|
|
1387
|
+
const ext = guessExtensionFromUri(originalUri) || 'jpg';
|
|
1388
|
+
const safeExt = ext === 'jpeg' ? 'jpg' : ext;
|
|
1389
|
+
const name = `IMAGE_XTK_${Date.now()}.${safeExt}`;
|
|
1386
1390
|
if (onConfirm) {
|
|
1387
1391
|
onConfirm(originalUri, name, cropMeta);
|
|
1388
1392
|
}
|
|
@@ -1439,14 +1443,10 @@ const rotatePreviewImage = async (degrees) => {
|
|
|
1439
1443
|
)}
|
|
1440
1444
|
|
|
1441
1445
|
<Modal visible={isLoading} transparent animationType="fade">
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
resizeMode="contain"
|
|
1447
|
-
/>
|
|
1448
|
-
</View>
|
|
1449
|
-
</Modal>
|
|
1446
|
+
<View style={styles.loadingOverlay}>
|
|
1447
|
+
<ActivityIndicator size="large" color={PRIMARY_GREEN} />
|
|
1448
|
+
</View>
|
|
1449
|
+
</Modal>
|
|
1450
1450
|
</View>
|
|
1451
1451
|
);
|
|
1452
1452
|
};
|