react-native-expo-cropper 1.2.48 → 1.2.50

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
- }
482
-
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
- }
460
+ // in WRAPPER-RELATIVE coordinates (same space as points/touches and SVG).
461
+ var imageRectX = imageRect.x;
462
+ var imageRectY = imageRect.y;
541
463
 
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,10 +1246,12 @@ 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, {
1340
- style: [_ImageCropperStyles["default"].commonWrapper, {
1341
- width: cameraPreviewWidth
1342
- }],
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, {
1254
+ style: _ImageCropperStyles["default"].commonWrapper,
1343
1255
  ref: commonWrapperRef,
1344
1256
  onLayout: onCommonWrapperLayout
1345
1257
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
@@ -1420,8 +1332,17 @@ var ImageCropper = function ImageCropper(_ref) {
1420
1332
  pointerEvents: "none"
1421
1333
  }, function () {
1422
1334
  // ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
1423
- var wrapperWidth = commonWrapperLayout.current.width || cameraPreviewWidth;
1424
- var wrapperHeight = commonWrapperLayout.current.height || cameraPreviewWidth * 16 / 9;
1335
+ var wrapperWidth = commonWrapperLayout.current.width || windowWidth;
1336
+ var wrapperHeight = commonWrapperLayout.current.height || windowHeight;
1337
+
1338
+ // ✅ Size scale: when picture area is smaller, make stroke/points smaller too
1339
+ var base = Math.min(windowWidth, windowHeight) || 1;
1340
+ var pictureDiagonal = Math.sqrt(wrapperWidth * wrapperWidth + wrapperHeight * wrapperHeight);
1341
+ var sizeScaleRaw = pictureDiagonal / base;
1342
+ var sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
1343
+
1344
+ var stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
1345
+ var handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
1425
1346
  return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Path, {
1426
1347
  d: "M 0 0 H ".concat(wrapperWidth, " V ").concat(wrapperHeight, " H 0 Z ").concat(createPath()),
1427
1348
  fill: showResult ? 'white' : 'rgba(0, 0, 0, 0.8)',
@@ -1430,13 +1351,13 @@ var ImageCropper = function ImageCropper(_ref) {
1430
1351
  d: createPath(),
1431
1352
  fill: "transparent",
1432
1353
  stroke: "white",
1433
- strokeWidth: 2
1354
+ strokeWidth: stroke
1434
1355
  }), !showResult && points.map(function (point, index) {
1435
1356
  return /*#__PURE__*/_react["default"].createElement(_reactNativeSvg.Circle, {
1436
1357
  key: index,
1437
1358
  cx: point.x,
1438
1359
  cy: point.y,
1439
- r: 10,
1360
+ r: handleRadius,
1440
1361
  fill: "white"
1441
1362
  });
1442
1363
  }));
@@ -1457,15 +1378,22 @@ var ImageCropper = function ImageCropper(_ref) {
1457
1378
  }), /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1458
1379
  style: {
1459
1380
  color: PRIMARY_GREEN,
1460
- marginTop: 8,
1461
- fontSize: 14
1381
+ marginTop: 8 * responsiveScale,
1382
+ fontSize: Math.max(12, Math.round(14 * responsiveScale))
1462
1383
  }
1463
- }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...'))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1384
+ }, rotationLabel !== null && rotationLabel !== void 0 ? rotationLabel : 'Rotation...')))), !showResult && image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1464
1385
  style: [_ImageCropperStyles["default"].buttonContainerBelow, {
1465
- paddingBottom: Math.max(insets.bottom, 16)
1386
+ paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
1387
+ paddingTop: Math.round(12 * responsiveScale),
1388
+ paddingHorizontal: Math.round(12 * responsiveScale),
1389
+ gap: Math.round(8 * responsiveScale)
1466
1390
  }]
1467
1391
  }, _reactNative.Platform.OS === 'android' && /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1468
- style: [_ImageCropperStyles["default"].rotationButton, isRotating && {
1392
+ style: [_ImageCropperStyles["default"].rotationButton, {
1393
+ width: Math.round(56 * responsiveScale),
1394
+ height: Math.round(48 * responsiveScale),
1395
+ borderRadius: Math.round(28 * responsiveScale)
1396
+ }, isRotating && {
1469
1397
  opacity: 0.7
1470
1398
  }],
1471
1399
  onPress: function onPress() {
@@ -1477,15 +1405,20 @@ var ImageCropper = function ImageCropper(_ref) {
1477
1405
  color: "white"
1478
1406
  }) : /*#__PURE__*/_react["default"].createElement(_vectorIcons.Ionicons, {
1479
1407
  name: "sync",
1480
- size: 24,
1408
+ size: Math.round(24 * responsiveScale),
1481
1409
  color: "white"
1482
1410
  })), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1483
- style: _ImageCropperStyles["default"].button,
1411
+ style: [_ImageCropperStyles["default"].button, {
1412
+ minHeight: Math.round(48 * responsiveScale),
1413
+ paddingVertical: Math.round(12 * responsiveScale),
1414
+ paddingHorizontal: Math.round(10 * responsiveScale)
1415
+ }],
1484
1416
  onPress: handleReset
1485
1417
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1486
- style: _ImageCropperStyles["default"].buttonText
1418
+ style: [_ImageCropperStyles["default"].buttonText, {
1419
+ fontSize: Math.max(14, Math.round(18 * responsiveScale))
1420
+ }]
1487
1421
  }, "Reset")), /*#__PURE__*/_react["default"].createElement(_reactNative.TouchableOpacity, {
1488
- style: _ImageCropperStyles["default"].button,
1489
1422
  onPress: /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
1490
1423
  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
1424
  return _regenerator().w(function (_context2) {
@@ -1651,9 +1584,16 @@ var ImageCropper = function ImageCropper(_ref) {
1651
1584
  return _context2.a(2);
1652
1585
  }
1653
1586
  }, _callee2, null, [[1, 5, 6, 7]]);
1654
- }))
1587
+ })),
1588
+ style: [_ImageCropperStyles["default"].button, {
1589
+ minHeight: Math.round(48 * responsiveScale),
1590
+ paddingVertical: Math.round(12 * responsiveScale),
1591
+ paddingHorizontal: Math.round(10 * responsiveScale)
1592
+ }]
1655
1593
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
1656
- style: _ImageCropperStyles["default"].buttonText
1594
+ style: [_ImageCropperStyles["default"].buttonText, {
1595
+ fontSize: Math.max(14, Math.round(18 * responsiveScale))
1596
+ }]
1657
1597
  }, "Confirm"))), !showResult && !image && /*#__PURE__*/_react["default"].createElement(_reactNative.View, {
1658
1598
  style: _ImageCropperStyles["default"].centerButtonsContainer
1659
1599
  }, /*#__PURE__*/_react["default"].createElement(_reactNative.Text, {
@@ -16,21 +16,36 @@ 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 occupies all space above the buttons
34
+ imageRegion: {
35
+ flex: 1,
36
+ width: '100%',
37
+ alignSelf: 'stretch',
38
+ justifyContent: 'flex-start',
39
+ alignItems: 'stretch'
40
+ },
41
+ // Crop preview: fill all available space in imageRegion
25
42
  commonWrapper: {
26
- aspectRatio: 9 / 16,
27
- borderRadius: 30,
43
+ flex: 1,
44
+ width: '100%',
28
45
  overflow: 'hidden',
29
46
  alignItems: 'center',
30
47
  justifyContent: 'center',
31
- position: 'relative',
32
- backgroundColor: 'black',
33
- marginBottom: 0
48
+ backgroundColor: 'black'
34
49
  },
35
50
  buttonContainer: {
36
51
  position: 'absolute',
@@ -46,18 +61,14 @@ var styles = _reactNative.StyleSheet.create({
46
61
  gap: 10
47
62
  },
48
63
  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
64
  flexDirection: 'row',
55
- justifyContent: 'center',
56
65
  alignItems: 'center',
57
- paddingHorizontal: 10,
58
- paddingTop: 16,
66
+ justifyContent: 'space-between',
67
+ width: '100%',
68
+ paddingHorizontal: 12,
69
+ paddingTop: 12,
59
70
  gap: 8,
60
- backgroundColor: DEEP_BLACK // Fond noir pour séparer visuellement
71
+ backgroundColor: DEEP_BLACK
61
72
  },
62
73
  iconButton: {
63
74
  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.50",
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,14 @@ 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={styles.commonWrapper}
1075
+ ref={commonWrapperRef}
1076
+ onLayout={onCommonWrapperLayout}
1077
+ >
1145
1078
  <View
1146
1079
  ref={viewRef}
1147
1080
  collapsable={false}
@@ -1217,8 +1150,18 @@ const rotatePreviewImage = async (degrees) => {
1217
1150
  <Svg style={styles.overlay} pointerEvents="none">
1218
1151
  {(() => {
1219
1152
  // ✅ Use wrapper dimensions for SVG path (wrapper coordinates)
1220
- const wrapperWidth = commonWrapperLayout.current.width || cameraPreviewWidth;
1221
- const wrapperHeight = commonWrapperLayout.current.height || (cameraPreviewWidth * 16 / 9);
1153
+ const wrapperWidth = commonWrapperLayout.current.width || windowWidth;
1154
+ const wrapperHeight = commonWrapperLayout.current.height || windowHeight;
1155
+
1156
+ // ✅ Size scale: when picture area is smaller, make stroke/points smaller too
1157
+ const base = Math.min(windowWidth, windowHeight) || 1;
1158
+ const pictureDiagonal = Math.sqrt(wrapperWidth * wrapperWidth + wrapperHeight * wrapperHeight);
1159
+ const sizeScaleRaw = pictureDiagonal / base;
1160
+ const sizeScale = Math.max(0.6, Math.min(1.4, sizeScaleRaw)); // clamp
1161
+
1162
+ const stroke = Math.max(0.75, 2 * responsiveScale * sizeScale);
1163
+ const handleRadius = Math.max(4, 10 * responsiveScale * sizeScale);
1164
+
1222
1165
  return (
1223
1166
  <>
1224
1167
  <Path
@@ -1227,10 +1170,10 @@ const rotatePreviewImage = async (degrees) => {
1227
1170
  fillRule="evenodd"
1228
1171
  />
1229
1172
  {!showResult && points.length > 0 && (
1230
- <Path d={createPath()} fill="transparent" stroke="white" strokeWidth={2} />
1173
+ <Path d={createPath()} fill="transparent" stroke="white" strokeWidth={stroke} />
1231
1174
  )}
1232
1175
  {!showResult && points.map((point, index) => (
1233
- <Circle key={index} cx={point.x} cy={point.y} r={10} fill="white" />
1176
+ <Circle key={index} cx={point.x} cy={point.y} r={handleRadius} fill="white" />
1234
1177
  ))}
1235
1178
  </>
1236
1179
  );
@@ -1240,35 +1183,61 @@ const rotatePreviewImage = async (degrees) => {
1240
1183
  {isRotating && (
1241
1184
  <View style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center' }}>
1242
1185
  <ActivityIndicator size="large" color={PRIMARY_GREEN} />
1243
- <Text style={{ color: PRIMARY_GREEN, marginTop: 8, fontSize: 14 }}>{rotationLabel ?? 'Rotation...'}</Text>
1186
+ <Text style={{ color: PRIMARY_GREEN, marginTop: 8 * responsiveScale, fontSize: Math.max(12, Math.round(14 * responsiveScale)) }}>{rotationLabel ?? 'Rotation...'}</Text>
1244
1187
  </View>
1245
1188
  )}
1246
1189
  </View>
1190
+ </View>
1247
1191
  )}
1248
1192
 
1249
- {/* ✅ Buttons positioned BELOW the image, not overlapping */}
1193
+ {/* ✅ Buttons positioned as a responsive bottom bar (scale with screen size) */}
1250
1194
  {!showResult && image && (
1251
- <View style={[styles.buttonContainerBelow, { paddingBottom: Math.max(insets.bottom, 16) }]}>
1195
+ <View style={[
1196
+ styles.buttonContainerBelow,
1197
+ {
1198
+ paddingBottom: Math.max(insets.bottom, Math.round(16 * responsiveScale)),
1199
+ paddingTop: Math.round(12 * responsiveScale),
1200
+ paddingHorizontal: Math.round(12 * responsiveScale),
1201
+ gap: Math.round(8 * responsiveScale),
1202
+ },
1203
+ ]}>
1252
1204
  {Platform.OS === 'android' && (
1253
1205
  <TouchableOpacity
1254
- style={[styles.rotationButton, isRotating && { opacity: 0.7 }]}
1206
+ style={[
1207
+ styles.rotationButton,
1208
+ {
1209
+ width: Math.round(56 * responsiveScale),
1210
+ height: Math.round(48 * responsiveScale),
1211
+ borderRadius: Math.round(28 * responsiveScale),
1212
+ },
1213
+ isRotating && { opacity: 0.7 },
1214
+ ]}
1255
1215
  onPress={() => enableRotation && rotatePreviewImage(90)}
1256
1216
  disabled={isRotating}
1257
1217
  >
1258
1218
  {isRotating ? (
1259
1219
  <ActivityIndicator size="small" color="white" />
1260
1220
  ) : (
1261
- <Ionicons name="sync" size={24} color="white" />
1221
+ <Ionicons name="sync" size={Math.round(24 * responsiveScale)} color="white" />
1262
1222
  )}
1263
1223
  </TouchableOpacity>
1264
1224
  )}
1265
1225
 
1266
- <TouchableOpacity style={styles.button} onPress={handleReset}>
1267
- <Text style={styles.buttonText}>Reset</Text>
1226
+ <TouchableOpacity
1227
+ style={[
1228
+ styles.button,
1229
+ {
1230
+ minHeight: Math.round(48 * responsiveScale),
1231
+ paddingVertical: Math.round(12 * responsiveScale),
1232
+ paddingHorizontal: Math.round(10 * responsiveScale),
1233
+ },
1234
+ ]}
1235
+ onPress={handleReset}
1236
+ >
1237
+ <Text style={[styles.buttonText, { fontSize: Math.max(14, Math.round(18 * responsiveScale)) }]}>Reset</Text>
1268
1238
  </TouchableOpacity>
1269
1239
 
1270
1240
  <TouchableOpacity
1271
- style={styles.button}
1272
1241
  onPress={async () => {
1273
1242
  setIsLoading(true);
1274
1243
  try {
@@ -1399,8 +1368,16 @@ const rotatePreviewImage = async (degrees) => {
1399
1368
  setShowFullScreenCapture(false);
1400
1369
  }
1401
1370
  }}
1371
+ style={[
1372
+ styles.button,
1373
+ {
1374
+ minHeight: Math.round(48 * responsiveScale),
1375
+ paddingVertical: Math.round(12 * responsiveScale),
1376
+ paddingHorizontal: Math.round(10 * responsiveScale),
1377
+ },
1378
+ ]}
1402
1379
  >
1403
- <Text style={styles.buttonText}>Confirm</Text>
1380
+ <Text style={[styles.buttonText, { fontSize: Math.max(14, Math.round(18 * responsiveScale)) }]}>Confirm</Text>
1404
1381
  </TouchableOpacity>
1405
1382
  </View>
1406
1383
  )}
@@ -1411,7 +1388,7 @@ const rotatePreviewImage = async (degrees) => {
1411
1388
  <Text style={styles.welcomeText}>Sélectionnez une image</Text>
1412
1389
  </View>
1413
1390
  )}
1414
- </>
1391
+ </View>
1415
1392
  )}
1416
1393
 
1417
1394
  {/* ✅ CORRECTION : Vue de masque temporaire pour la capture
@@ -11,20 +11,36 @@ 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 occupies all space above the buttons
29
+ imageRegion: {
30
+ flex: 1,
31
+ width: '100%',
32
+ alignSelf: 'stretch',
33
+ justifyContent: 'flex-start',
34
+ alignItems: 'stretch',
35
+ },
36
+ // Crop preview: fill all available space in imageRegion
19
37
  commonWrapper: {
20
- aspectRatio: 9 / 16,
21
- borderRadius: 30,
38
+ flex: 1,
39
+ width: '100%',
22
40
  overflow: 'hidden',
23
41
  alignItems: 'center',
24
42
  justifyContent: 'center',
25
- position: 'relative',
26
43
  backgroundColor: 'black',
27
- marginBottom: 0,
28
44
  },
29
45
  buttonContainer: {
30
46
  position: 'absolute',
@@ -40,18 +56,14 @@ const styles = StyleSheet.create({
40
56
  gap: 10,
41
57
  },
42
58
  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
59
  flexDirection: 'row',
49
- justifyContent: 'center',
50
60
  alignItems: 'center',
51
- paddingHorizontal: 10,
52
- paddingTop: 16,
61
+ justifyContent: 'space-between',
62
+ width: '100%',
63
+ paddingHorizontal: 12,
64
+ paddingTop: 12,
53
65
  gap: 8,
54
- backgroundColor: DEEP_BLACK, // Fond noir pour séparer visuellement
66
+ backgroundColor: DEEP_BLACK,
55
67
  },
56
68
  iconButton: {
57
69
  backgroundColor: PRIMARY_GREEN,
@@ -103,10 +115,10 @@ const styles = StyleSheet.create({
103
115
  },
104
116
 
105
117
  image: {
106
- width: '100%',
107
- height: '100%',
108
- resizeMode: 'contain',
109
- },
118
+ width: '100%',
119
+ height: '100%',
120
+ resizeMode: 'contain',
121
+ },
110
122
 
111
123
  overlay: {
112
124
  position: 'absolute',