From a5d0251545bef0c39333fdbfbbeeb33a9b30ce28 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Wed, 27 May 2026 17:44:57 +0200 Subject: [PATCH] =?UTF-8?q?fix(camera):=20synchroniser=20le=20ratio=20de?= =?UTF-8?q?=20vis=C3=A9e=203/4=20avec=20le=20capteur=20natif=20et=20corrig?= =?UTF-8?q?er=20le=20d=C3=A9calage=20du=20crop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/capture/capture_screen.dart | 22 +++++++++---- lib/features/crop/crop_screen.dart | 22 ++++++++++++- lib/services/image_crop_service.dart | 39 ++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index bc1979cf..eb87060e 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -13,7 +13,7 @@ import '../../data/models/target_type.dart'; import '../crop/crop_screen.dart'; import '../session/session_provider.dart'; import 'widgets/image_source_button.dart'; - +import '../../services/image_crop_service.dart'; class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @@ -23,10 +23,12 @@ class CaptureScreen extends StatefulWidget { class _CaptureScreenState extends State with SingleTickerProviderStateMixin { final ImagePicker _picker = ImagePicker(); + final ImageCropService _cropService = ImageCropService(); final TargetType _selectedType = TargetType.concentric; String? _selectedImagePath; bool _isLoading = false; + // AJOUT : Variables de gestion du flux caméra en direct CameraController? _cameraController; List? _cameras; @@ -76,11 +78,14 @@ class _CaptureScreenState extends State with SingleTickerProvider if (_cameras != null && _cameras!.isNotEmpty) { _cameraController = CameraController( _cameras![0], - ResolutionPreset.high, + ResolutionPreset.max, // Force le 4:3 natif au maximum du capteur enableAudio: false, + imageFormatGroup: ImageFormatGroup.jpeg, ); + // INITIALISATION UNIQUE ET PROPRE : await _cameraController!.initialize(); + setState(() { _isCameraInitialized = true; _showLiveCamera = true; @@ -215,7 +220,12 @@ class _CaptureScreenState extends State with SingleTickerProvider fit: StackFit.expand, children: [ // 1. Le flux vidéo de la caméra - CameraPreview(_cameraController!), + Center( + child: AspectRatio( + aspectRatio: 3 / 4, // Aligné sur le format natif du capteur photo + child: CameraPreview(_cameraController!), + ), + ), // 2. L'ASSISTANT DE CADRAGE VIRTUEL (Avec le grand cadre complet et la petite cible) Center( @@ -384,15 +394,15 @@ class _CaptureScreenState extends State with SingleTickerProvider setState(() => _isLoading = true); try { - // Prise de la photo + // 1. On prend la photo brute normale (sans y toucher) final XFile photo = await _cameraController!.takePicture(); setState(() { _selectedImagePath = photo.path; - _showLiveCamera = false; // Quitte la vue caméra + _showLiveCamera = false; }); - // Redirection automatique vers l'écran de Crop + // 2. On l'envoie directement à l'analyse _analyzeImage(); } catch (e) { debugPrint('Erreur lors du clic photo: $e'); diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index ebb7ee96..b350896c 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -237,7 +237,22 @@ class _CropScreenState extends State { return LayoutBuilder( builder: (context, constraints) { _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); - _cropSize = math.min(constraints.maxWidth, constraints.maxHeight) * 0.95; + +// FIX : On calcule d'abord la taille de la photo affichée avant de définir la taille du cadre vert ! + final imageAspect = _imageSize != null ? _imageSize!.width / _imageSize!.height : 1.0; + final viewportAspect = _viewportSize.width / _viewportSize.height; + + double displayWidth, displayHeight; + if (imageAspect > viewportAspect) { + displayWidth = _viewportSize.width; + displayHeight = _viewportSize.width / imageAspect; + } else { + displayHeight = _viewportSize.height; + displayWidth = _viewportSize.height * imageAspect; + } + +// On passe de 0.95 à 0.85 pour matcher parfaitement avec l'appareil photo ! + _cropSize = math.min(displayWidth, displayHeight)* 0.85; if (_scale == 1.0 && _offset == Offset.zero && _rotation == 0.0) { _initializeImagePosition(); @@ -333,6 +348,7 @@ class _CropScreenState extends State { final imageAspect = _imageSize!.width / _imageSize!.height; final viewportAspect = _viewportSize.width / _viewportSize.height; + // 1. Calcul strict de la taille de l'image affichée en BoxFit.contain double displayWidth, displayHeight; if (imageAspect > viewportAspect) { displayWidth = _viewportSize.width; @@ -344,11 +360,15 @@ class _CropScreenState extends State { final minDisplayDim = math.min(displayWidth, displayHeight); + // 2. Calcul du scale initial basé sur la dimension de l'image (et non du viewport) _scale = widget.initialScale ?? (_cropSize / minDisplayDim); if (_scale < 1.0 && widget.initialScale == null) _scale = 1.0; + // 3. Réinitialisation propre de l'offset au centre de la zone d'affichage if (widget.initialOffset != null) { _offset = widget.initialOffset!; + } else { + _offset = Offset.zero; // Force l'image à se centrer parfaitement sur la croix verte } } diff --git a/lib/services/image_crop_service.dart b/lib/services/image_crop_service.dart index 3daa3991..1ee7402b 100644 --- a/lib/services/image_crop_service.dart +++ b/lib/services/image_crop_service.dart @@ -154,4 +154,43 @@ class ImageCropService { // Ignorer les erreurs } } + + /// Coupe automatiquement une photo brute de l'appareil photo en carré + /// calé sur la position haute du viseur de l'écran. + /// Coupe une photo brute selon les proportions exactes du viseur de l'écran. + Future cropRawCameraToSquare(String sourcePath) async { + final file = File(sourcePath); + final bytes = await file.readAsBytes(); + final originalImage = img.decodeImage(bytes); + + if (originalImage == null) return sourcePath; + + // 1. Déterminer les dimensions réelles (gestion automatique de la rotation du capteur) + final int imgW = originalImage.width; + final int imgH = originalImage.height; + + // 2. Ton cadre à l'écran fait 85% de la largeur du viewport (0.85). + // On calcule la taille du carré par rapport à la plus petite dimension. + final int cropSizePixels = (math.min(imgW, imgH) * 0.85).round(); + + // 3. Calcul du centre parfait pour le x et le y + final int x = ((imgW - cropSizePixels) / 2).round(); + final int y = ((imgH - cropSizePixels) / 2).round(); + + // 4. Découpe physique au pixel près + final squareImage = img.copyCrop( + originalImage, + x: x.clamp(0, imgW - 1), + y: y.clamp(0, imgH - 1), + width: cropSizePixels, + height: cropSizePixels, + ); + + // 5. Sauvegarde + final tempDir = await getTemporaryDirectory(); + final outputPath = '${tempDir.path}/camera_square_${DateTime.now().millisecondsSinceEpoch}.jpg'; + await File(outputPath).writeAsBytes(img.encodeJpg(squareImage, quality: 90)); + + return outputPath; + } } \ No newline at end of file