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