react-native-expo-cropper 1.2.39 → 1.2.41

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.
@@ -97,7 +97,7 @@ function CustomCamera(_ref) {
97
97
  // ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
98
98
  // The green frame should be calculated on the wrapper (as it's visually drawn there)
99
99
  // But we store it with wrapper dimensions so ImageCropper can map it correctly
100
- //
100
+ //
101
101
  // NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
102
102
  // This is handled in ImageCropper by using "cover" mode to match preview content
103
103
  var calculateGreenFrameCoordinates = function calculateGreenFrameCoordinates() {
@@ -35,12 +35,14 @@ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r)
35
35
  function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
36
36
  function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
37
37
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
38
+ var PRIMARY_GREEN = '#198754';
38
39
  var ImageCropper = function ImageCropper(_ref) {
39
40
  var _cameraFrameData$curr3;
40
41
  var onConfirm = _ref.onConfirm,
41
42
  openCameraFirst = _ref.openCameraFirst,
42
43
  initialImage = _ref.initialImage,
43
- addheight = _ref.addheight;
44
+ addheight = _ref.addheight,
45
+ rotationLabel = _ref.rotationLabel;
44
46
  var _useState = (0, _react.useState)(null),
45
47
  _useState2 = _slicedToArray(_useState, 2),
46
48
  image = _useState2[0],
@@ -242,6 +244,7 @@ var ImageCropper = function ImageCropper(_ref) {
242
244
  _useState20 = _slicedToArray(_useState19, 2),
243
245
  isRotating = _useState20[0],
244
246
  setIsRotating = _useState20[1];
247
+ var rotationInProgressRef = (0, _react.useRef)(false); // block duplicate taps immediately
245
248
  var lastValidPosition = (0, _react.useRef)(null);
246
249
  var insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
247
250
 
@@ -249,7 +252,7 @@ var ImageCropper = function ImageCropper(_ref) {
249
252
 
250
253
  // No view-shot / captureRef / bitmap masking on device.
251
254
  var enableMask = false;
252
- var enableRotation = false; // rotation would require careful coord transforms; keep off for pixel-perfect pipeline.
255
+ var enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
253
256
 
254
257
  (0, _react.useEffect)(function () {
255
258
  if (openCameraFirst) {
@@ -1212,45 +1215,54 @@ var ImageCropper = function ImageCropper(_ref) {
1212
1215
  return _regenerator().w(function (_context) {
1213
1216
  while (1) switch (_context.p = _context.n) {
1214
1217
  case 0:
1215
- if (!(!image || isRotating)) {
1218
+ if (image) {
1216
1219
  _context.n = 1;
1217
1220
  break;
1218
1221
  }
1219
1222
  return _context.a(2);
1220
1223
  case 1:
1224
+ if (!rotationInProgressRef.current) {
1225
+ _context.n = 2;
1226
+ break;
1227
+ }
1228
+ return _context.a(2);
1229
+ case 2:
1230
+ // block duplicate taps immediately (no re-render delay)
1231
+ rotationInProgressRef.current = true;
1221
1232
  setIsRotating(true);
1222
- _context.p = 2;
1223
- // ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
1224
- // L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
1233
+ _context.p = 3;
1225
1234
  rotationAngle.current = (rotationAngle.current + degrees) % 360;
1226
- _context.n = 3;
1235
+
1236
+ // Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
1237
+ _context.n = 4;
1227
1238
  return ImageManipulator.manipulateAsync(image, [{
1228
1239
  rotate: degrees
1229
1240
  }], {
1230
- compress: 1,
1231
- // Qualité maximale (pas de compression)
1232
- format: ImageManipulator.SaveFormat.PNG // Format sans perte
1241
+ compress: 0.92,
1242
+ format: ImageManipulator.SaveFormat.JPEG
1233
1243
  });
1234
- case 3:
1244
+ case 4:
1235
1245
  rotated = _context.v;
1236
- // Update image pour l'aperçu - onImageLayout will call initializeCropBox automatically
1246
+ setPoints([]);
1247
+ hasInitializedCropBox.current = false;
1237
1248
  setImage(rotated.uri);
1238
- console.log("Rotation applied (preview increment):", degrees, "degrees; accumulated:", rotationAngle.current);
1239
- _context.n = 5;
1249
+ console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
1250
+ _context.n = 6;
1240
1251
  break;
1241
- case 4:
1242
- _context.p = 4;
1252
+ case 5:
1253
+ _context.p = 5;
1243
1254
  _t = _context.v;
1244
1255
  console.error("Error rotating image:", _t);
1245
1256
  alert("Error rotating image");
1246
- case 5:
1247
- _context.p = 5;
1248
- setIsRotating(false);
1249
- return _context.f(5);
1250
1257
  case 6:
1258
+ _context.p = 6;
1259
+ rotationInProgressRef.current = false;
1260
+ setIsRotating(false);
1261
+ return _context.f(6);
1262
+ case 7:
1251
1263
  return _context.a(2);
1252
1264
  }
1253
- }, _callee, null, [[2, 4, 5, 6]]);
1265
+ }, _callee, null, [[3, 5, 6, 7]]);
1254
1266
  }));
1255
1267
  return function rotatePreviewImage(_x) {
1256
1268
  return _ref2.apply(this, arguments);
@@ -1411,17 +1423,42 @@ var ImageCropper = function ImageCropper(_ref) {
1411
1423
  fill: "white"
1412
1424
  });
1413
1425
  }));
1414
- }()))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1426
+ }())), isRotating && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1427
+ style: {
1428
+ position: 'absolute',
1429
+ left: 0,
1430
+ right: 0,
1431
+ top: 0,
1432
+ bottom: 0,
1433
+ backgroundColor: 'rgba(0,0,0,0.6)',
1434
+ justifyContent: 'center',
1435
+ alignItems: 'center'
1436
+ }
1437
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
1438
+ size: "large",
1439
+ color: PRIMARY_GREEN
1440
+ }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1441
+ style: {
1442
+ color: PRIMARY_GREEN,
1443
+ marginTop: 8,
1444
+ fontSize: 14
1445
+ }
1446
+ }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...'))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1415
1447
  style: [_ImageCropperStyles["default"].buttonContainerBelow, {
1416
1448
  paddingBottom: Math.max(insets.bottom, 16)
1417
1449
  }]
1418
1450
  }, _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1419
- style: _ImageCropperStyles["default"].rotationButton,
1451
+ style: [_ImageCropperStyles["default"].rotationButton, isRotating && {
1452
+ opacity: 0.7
1453
+ }],
1420
1454
  onPress: function onPress() {
1421
1455
  return enableRotation && rotatePreviewImage(90);
1422
1456
  },
1423
1457
  disabled: isRotating
1424
- }, /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1458
+ }, isRotating ? /*#__PURE__*/_react["default"].createElement(_reactNative.ActivityIndicator, {
1459
+ size: "small",
1460
+ color: "white"
1461
+ }) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1425
1462
  name: "sync",
1426
1463
  size: 24,
1427
1464
  color: "white"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-expo-cropper",
3
- "version": "1.2.39",
3
+ "version": "1.2.41",
4
4
  "description": "Recadrage polygonal d'images.",
5
5
  "main": "index.js",
6
6
  "author": "PCS AGRI",
@@ -52,24 +52,24 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
52
52
  // ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
53
53
  // The green frame should be calculated on the wrapper (as it's visually drawn there)
54
54
  // But we store it with wrapper dimensions so ImageCropper can map it correctly
55
- //
55
+ //
56
56
  // NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
57
57
  // This is handled in ImageCropper by using "cover" mode to match preview content
58
58
  const calculateGreenFrameCoordinates = () => {
59
59
  const wrapperWidth = cameraWrapperLayout.width;
60
60
  const wrapperHeight = cameraWrapperLayout.height;
61
-
61
+
62
62
  if (wrapperWidth === 0 || wrapperHeight === 0) {
63
63
  console.warn("Camera wrapper layout not ready, cannot calculate green frame");
64
64
  return null;
65
65
  }
66
-
66
+
67
67
  // ✅ Calculate green frame as percentage of WRAPPER
68
68
  const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
69
69
  const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
70
70
  const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
71
71
  const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
72
-
72
+
73
73
  const frameCoords = {
74
74
  x: frameX,
75
75
  y: frameY,
@@ -83,7 +83,7 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
83
83
  percentWidth: 85, // 85% of wrapper width
84
84
  percentHeight: 70 // 70% of wrapper height
85
85
  };
86
-
86
+
87
87
  console.log("✅ Green frame coordinates calculated:", frameCoords);
88
88
  return frameCoords;
89
89
  };
@@ -104,26 +104,26 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
104
104
  waitForRender(5).then(() => {
105
105
  setLoadingBeforeCapture(true);
106
106
  });
107
-
107
+
108
108
  // Wait a bit before taking picture (works on iOS)
109
109
  await waitForRender(2);
110
-
110
+
111
111
  // ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
112
112
  // Platform-specific optimizations for best quality
113
113
  const captureOptions = {
114
114
  // Maximum quality (0-1, 1 = best quality, no compression)
115
115
  quality: 1,
116
-
116
+
117
117
  // Disable shutter sound for better UX
118
118
  shutterSound: false,
119
-
119
+
120
120
  // ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
121
121
  // This ensures pixel-perfect quality and prevents premature resizing
122
122
  skipProcessing: true,
123
-
123
+
124
124
  // Include EXIF metadata (orientation, camera settings, etc.)
125
125
  exif: true,
126
-
126
+
127
127
  // ✅ Platform-specific optimizations
128
128
  ...(Platform.OS === 'ios' && {
129
129
  // iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
@@ -162,39 +162,39 @@ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
162
162
  // ✅ CRITICAL FIX: Use the same green frame coordinates that are used for rendering
163
163
  // Fallback to recalculation if, for some reason, state is not yet set
164
164
  const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
165
-
165
+
166
166
  if (!greenFrameCoords) {
167
167
  throw new Error("Green frame coordinates not available");
168
168
  }
169
-
169
+
170
170
  // ✅ Send photo URI and frame data to ImageCropper
171
171
  // The photo maintains its native resolution and aspect ratio
172
172
  // ImageCropper will handle display and cropping while preserving quality
173
173
  onPhotoCaptured(photo.uri, {
174
174
  greenFrame: greenFrameCoords,
175
- capturedImageSize: {
176
- width: photo.width,
175
+ capturedImageSize: {
176
+ width: photo.width,
177
177
  height: photo.height,
178
178
  aspectRatio: capturedAspectRatio
179
179
  }
180
180
  });
181
-
181
+
182
182
  setLoadingBeforeCapture(false);
183
183
  } catch (error) {
184
184
  console.error("❌ Error capturing photo:", error);
185
185
  setLoadingBeforeCapture(false);
186
186
  Alert.alert(
187
- "Erreur",
187
+ "Erreur",
188
188
  `Impossible de capturer la photo: ${error.message || "Erreur inconnue"}. Veuillez réessayer.`
189
189
  );
190
190
  }
191
191
  }
192
192
  };
193
-
193
+
194
194
 
195
195
  return (
196
196
  <SafeAreaView style={styles.outerContainer}>
197
- <View
197
+ <View
198
198
  style={styles.cameraWrapper}
199
199
  ref={cameraWrapperRef}
200
200
  onLayout={(e) => {
@@ -349,4 +349,4 @@ const styles = StyleSheet.create({
349
349
  color: GLOW_WHITE,
350
350
  fontWeight: '600',
351
351
  },
352
- });
352
+ });
@@ -1,6 +1,6 @@
1
1
  import styles from './ImageCropperStyles';
2
2
  import React, { useState, useRef, useEffect } from 'react';
3
- import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet } from 'react-native';
3
+ import { Modal, View, Image, Dimensions, TouchableOpacity, Animated, Text, Platform, SafeAreaView, PixelRatio, StyleSheet, ActivityIndicator } from 'react-native';
4
4
  import Svg, { Path, Circle } from 'react-native-svg';
5
5
  import CustomCamera from './CustomCamera';
6
6
  import * as ImageManipulator from 'expo-image-manipulator';
@@ -8,7 +8,8 @@ import { Ionicons } from '@expo/vector-icons';
8
8
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
9
  import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
10
10
 
11
- const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) => {
11
+ const PRIMARY_GREEN = '#198754';
12
+ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rotationLabel }) => {
12
13
 
13
14
 
14
15
  const [image, setImage] = useState(null);
@@ -132,6 +133,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
132
133
  const [isLoading, setIsLoading] = useState(false);
133
134
  const [showFullScreenCapture, setShowFullScreenCapture] = useState(false);
134
135
  const [isRotating, setIsRotating] = useState(false);
136
+ const rotationInProgressRef = useRef(false); // block duplicate taps immediately
135
137
  const lastValidPosition = useRef(null);
136
138
  const insets = useSafeAreaInsets();
137
139
 
@@ -139,7 +141,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
139
141
 
140
142
  // No view-shot / captureRef / bitmap masking on device.
141
143
  const enableMask = false;
142
- const enableRotation = false; // rotation would require careful coord transforms; keep off for pixel-perfect pipeline.
144
+ const enableRotation = true; // rotation resets crop box so it is re-initialized on rotated image.
143
145
 
144
146
 
145
147
 
@@ -1027,32 +1029,34 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1027
1029
 
1028
1030
  // ✅ REFACTORISATION : Stocker l'angle de rotation au lieu de modifier l'image immédiatement
1029
1031
  // La rotation sera appliquée uniquement lors du crop final pour éviter les interpolations multiples
1030
- const rotatePreviewImage = async (degrees) => {
1031
- if (!image || isRotating) return;
1032
+ const rotatePreviewImage = async (degrees) => {
1033
+ if (!image) return;
1034
+ if (rotationInProgressRef.current) return; // block duplicate taps immediately (no re-render delay)
1035
+ rotationInProgressRef.current = true;
1032
1036
  setIsRotating(true);
1033
-
1037
+
1034
1038
  try {
1035
- // ✅ CORRECTION : appliquer la rotation de façon incrémentale sur le fichier (pas cumulée sur un fichier déjà roté).
1036
- // L'ancienne version rotait l'image déjà rotée par l'angle total, ce qui donnait des rotations incorrectes (90 + 180 => 270).
1037
1039
  rotationAngle.current = (rotationAngle.current + degrees) % 360;
1038
1040
 
1041
+ // Use JPEG for preview rotation (faster than PNG for large images; quality 0.92 is fine for preview)
1039
1042
  const rotated = await ImageManipulator.manipulateAsync(
1040
1043
  image,
1041
1044
  [{ rotate: degrees }],
1042
- {
1043
- compress: 1, // Qualité maximale (pas de compression)
1044
- format: ImageManipulator.SaveFormat.PNG // Format sans perte
1045
+ {
1046
+ compress: 0.92,
1047
+ format: ImageManipulator.SaveFormat.JPEG,
1045
1048
  }
1046
1049
  );
1047
-
1048
- // Update image pour l'aperçu - onImageLayout will call initializeCropBox automatically
1050
+
1051
+ setPoints([]);
1052
+ hasInitializedCropBox.current = false;
1049
1053
  setImage(rotated.uri);
1050
-
1051
- console.log("Rotation applied (preview increment):", degrees, "degrees; accumulated:", rotationAngle.current);
1054
+ console.log("Rotation applied:", degrees, "degrees; accumulated:", rotationAngle.current);
1052
1055
  } catch (error) {
1053
1056
  console.error("Error rotating image:", error);
1054
1057
  alert("Error rotating image");
1055
1058
  } finally {
1059
+ rotationInProgressRef.current = false;
1056
1060
  setIsRotating(false);
1057
1061
  }
1058
1062
  };
@@ -1214,6 +1218,12 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1214
1218
  })()}
1215
1219
  </Svg>
1216
1220
  </View>
1221
+ {isRotating && (
1222
+ <View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
1223
+ <ActivityIndicator size="large" color={PRIMARY_GREEN} />
1224
+ <Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
1225
+ </View>
1226
+ )}
1217
1227
  </View>
1218
1228
  )}
1219
1229
 
@@ -1222,11 +1232,15 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage ,addheight}) =>
1222
1232
  <View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
1223
1233
  {Platform.OS === 'android' && (
1224
1234
  <TouchableOpacity
1225
- style={styles.rotationButton}
1235
+ style={[styles.rotationButton, isRotating && { opacity: 0.7 }]}
1226
1236
  onPress={() => enableRotation && rotatePreviewImage(90)}
1227
1237
  disabled={isRotating}
1228
1238
  >
1229
- <Ionicons name="sync" size={24} color="white" />
1239
+ {isRotating ? (
1240
+ <ActivityIndicator size="small" color="white" />
1241
+ ) : (
1242
+ <Ionicons name="sync" size={24} color="white" />
1243
+ )}
1230
1244
  </TouchableOpacity>
1231
1245
  )}
1232
1246