react-native-expo-cropper 1.2.48 → 1.2.49

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.
@@ -37,8 +37,8 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
37
37
  function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
38
38
  var PRIMARY_GREEN = '#198754';
39
39
 
40
- // Max width for crop preview on large screens (tablets) - must match CustomCamera.js
41
- var CAMERA_PREVIEW_MAX_WIDTH = 500;
40
+ // Base dimension for responsive scaling (e.g. ~375pt design width)
41
+ var RESPONSIVE_BASE = 375;
42
42
  var ImageCropper = function ImageCropper(_ref) {
43
43
  var _cameraFrameData$curr3;
44
44
  var onConfirm = _ref.onConfirm,
@@ -47,8 +47,11 @@ var ImageCropper = function ImageCropper(_ref) {
47
47
  addheight = _ref.addheight,
48
48
  rotationLabel = _ref.rotationLabel;
49
49
  var _useWindowDimensions = (0, _reactNative.useWindowDimensions)(),
50
- windowWidth = _useWindowDimensions.width;
51
- var cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
50
+ windowWidth = _useWindowDimensions.width,
51
+ windowHeight = _useWindowDimensions.height;
52
+
53
+ // Responsive scale: image and UI adapt to small (phone) and large (tablet) screens
54
+ var responsiveScale = Math.max(0.65, Math.min(2, Math.min(windowWidth, windowHeight) / RESPONSIVE_BASE));
52
55
  var guessExtensionFromUri = function guessExtensionFromUri(uri) {
53
56
  if (!uri || typeof uri !== 'string') return null;
54
57
  var match = uri.match(/\.([a-zA-Z0-9]+)(?:[?#].*)?$/);
@@ -454,92 +457,11 @@ var ImageCropper = function ImageCropper(_ref) {
454
457
  }
455
458
 
456
459
  // ✅ CRITICAL FIX: Calculate crop box as percentage of VISIBLE IMAGE AREA (imageDisplayRect)
457
- // NOT the wrapper. This ensures the crop box is truly 80% of the image, not 80% of wrapper then clamped.
458
- // Calculate absolute position of imageDisplayRect within wrapper
459
- var imageRectX = wrapper.x + imageRect.x;
460
- var imageRectY = wrapper.y + imageRect.y;
461
-
462
- // ✅ PRIORITY RULE #3: IF image comes from camera → Use EXACT green frame coordinates
463
- // Image is displayed in "cover" mode (full wrapper), so green frame coords = white frame coords.
464
- // Store points in WRAPPER-RELATIVE coordinates (greenFrame.x, greenFrame.y) so they match
465
- // touch events (locationX/locationY are relative to the wrapper). This keeps display→image
466
- // conversion correct in Confirm.
467
- if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
468
- var greenFrame = cameraFrameData.current.greenFrame;
469
- var _boxX = greenFrame.x;
470
- var _boxY = greenFrame.y;
471
- var _boxWidth = greenFrame.width;
472
- var _boxHeight = greenFrame.height;
473
-
474
- // ✅ SAFETY: Validate calculated coordinates before creating points
475
- var _isValidCoordinate = function _isValidCoordinate(val) {
476
- return typeof val === 'number' && isFinite(val) && !isNaN(val);
477
- };
478
- if (!_isValidCoordinate(_boxX) || !_isValidCoordinate(_boxY) || !_isValidCoordinate(_boxWidth) || !_isValidCoordinate(_boxHeight)) {
479
- console.warn("⚠️ Invalid coordinates calculated for crop box, skipping initialization");
480
- return;
481
- }
460
+ // in WRAPPER-RELATIVE coordinates (same space as points/touches and SVG).
461
+ var imageRectX = imageRect.x;
462
+ var imageRectY = imageRect.y;
482
463
 
483
- // ✅ Points in wrapper-relative coords (same as touch events)
484
- var _newPoints = [{
485
- x: _boxX,
486
- y: _boxY
487
- },
488
- // Top-left
489
- {
490
- x: _boxX + _boxWidth,
491
- y: _boxY
492
- },
493
- // Top-right
494
- {
495
- x: _boxX + _boxWidth,
496
- y: _boxY + _boxHeight
497
- },
498
- // Bottom-right
499
- {
500
- x: _boxX,
501
- y: _boxY + _boxHeight
502
- } // Bottom-left
503
- ];
504
-
505
- // ✅ SAFETY: Validate all points before setting
506
- var _validPoints = _newPoints.filter(function (p) {
507
- return _isValidCoordinate(p.x) && _isValidCoordinate(p.y);
508
- });
509
- if (_validPoints.length !== _newPoints.length) {
510
- console.warn("⚠️ Some points have invalid coordinates, skipping initialization");
511
- return;
512
- }
513
- console.log("✅ Initializing crop box for camera image (COVER MODE - exact green frame):", {
514
- greenFrame: {
515
- x: greenFrame.x,
516
- y: greenFrame.y,
517
- width: greenFrame.width,
518
- height: greenFrame.height
519
- },
520
- whiteFrame: {
521
- x: _boxX.toFixed(2),
522
- y: _boxY.toFixed(2),
523
- width: _boxWidth.toFixed(2),
524
- height: _boxHeight.toFixed(2)
525
- },
526
- note: "Points in wrapper-relative coords - same as touch events and crop_image_size"
527
- });
528
- setPoints(_newPoints);
529
- hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
530
- // ✅ CRITICAL: DO NOT nullify cameraFrameData here - keep it for Image.getSize() callback check
531
- // It will be cleared when loading a new image
532
- return;
533
- }
534
-
535
- // ✅ PRIORITY RULE #3: DEFAULT logic ONLY for gallery images (NOT camera)
536
- // If we reach here and imageSource is 'camera', something went wrong
537
- if (imageSource.current === 'camera') {
538
- console.warn("⚠️ Camera image but no greenFrame found - this should not happen");
539
- return;
540
- }
541
-
542
- // ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered) - ONLY for gallery images
464
+ // ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered)
543
465
  var boxWidth = imageRect.width * 0.70; // 70% of visible image width
544
466
  var boxHeight = imageRect.height * 0.70; // 70% of visible image height
545
467
  var boxX = imageRectX + (imageRect.width - boxWidth) / 2; // Centered in image area
@@ -581,7 +503,7 @@ var ImageCropper = function ImageCropper(_ref) {
581
503
  height: wrapper.height
582
504
  },
583
505
  imageDisplayRect: imageRect,
584
- boxInImage: {
506
+ boxInWrapperCoords: {
585
507
  x: boxX,
586
508
  y: boxY,
587
509
  width: boxWidth,
@@ -770,12 +692,12 @@ var ImageCropper = function ImageCropper(_ref) {
770
692
  updateImageDisplayRect(wrapper.width, wrapper.height);
771
693
  imageRect = imageDisplayRect.current;
772
694
  }
773
- // If still not available, use wrapper as fallback
695
+ // If still not available, use full wrapper as fallback (wrapper-relative coords)
774
696
  if (imageRect.width === 0 || imageRect.height === 0) {
775
697
  if (wrapper.width > 0 && wrapper.height > 0) {
776
698
  imageRect = {
777
- x: wrapper.x,
778
- y: wrapper.y,
699
+ x: 0,
700
+ y: 0,
779
701
  width: wrapper.width,
780
702
  height: wrapper.height
781
703
  };
@@ -787,12 +709,10 @@ var ImageCropper = function ImageCropper(_ref) {
787
709
  }
788
710
 
789
711
  // ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
790
- // Les coordonnées tapX/tapY sont relatives au wrapper commun
791
- var imageRectX = wrapper.x + imageRect.x;
792
- var imageRectY = wrapper.y + imageRect.y;
712
+ // Les coordonnées tapX/tapY et imageRect sont RELATIVES au wrapper commun (origine 0,0)
793
713
  var _x$y$width$height = {
794
- x: imageRectX,
795
- y: imageRectY,
714
+ x: imageRect.x,
715
+ y: imageRect.y,
796
716
  width: imageRect.width,
797
717
  height: imageRect.height
798
718
  },
@@ -800,8 +720,8 @@ var ImageCropper = function ImageCropper(_ref) {
800
720
  cy = _x$y$width$height.y,
801
721
  cw = _x$y$width$height.width,
802
722
  ch = _x$y$width$height.height;
803
- // ✅ Larger select radius for easier point selection (especially on touch screens)
804
- var selectRadius = 50; // Increased from 28 to 50 for better UX
723
+ // ✅ Responsive select radius: scales with screen so touch target matches handle size
724
+ var selectRadius = Math.max(28, Math.round(50 * responsiveScale));
805
725
 
806
726
  // ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
807
727
  // Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
@@ -935,12 +855,12 @@ var ImageCropper = function ImageCropper(_ref) {
935
855
  updateImageDisplayRect(wrapper.width, wrapper.height);
936
856
  imageRect = imageDisplayRect.current;
937
857
  }
938
- // If still not available, use wrapper as fallback
858
+ // If still not available, use full wrapper as fallback (wrapper-relative coords)
939
859
  if (imageRect.width === 0 || imageRect.height === 0) {
940
860
  if (wrapper.width > 0 && wrapper.height > 0) {
941
861
  imageRect = {
942
- x: wrapper.x,
943
- y: wrapper.y,
862
+ x: 0,
863
+ y: 0,
944
864
  width: wrapper.width,
945
865
  height: wrapper.height
946
866
  };
@@ -951,13 +871,11 @@ var ImageCropper = function ImageCropper(_ref) {
951
871
  }
952
872
  }
953
873
 
954
- // ✅ CRITICAL: Calculate absolute bounds of imageDisplayRect within the wrapper
955
- // imageRect is relative to wrapper, so we need to add wrapper offset
956
- var imageRectX = wrapper.x + imageRect.x;
957
- var imageRectY = wrapper.y + imageRect.y;
874
+ // ✅ Bounds in wrapper-relative coordinates (touch/points use wrapper as origin)
875
+ // Full picture = entire wrapper; visible image = imageDisplayRect
958
876
  var contentRect = {
959
- x: imageRectX,
960
- y: imageRectY,
877
+ x: imageRect.x,
878
+ y: imageRect.y,
961
879
  width: imageRect.width,
962
880
  height: imageRect.height
963
881
  };
@@ -985,28 +903,19 @@ var ImageCropper = function ImageCropper(_ref) {
985
903
  var newX = currentX + touchOffset.current.x;
986
904
  var newY = currentY + touchOffset.current.y;
987
905
 
988
- // ✅ SEPARATE DRAG BOUNDS vs CROP BOUNDS
989
- var cx = contentRect.x,
990
- cy = contentRect.y,
991
- cw = contentRect.width,
992
- ch = contentRect.height;
993
-
994
- // ✅ STRICT BOUNDS: For final crop safety (imageDisplayRect)
995
- var strictMinX = cx;
996
- var strictMaxX = cx + cw;
997
- var strictMinY = cy;
998
- var strictMaxY = cy + ch;
999
-
1000
- // ✅ DRAG BOUNDS: Allow movement ANYWHERE in wrapper during drag
1001
- // Points can move freely across the entire screen for maximum flexibility
1002
- // They will be clamped to imageDisplayRect only on release for safe cropping
1003
- var wrapperRect = wrapper;
1004
- var overshootMinX = wrapperRect.x;
1005
- var overshootMaxX = wrapperRect.x + wrapperRect.width;
1006
- var overshootMinY = wrapperRect.y;
1007
- var overshootMaxY = wrapperRect.y + wrapperRect.height;
1008
-
1009
- // ✅ DRAG BOUNDS: Clamp ONLY to overshootBounds during drag (NOT strictBounds)
906
+ // ✅ STRICT BOUNDS: visible image area (wrapper-relative) for lastValidPosition
907
+ var strictMinX = contentRect.x;
908
+ var strictMaxX = contentRect.x + contentRect.width;
909
+ var strictMinY = contentRect.y;
910
+ var strictMaxY = contentRect.y + contentRect.height;
911
+
912
+ // ✅ DRAG BOUNDS: keep points strictly on the visible picture (no going outside image)
913
+ var overshootMinX = strictMinX;
914
+ var overshootMaxX = strictMaxX;
915
+ var overshootMinY = strictMinY;
916
+ var overshootMaxY = strictMaxY;
917
+
918
+ // ✅ Clamp to visible image so points always stay on the picture
1010
919
  var dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
1011
920
  var dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
1012
921
 
@@ -1146,12 +1055,13 @@ var ImageCropper = function ImageCropper(_ref) {
1146
1055
  }
1147
1056
  }
1148
1057
  if (imageRect.width > 0 && imageRect.height > 0) {
1149
- var imageRectX = wrapper.x + imageRect.x;
1150
- var imageRectY = wrapper.y + imageRect.y;
1151
- var imageRectMaxX = imageRectX + imageRect.width;
1152
- var imageRectMaxY = imageRectY + imageRect.height;
1058
+ // Bounds in wrapper-relative coords (same as point coords)
1059
+ var minX = imageRect.x;
1060
+ var minY = imageRect.y;
1061
+ var maxX = imageRect.x + imageRect.width;
1062
+ var maxY = imageRect.y + imageRect.height;
1153
1063
 
1154
- // Clamp the dragged point back to strict image bounds
1064
+ // Clamp the dragged point back to visible image area (full picture in wrapper-relative coords)
1155
1065
  setPoints(function (prev) {
1156
1066
  // ✅ SAFETY: Ensure prev is a valid array
1157
1067
  if (!Array.isArray(prev) || prev.length === 0) {
@@ -1168,8 +1078,8 @@ var ImageCropper = function ImageCropper(_ref) {
1168
1078
  return prev;
1169
1079
  }
1170
1080
  var clampedPoint = {
1171
- x: Math.max(imageRectX, Math.min(point.x, imageRectMaxX)),
1172
- y: Math.max(imageRectY, Math.min(point.y, imageRectMaxY))
1081
+ x: Math.max(minX, Math.min(point.x, maxX)),
1082
+ y: Math.max(minY, Math.min(point.y, maxY))
1173
1083
  };
1174
1084
 
1175
1085
  // Only update if point was outside bounds
@@ -1184,10 +1094,10 @@ var ImageCropper = function ImageCropper(_ref) {
1184
1094
  y: clampedPoint.y.toFixed(2)
1185
1095
  },
1186
1096
  bounds: {
1187
- minX: imageRectX.toFixed(2),
1188
- maxX: imageRectMaxX.toFixed(2),
1189
- minY: imageRectY.toFixed(2),
1190
- maxY: imageRectMaxY.toFixed(2)
1097
+ minX: minX.toFixed(2),
1098
+ maxX: maxX.toFixed(2),
1099
+ minY: minY.toFixed(2),
1100
+ maxY: maxY.toFixed(2)
1191
1101
  }
1192
1102
  });
1193
1103
  return prev.map(function (p, i) {
@@ -1336,9 +1246,13 @@ var ImageCropper = function ImageCropper(_ref) {
1336
1246
  onCancel: function onCancel() {
1337
1247
  return setShowCustomCamera(false);
1338
1248
  }
1339
- }) : /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1249
+ }) : /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1250
+ style: _ImageCropperStyles["default"].content
1251
+ }, image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1252
+ style: _ImageCropperStyles["default"].imageRegion
1253
+ }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1340
1254
  style: [_ImageCropperStyles["default"].commonWrapper, {
1341
- width: cameraPreviewWidth
1255
+ maxHeight: windowHeight * 0.7
1342
1256
  }],
1343
1257
  ref: commonWrapperRef,
1344
1258
  onLayout: onCommonWrapperLayout
@@ -1420,8 +1334,17 @@ var ImageCropper = function ImageCropper(_ref) {
1420
1334
  pointerEvents: "none"
1421
1335
  }, function () {
1422
1336
  // ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
1423
- var wrapperWidth = commonWrapperLayout.current.width || cameraPreviewWidth;
1424
- var wrapperHeight = commonWrapperLayout.current.height || cameraPreviewWidth * 16 / 9;
1337
+ var wrapperWidth = commonWrapperLayout.current.width || windowWidth;
1338
+ var wrapperHeight = commonWrapperLayout.current.height || windowHeight;
1339
+
1340
+ // ✅ Size scale: when picture area is smaller, make stroke/points smaller too
1341
+ var base = Math.min(windowWidth, windowHeight) || 1;
1342
+ var pictureDiagonal = Math.sqrt(wrapperWidth * wrapperWidth + wrapperHeight * wrapperHeight);
1343
+ var sizeScaleRaw = pictureDiagonal / base;
1344
+ var sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
1345
+
1346
+ var stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
1347
+ var handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
1425
1348
  return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1426
1349
  d: "M 0 0 H ".concat(wrapperWidth, " V ").concat(wrapperHeight, " H 0 Z ").concat(createPath()),
1427
1350
  fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
@@ -1430,13 +1353,13 @@ var ImageCropper = function ImageCropper(_ref) {
1430
1353
  d: createPath(),
1431
1354
  fill: "transparent",
1432
1355
  stroke: "white",
1433
- strokeWidth: 2
1356
+ strokeWidth: stroke
1434
1357
  }), !showResult && points.map(function (point, index) {
1435
1358
  return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
1436
1359
  key: index,
1437
1360
  cx: point.x,
1438
1361
  cy: point.y,
1439
- r: 10,
1362
+ r: handleRadius,
1440
1363
  fill: "white"
1441
1364
  });
1442
1365
  }));
@@ -1457,15 +1380,22 @@ var ImageCropper = function ImageCropper(_ref) {
1457
1380
  }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1458
1381
  style: {
1459
1382
  color: PRIMARY_GREEN,
1460
- marginTop: 8,
1461
- fontSize: 14
1383
+ marginTop: 8 * responsiveScale,
1384
+ fontSize: Math.max(12, Math.round(14 * responsiveScale))
1462
1385
  }
1463
- }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...'))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1386
+ }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...')))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1464
1387
  style: [_ImageCropperStyles["default"].buttonContainerBelow, {
1465
- paddingBottom: Math.max(insets.bottom, 16)
1388
+ paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
1389
+ paddingTop: Math.round(12 * responsiveScale),
1390
+ paddingHorizontal: Math.round(12 * responsiveScale),
1391
+ gap: Math.round(8 * responsiveScale)
1466
1392
  }]
1467
1393
  }, _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1468
- style: [_ImageCropperStyles["default"].rotationButton, isRotating && {
1394
+ style: [_ImageCropperStyles["default"].rotationButton, {
1395
+ width: Math.round(56 * responsiveScale),
1396
+ height: Math.round(48 * responsiveScale),
1397
+ borderRadius: Math.round(28 * responsiveScale)
1398
+ }, isRotating && {
1469
1399
  opacity: 0.7
1470
1400
  }],
1471
1401
  onPress: function onPress() {
@@ -1477,15 +1407,20 @@ var ImageCropper = function ImageCropper(_ref) {
1477
1407
  color: "white"
1478
1408
  }) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1479
1409
  name: "sync",
1480
- size: 24,
1410
+ size: Math.round(24 * responsiveScale),
1481
1411
  color: "white"
1482
1412
  })), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1483
- style: _ImageCropperStyles["default"].button,
1413
+ style: [_ImageCropperStyles["default"].button, {
1414
+ minHeight: Math.round(48 * responsiveScale),
1415
+ paddingVertical: Math.round(12 * responsiveScale),
1416
+ paddingHorizontal: Math.round(10 * responsiveScale)
1417
+ }],
1484
1418
  onPress: handleReset
1485
1419
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1486
- style: _ImageCropperStyles["default"].buttonText
1420
+ style: [_ImageCropperStyles["default"].buttonText, {
1421
+ fontSize: Math.max(14, Math.round(18 * responsiveScale))
1422
+ }]
1487
1423
  }, "Reset")), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1488
- style: _ImageCropperStyles["default"].button,
1489
1424
  onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1490
1425
  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;
1491
1426
  return _regenerator().w(function (_context2) {
@@ -1651,9 +1586,16 @@ var ImageCropper = function ImageCropper(_ref) {
1651
1586
  return _context2.a(2);
1652
1587
  }
1653
1588
  }, _callee2, null, [[1, 5, 6, 7]]);
1654
- }))
1589
+ })),
1590
+ style: [_ImageCropperStyles["default"].button, {
1591
+ minHeight: Math.round(48 * responsiveScale),
1592
+ paddingVertical: Math.round(12 * responsiveScale),
1593
+ paddingHorizontal: Math.round(10 * responsiveScale)
1594
+ }]
1655
1595
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1656
- style: _ImageCropperStyles["default"].buttonText
1596
+ style: [_ImageCropperStyles["default"].buttonText, {
1597
+ fontSize: Math.max(14, Math.round(18 * responsiveScale))
1598
+ }]
1657
1599
  }, "Confirm"))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1658
1600
  style: _ImageCropperStyles["default"].centerButtonsContainer
1659
1601
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
@@ -16,21 +16,35 @@ var GLOW_WHITE = 'rgba(255, 255, 255, 0.85)';
16
16
  var styles = _reactNative.StyleSheet.create({
17
17
  container: {
18
18
  flex: 1,
19
- alignItems: 'center',
19
+ width: '100%',
20
+ height: '100%',
21
+ alignItems: 'stretch',
20
22
  justifyContent: 'flex-start',
21
- // ✅ Start from top, allow content to flow down
22
23
  backgroundColor: DEEP_BLACK
23
24
  },
24
- // Wrapper for crop preview (9/16, same as CustomCamera); width set dynamically for tablet
25
+ // Main content area: full screen; image centered, buttons below
26
+ content: {
27
+ flex: 1,
28
+ width: '100%',
29
+ alignSelf: 'stretch',
30
+ flexDirection: 'column',
31
+ minHeight: 0
32
+ },
33
+ // Wrapper that centers the crop box vertically and horizontally on screen
34
+ imageRegion: {
35
+ flex: 1,
36
+ width: '100%',
37
+ justifyContent: 'center',
38
+ alignItems: 'center'
39
+ },
40
+ // Crop preview: responsive box, centered inside imageRegion
25
41
  commonWrapper: {
42
+ width: '100%',
26
43
  aspectRatio: 9 / 16,
27
- borderRadius: 30,
28
44
  overflow: 'hidden',
29
45
  alignItems: 'center',
30
46
  justifyContent: 'center',
31
- position: 'relative',
32
- backgroundColor: 'black',
33
- marginBottom: 0
47
+ backgroundColor: 'black'
34
48
  },
35
49
  buttonContainer: {
36
50
  position: 'absolute',
@@ -46,18 +60,14 @@ var styles = _reactNative.StyleSheet.create({
46
60
  gap: 10
47
61
  },
48
62
  buttonContainerBelow: {
49
- // ✅ Buttons positioned BELOW image, not overlapping, above Android navigation bar
50
- position: 'absolute',
51
- bottom: 0,
52
- left: 0,
53
- right: 0,
54
63
  flexDirection: 'row',
55
- justifyContent: 'center',
56
64
  alignItems: 'center',
57
- paddingHorizontal: 10,
58
- paddingTop: 16,
65
+ justifyContent: 'space-between',
66
+ width: '100%',
67
+ paddingHorizontal: 12,
68
+ paddingTop: 12,
59
69
  gap: 8,
60
- backgroundColor: DEEP_BLACK // Fond noir pour séparer visuellement
70
+ backgroundColor: DEEP_BLACK
61
71
  },
62
72
  iconButton: {
63
73
  backgroundColor: PRIMARY_GREEN,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-expo-cropper",
3
- "version": "1.2.48",
3
+ "version": "1.2.49",
4
4
  "description": "Recadrage polygonal d'images.",
5
5
  "main": "index.js",
6
6
  "author": "PCS AGRI",
@@ -10,12 +10,14 @@ import { applyMaskToImage, MaskView } from './ImageMaskProcessor';
10
10
 
11
11
  const PRIMARY_GREEN = '#198754';
12
12
 
13
- // Max width for crop preview on large screens (tablets) - must match CustomCamera.js
14
- const CAMERA_PREVIEW_MAX_WIDTH = 500;
13
+ // Base dimension for responsive scaling (e.g. ~375pt design width)
14
+ const RESPONSIVE_BASE = 375;
15
15
 
16
16
  const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rotationLabel }) => {
17
- const { width: windowWidth } = useWindowDimensions();
18
- const cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
17
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
18
+
19
+ // Responsive scale: image and UI adapt to small (phone) and large (tablet) screens
20
+ const responsiveScale = Math.max(0.65, Math.min(2, Math.min(windowWidth, windowHeight) / RESPONSIVE_BASE));
19
21
 
20
22
  const guessExtensionFromUri = (uri) => {
21
23
  if (!uri || typeof uri !== 'string') return null;
@@ -343,69 +345,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
343
345
  }
344
346
 
345
347
  // ✅ CRITICAL FIX: Calculate crop box as percentage of VISIBLE IMAGE AREA (imageDisplayRect)
346
- // NOT the wrapper. This ensures the crop box is truly 80% of the image, not 80% of wrapper then clamped.
347
- // Calculate absolute position of imageDisplayRect within wrapper
348
- const imageRectX = wrapper.x + imageRect.x;
349
- const imageRectY = wrapper.y + imageRect.y;
350
-
351
- // ✅ PRIORITY RULE #3: IF image comes from camera → Use EXACT green frame coordinates
352
- // Image is displayed in "cover" mode (full wrapper), so green frame coords = white frame coords.
353
- // Store points in WRAPPER-RELATIVE coordinates (greenFrame.x, greenFrame.y) so they match
354
- // touch events (locationX/locationY are relative to the wrapper). This keeps display→image
355
- // conversion correct in Confirm.
356
- if (cameraFrameData.current && cameraFrameData.current.greenFrame && originalImageDimensions.current.width > 0) {
357
- const greenFrame = cameraFrameData.current.greenFrame;
358
-
359
- const boxX = greenFrame.x;
360
- const boxY = greenFrame.y;
361
- const boxWidth = greenFrame.width;
362
- const boxHeight = greenFrame.height;
363
-
364
- // ✅ SAFETY: Validate calculated coordinates before creating points
365
- const isValidCoordinate = (val) => typeof val === 'number' && isFinite(val) && !isNaN(val);
366
-
367
- if (!isValidCoordinate(boxX) || !isValidCoordinate(boxY) ||
368
- !isValidCoordinate(boxWidth) || !isValidCoordinate(boxHeight)) {
369
- console.warn("⚠️ Invalid coordinates calculated for crop box, skipping initialization");
370
- return;
371
- }
372
-
373
- // ✅ Points in wrapper-relative coords (same as touch events)
374
- const newPoints = [
375
- { x: boxX, y: boxY }, // Top-left
376
- { x: boxX + boxWidth, y: boxY }, // Top-right
377
- { x: boxX + boxWidth, y: boxY + boxHeight }, // Bottom-right
378
- { x: boxX, y: boxY + boxHeight }, // Bottom-left
379
- ];
380
-
381
- // ✅ SAFETY: Validate all points before setting
382
- const validPoints = newPoints.filter(p => isValidCoordinate(p.x) && isValidCoordinate(p.y));
383
- if (validPoints.length !== newPoints.length) {
384
- console.warn("⚠️ Some points have invalid coordinates, skipping initialization");
385
- return;
386
- }
387
-
388
- console.log("✅ Initializing crop box for camera image (COVER MODE - exact green frame):", {
389
- greenFrame: { x: greenFrame.x, y: greenFrame.y, width: greenFrame.width, height: greenFrame.height },
390
- whiteFrame: { x: boxX.toFixed(2), y: boxY.toFixed(2), width: boxWidth.toFixed(2), height: boxHeight.toFixed(2) },
391
- note: "Points in wrapper-relative coords - same as touch events and crop_image_size",
392
- });
393
-
394
- setPoints(newPoints);
395
- hasInitializedCropBox.current = true; // ✅ CRITICAL: Mark as initialized
396
- // ✅ CRITICAL: DO NOT nullify cameraFrameData here - keep it for Image.getSize() callback check
397
- // It will be cleared when loading a new image
398
- return;
399
- }
400
-
401
- // ✅ PRIORITY RULE #3: DEFAULT logic ONLY for gallery images (NOT camera)
402
- // If we reach here and imageSource is 'camera', something went wrong
403
- if (imageSource.current === 'camera') {
404
- console.warn("⚠️ Camera image but no greenFrame found - this should not happen");
405
- return;
406
- }
348
+ // in WRAPPER-RELATIVE coordinates (same space as points/touches and SVG).
349
+ const imageRectX = imageRect.x;
350
+ const imageRectY = imageRect.y;
407
351
 
408
- // ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered) - ONLY for gallery images
352
+ // ✅ DEFAULT: Crop box (70% of VISIBLE IMAGE AREA - centered)
409
353
  const boxWidth = imageRect.width * 0.70; // 70% of visible image width
410
354
  const boxHeight = imageRect.height * 0.70; // 70% of visible image height
411
355
  const boxX = imageRectX + (imageRect.width - boxWidth) / 2; // Centered in image area
@@ -437,8 +381,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
437
381
  console.log("✅ Initializing crop box (default - 70% of visible image area, gallery only):", {
438
382
  wrapper: { width: wrapper.width, height: wrapper.height },
439
383
  imageDisplayRect: imageRect,
440
- boxInImage: { x: boxX, y: boxY, width: boxWidth, height: boxHeight },
441
- points: newPoints
384
+ boxInWrapperCoords: { x: boxX, y: boxY, width: boxWidth, height: boxHeight },
385
+ points: newPoints,
442
386
  });
443
387
 
444
388
  setPoints(newPoints);
@@ -620,14 +564,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
620
564
  updateImageDisplayRect(wrapper.width, wrapper.height);
621
565
  imageRect = imageDisplayRect.current;
622
566
  }
623
- // If still not available, use wrapper as fallback
567
+ // If still not available, use full wrapper as fallback (wrapper-relative coords)
624
568
  if (imageRect.width === 0 || imageRect.height === 0) {
625
569
  if (wrapper.width > 0 && wrapper.height > 0) {
626
570
  imageRect = {
627
- x: wrapper.x,
628
- y: wrapper.y,
571
+ x: 0,
572
+ y: 0,
629
573
  width: wrapper.width,
630
- height: wrapper.height
574
+ height: wrapper.height,
631
575
  };
632
576
  } else {
633
577
  console.warn("⚠️ Cannot handle tap: wrapper or imageDisplayRect not available");
@@ -637,17 +581,15 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
637
581
  }
638
582
 
639
583
  // ✅ Clamp to real displayed image content (imageDisplayRect dans le wrapper)
640
- // Les coordonnées tapX/tapY sont relatives au wrapper commun
641
- const imageRectX = wrapper.x + imageRect.x;
642
- const imageRectY = wrapper.y + imageRect.y;
584
+ // Les coordonnées tapX/tapY et imageRect sont RELATIVES au wrapper commun (origine 0,0)
643
585
  const { x: cx, y: cy, width: cw, height: ch } = {
644
- x: imageRectX,
645
- y: imageRectY,
586
+ x: imageRect.x,
587
+ y: imageRect.y,
646
588
  width: imageRect.width,
647
- height: imageRect.height
589
+ height: imageRect.height,
648
590
  };
649
- // ✅ Larger select radius for easier point selection (especially on touch screens)
650
- const selectRadius = 50; // Increased from 28 to 50 for better UX
591
+ // ✅ Responsive select radius: scales with screen so touch target matches handle size
592
+ const selectRadius = Math.max(28, Math.round(50 * responsiveScale));
651
593
 
652
594
  // ✅ CRITICAL: Check for existing point selection FIRST (using raw tap coordinates)
653
595
  // Don't clamp tapX/Y for point selection - points can be anywhere in wrapper now
@@ -770,14 +712,14 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
770
712
  updateImageDisplayRect(wrapper.width, wrapper.height);
771
713
  imageRect = imageDisplayRect.current;
772
714
  }
773
- // If still not available, use wrapper as fallback
715
+ // If still not available, use full wrapper as fallback (wrapper-relative coords)
774
716
  if (imageRect.width === 0 || imageRect.height === 0) {
775
717
  if (wrapper.width > 0 && wrapper.height > 0) {
776
718
  imageRect = {
777
- x: wrapper.x,
778
- y: wrapper.y,
719
+ x: 0,
720
+ y: 0,
779
721
  width: wrapper.width,
780
- height: wrapper.height
722
+ height: wrapper.height,
781
723
  };
782
724
  } else {
783
725
  console.warn("⚠️ Cannot move point: wrapper or imageDisplayRect not available");
@@ -786,13 +728,11 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
786
728
  }
787
729
  }
788
730
 
789
- // ✅ CRITICAL: Calculate absolute bounds of imageDisplayRect within the wrapper
790
- // imageRect is relative to wrapper, so we need to add wrapper offset
791
- const imageRectX = wrapper.x + imageRect.x;
792
- const imageRectY = wrapper.y + imageRect.y;
731
+ // ✅ Bounds in wrapper-relative coordinates (touch/points use wrapper as origin)
732
+ // Full picture = entire wrapper; visible image = imageDisplayRect
793
733
  const contentRect = {
794
- x: imageRectX,
795
- y: imageRectY,
734
+ x: imageRect.x,
735
+ y: imageRect.y,
796
736
  width: imageRect.width,
797
737
  height: imageRect.height
798
738
  };
@@ -820,25 +760,19 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
820
760
  const newX = currentX + touchOffset.current.x;
821
761
  const newY = currentY + touchOffset.current.y;
822
762
 
823
- // ✅ SEPARATE DRAG BOUNDS vs CROP BOUNDS
824
- const { x: cx, y: cy, width: cw, height: ch } = contentRect;
763
+ // ✅ STRICT BOUNDS: visible image area (wrapper-relative) for lastValidPosition
764
+ const strictMinX = contentRect.x;
765
+ const strictMaxX = contentRect.x + contentRect.width;
766
+ const strictMinY = contentRect.y;
767
+ const strictMaxY = contentRect.y + contentRect.height;
825
768
 
826
- // ✅ STRICT BOUNDS: For final crop safety (imageDisplayRect)
827
- const strictMinX = cx;
828
- const strictMaxX = cx + cw;
829
- const strictMinY = cy;
830
- const strictMaxY = cy + ch;
769
+ // ✅ DRAG BOUNDS: keep points strictly on the visible picture (no going outside image)
770
+ const overshootMinX = strictMinX;
771
+ const overshootMaxX = strictMaxX;
772
+ const overshootMinY = strictMinY;
773
+ const overshootMaxY = strictMaxY;
831
774
 
832
- // ✅ DRAG BOUNDS: Allow movement ANYWHERE in wrapper during drag
833
- // Points can move freely across the entire screen for maximum flexibility
834
- // They will be clamped to imageDisplayRect only on release for safe cropping
835
- const wrapperRect = wrapper;
836
- const overshootMinX = wrapperRect.x;
837
- const overshootMaxX = wrapperRect.x + wrapperRect.width;
838
- const overshootMinY = wrapperRect.y;
839
- const overshootMaxY = wrapperRect.y + wrapperRect.height;
840
-
841
- // ✅ DRAG BOUNDS: Clamp ONLY to overshootBounds during drag (NOT strictBounds)
775
+ // ✅ Clamp to visible image so points always stay on the picture
842
776
  const dragX = Math.max(overshootMinX, Math.min(newX, overshootMaxX));
843
777
  const dragY = Math.max(overshootMinY, Math.min(newY, overshootMaxY));
844
778
 
@@ -974,12 +908,13 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
974
908
  }
975
909
 
976
910
  if (imageRect.width > 0 && imageRect.height > 0) {
977
- const imageRectX = wrapper.x + imageRect.x;
978
- const imageRectY = wrapper.y + imageRect.y;
979
- const imageRectMaxX = imageRectX + imageRect.width;
980
- const imageRectMaxY = imageRectY + imageRect.height;
911
+ // Bounds in wrapper-relative coords (same as point coords)
912
+ const minX = imageRect.x;
913
+ const minY = imageRect.y;
914
+ const maxX = imageRect.x + imageRect.width;
915
+ const maxY = imageRect.y + imageRect.height;
981
916
 
982
- // Clamp the dragged point back to strict image bounds
917
+ // Clamp the dragged point back to visible image area (full picture in wrapper-relative coords)
983
918
  setPoints(prev => {
984
919
  // ✅ SAFETY: Ensure prev is a valid array
985
920
  if (!Array.isArray(prev) || prev.length === 0) {
@@ -999,8 +934,8 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
999
934
  }
1000
935
 
1001
936
  const clampedPoint = {
1002
- x: Math.max(imageRectX, Math.min(point.x, imageRectMaxX)),
1003
- y: Math.max(imageRectY, Math.min(point.y, imageRectMaxY))
937
+ x: Math.max(minX, Math.min(point.x, maxX)),
938
+ y: Math.max(minY, Math.min(point.y, maxY))
1004
939
  };
1005
940
 
1006
941
  // Only update if point was outside bounds
@@ -1008,7 +943,7 @@ const ImageCropper = ({ onConfirm, openCameraFirst, initialImage, addheight, rot
1008
943
  console.log("🔒 Clamping point back to image bounds on release:", {
1009
944
  before: { x: point.x.toFixed(2), y: point.y.toFixed(2) },
1010
945
  after: { x: clampedPoint.x.toFixed(2), y: clampedPoint.y.toFixed(2) },
1011
- bounds: { minX: imageRectX.toFixed(2), maxX: imageRectMaxX.toFixed(2), minY: imageRectY.toFixed(2), maxY: imageRectMaxY.toFixed(2) }
946
+ bounds: { minX: minX.toFixed(2), maxX: maxX.toFixed(2), minY: minY.toFixed(2), maxY: maxY.toFixed(2) }
1012
947
  });
1013
948
 
1014
949
  return prev.map((p, i) => i === pointIndex ? clampedPoint : p);
@@ -1132,16 +1067,19 @@ const rotatePreviewImage = async (degrees) => {
1132
1067
  onCancel={() => setShowCustomCamera(false)}
1133
1068
  />
1134
1069
  ) : (
1135
- <>
1070
+ <View style={styles.content}>
1136
1071
  {image && (
1137
- <View
1138
- style={[
1139
- styles.commonWrapper,
1140
- { width: cameraPreviewWidth },
1141
- ]}
1142
- ref={commonWrapperRef}
1143
- onLayout={onCommonWrapperLayout}
1144
- >
1072
+ <View style={styles.imageRegion}>
1073
+ <View
1074
+ style={[
1075
+ styles.commonWrapper,
1076
+ {
1077
+ maxHeight: windowHeight * 0.7,
1078
+ },
1079
+ ]}
1080
+ ref={commonWrapperRef}
1081
+ onLayout={onCommonWrapperLayout}
1082
+ >
1145
1083
  <View
1146
1084
  ref={viewRef}
1147
1085
  collapsable={false}
@@ -1217,8 +1155,18 @@ const rotatePreviewImage = async (degrees) => {
1217
1155
  <Svg style={styles.overlay} pointerEvents="none">
1218
1156
  {(() => {
1219
1157
  // ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
1220
- const wrapperWidth = commonWrapperLayout.current.width || cameraPreviewWidth;
1221
- const wrapperHeight = commonWrapperLayout.current.height || (cameraPreviewWidth * 16 / 9);
1158
+ const wrapperWidth = commonWrapperLayout.current.width || windowWidth;
1159
+ const wrapperHeight = commonWrapperLayout.current.height || windowHeight;
1160
+
1161
+ // ✅ Size scale: when picture area is smaller, make stroke/points smaller too
1162
+ const base = Math.min(windowWidth, windowHeight) || 1;
1163
+ const pictureDiagonal = Math.sqrt(wrapperWidth * wrapperWidth + wrapperHeight * wrapperHeight);
1164
+ const sizeScaleRaw = pictureDiagonal / base;
1165
+ const sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
1166
+
1167
+ const stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
1168
+ const handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
1169
+
1222
1170
  return (
1223
1171
  <>
1224
1172
  <Path
@@ -1227,10 +1175,10 @@ const rotatePreviewImage = async (degrees) => {
1227
1175
  fillRule="evenodd"
1228
1176
  />
1229
1177
  {!showResult && points.length > 0 && (
1230
- <Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
1178
+ <Path d={createPath()} fill="transparent" stroke="white" strokeWidth={stroke} />
1231
1179
  )}
1232
1180
  {!showResult && points.map((point, index) => (
1233
- <Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
1181
+ <Circle key={index} cx={point.x} cy={point.y} r={handleRadius} fill="white" />
1234
1182
  ))}
1235
1183
  </>
1236
1184
  );
@@ -1240,35 +1188,61 @@ const rotatePreviewImage = async (degrees) => {
1240
1188
  {isRotating && (
1241
1189
  <View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
1242
1190
  <ActivityIndicator size="large" color={PRIMARY_GREEN} />
1243
- <Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
1191
+ <Text style={{ color: PRIMARY_GREEN, marginTop: 8 * responsiveScale, fontSize: Math.max(12, Math.round(14 * responsiveScale)) }}>{rotationLabel ?? 'Rotation...'}</Text>
1244
1192
  </View>
1245
1193
  )}
1246
1194
  </View>
1195
+ </View>
1247
1196
  )}
1248
1197
 
1249
- {/* ✅ Buttons positioned BELOW the image, not overlapping */}
1198
+ {/* ✅ Buttons positioned as a responsive bottom bar (scale with screen size) */}
1250
1199
  {!showResult && image && (
1251
- <View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
1200
+ <View style={[
1201
+ styles.buttonContainerBelow,
1202
+ {
1203
+ paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
1204
+ paddingTop: Math.round(12 * responsiveScale),
1205
+ paddingHorizontal: Math.round(12 * responsiveScale),
1206
+ gap: Math.round(8 * responsiveScale),
1207
+ },
1208
+ ]}>
1252
1209
  {Platform.OS === 'android' && (
1253
1210
  <TouchableOpacity
1254
- style={[styles.rotationButton, isRotating && { opacity: 0.7 }]}
1211
+ style={[
1212
+ styles.rotationButton,
1213
+ {
1214
+ width: Math.round(56 * responsiveScale),
1215
+ height: Math.round(48 * responsiveScale),
1216
+ borderRadius: Math.round(28 * responsiveScale),
1217
+ },
1218
+ isRotating && { opacity: 0.7 },
1219
+ ]}
1255
1220
  onPress={() => enableRotation && rotatePreviewImage(90)}
1256
1221
  disabled={isRotating}
1257
1222
  >
1258
1223
  {isRotating ? (
1259
1224
  <ActivityIndicator size="small" color="white" />
1260
1225
  ) : (
1261
- <Ionicons name="sync" size={24} color="white" />
1226
+ <Ionicons name="sync" size={Math.round(24 * responsiveScale)} color="white" />
1262
1227
  )}
1263
1228
  </TouchableOpacity>
1264
1229
  )}
1265
1230
 
1266
- <TouchableOpacity style={styles.button} onPress={handleReset}>
1267
- <Text style={styles.buttonText}>Reset</Text>
1231
+ <TouchableOpacity
1232
+ style={[
1233
+ styles.button,
1234
+ {
1235
+ minHeight: Math.round(48 * responsiveScale),
1236
+ paddingVertical: Math.round(12 * responsiveScale),
1237
+ paddingHorizontal: Math.round(10 * responsiveScale),
1238
+ },
1239
+ ]}
1240
+ onPress={handleReset}
1241
+ >
1242
+ <Text style={[styles.buttonText, { fontSize: Math.max(14, Math.round(18 * responsiveScale)) }]}>Reset</Text>
1268
1243
  </TouchableOpacity>
1269
1244
 
1270
1245
  <TouchableOpacity
1271
- style={styles.button}
1272
1246
  onPress={async () => {
1273
1247
  setIsLoading(true);
1274
1248
  try {
@@ -1399,8 +1373,16 @@ const rotatePreviewImage = async (degrees) => {
1399
1373
  setShowFullScreenCapture(false);
1400
1374
  }
1401
1375
  }}
1376
+ style={[
1377
+ styles.button,
1378
+ {
1379
+ minHeight: Math.round(48 * responsiveScale),
1380
+ paddingVertical: Math.round(12 * responsiveScale),
1381
+ paddingHorizontal: Math.round(10 * responsiveScale),
1382
+ },
1383
+ ]}
1402
1384
  >
1403
- <Text style={styles.buttonText}>Confirm</Text>
1385
+ <Text style={[styles.buttonText, { fontSize: Math.max(14, Math.round(18 * responsiveScale)) }]}>Confirm</Text>
1404
1386
  </TouchableOpacity>
1405
1387
  </View>
1406
1388
  )}
@@ -1411,7 +1393,7 @@ const rotatePreviewImage = async (degrees) => {
1411
1393
  <Text style={styles.welcomeText}>Sélectionnez une image</Text>
1412
1394
  </View>
1413
1395
  )}
1414
- </>
1396
+ </View>
1415
1397
  )}
1416
1398
 
1417
1399
  {/* ✅ CORRECTION : Vue de masque temporaire pour la capture
@@ -11,20 +11,35 @@ const GLOW_WHITE = 'rgba(255, 255, 255, 0.85)';
11
11
  const styles = StyleSheet.create({
12
12
  container: {
13
13
  flex: 1,
14
- alignItems: 'center',
15
- justifyContent: 'flex-start', // ✅ Start from top, allow content to flow down
14
+ width: '100%',
15
+ height: '100%',
16
+ alignItems: 'stretch',
17
+ justifyContent: 'flex-start',
16
18
  backgroundColor: DEEP_BLACK,
17
19
  },
18
- // Wrapper for crop preview (9/16, same as CustomCamera); width set dynamically for tablet
20
+ // Main content area: full screen; image centered, buttons below
21
+ content: {
22
+ flex: 1,
23
+ width: '100%',
24
+ alignSelf: 'stretch',
25
+ flexDirection: 'column',
26
+ minHeight: 0,
27
+ },
28
+ // Wrapper that centers the crop box vertically and horizontally on screen
29
+ imageRegion: {
30
+ flex: 1,
31
+ width: '100%',
32
+ justifyContent: 'center',
33
+ alignItems: 'center',
34
+ },
35
+ // Crop preview: responsive box, centered inside imageRegion
19
36
  commonWrapper: {
37
+ width: '100%',
20
38
  aspectRatio: 9 / 16,
21
- borderRadius: 30,
22
39
  overflow: 'hidden',
23
40
  alignItems: 'center',
24
41
  justifyContent: 'center',
25
- position: 'relative',
26
42
  backgroundColor: 'black',
27
- marginBottom: 0,
28
43
  },
29
44
  buttonContainer: {
30
45
  position: 'absolute',
@@ -40,18 +55,14 @@ const styles = StyleSheet.create({
40
55
  gap: 10,
41
56
  },
42
57
  buttonContainerBelow: {
43
- // ✅ Buttons positioned BELOW image, not overlapping, above Android navigation bar
44
- position: 'absolute',
45
- bottom: 0,
46
- left: 0,
47
- right: 0,
48
58
  flexDirection: 'row',
49
- justifyContent: 'center',
50
59
  alignItems: 'center',
51
- paddingHorizontal: 10,
52
- paddingTop: 16,
60
+ justifyContent: 'space-between',
61
+ width: '100%',
62
+ paddingHorizontal: 12,
63
+ paddingTop: 12,
53
64
  gap: 8,
54
- backgroundColor: DEEP_BLACK, // Fond noir pour séparer visuellement
65
+ backgroundColor: DEEP_BLACK,
55
66
  },
56
67
  iconButton: {
57
68
  backgroundColor: PRIMARY_GREEN,
@@ -103,10 +114,10 @@ const styles = StyleSheet.create({
103
114
  },
104
115
 
105
116
  image: {
106
- width: '100%',
107
- height: '100%',
108
- resizeMode: 'contain',
109
- },
117
+ width: '100%',
118
+ height: '100%',
119
+ resizeMode: 'contain',
120
+ },
110
121
 
111
122
  overlay: {
112
123
  position: 'absolute',