diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index da4faee5..f885d72b 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -884,16 +884,23 @@ class _CaptureScreenState extends State } } - /// Réduit fortement la résolution de la photo pour accélérer tous les - /// écrans suivants (crop, calibration, plotting). 1080 px de côté max - /// + JPEG qualité 70 : largement suffisant pour la détection visuelle. + /// Recadre la photo sur la zone du viseur PUIS réduit sa résolution, en un + /// seul décodage dans un Isolate (thread UI fluide). /// - /// Baisser `maxSide` (ex. 900) ou `quality` (ex. 60) rend le chargement - /// encore plus rapide au prix d'un peu de finesse à l'écran. + /// 1) Recadrage viseur : carré centré à 85 % du petit côté de la photo. C'est + /// EXACTEMENT le cadre des 4 coins verts/orange affiché à la capture + /// (`SizedBox` de `0.85 * largeur écran`, centré, l'aperçu caméra remplissant + /// la largeur). → l'écran de centrage ne reçoit QUE ce qui était visé entre + /// les coins, sans le fond capturé autour : plus d'impression de « dézoom ». + /// 2) Dégradation résolution : 1080 px de côté max + JPEG qualité 70, largement + /// suffisant pour la détection visuelle et bien plus rapide à recharger. + /// + /// Baisser `maxSide` (ex. 900) ou `quality` (ex. 60) accélère encore au prix + /// d'un peu de finesse à l'écran. Future _downscaleForPipeline(String sourcePath) async { // On résout le chemin de sortie sur le thread UI (path_provider a besoin - // du canal de plateforme), puis tout le travail lourd (décodage, resize, - // réencodage JPEG) part dans un Isolate → aucun gel du thread UI. + // du canal de plateforme), puis tout le travail lourd (décodage, crop, + // resize, réencodage JPEG) part dans un Isolate → aucun gel du thread UI. final tempDir = await getTemporaryDirectory(); final outPath = '${tempDir.path}/downscaled_${DateTime.now().millisecondsSinceEpoch}.jpg'; @@ -903,21 +910,34 @@ class _CaptureScreenState extends State final decoded = img.decodeImage(bytes); if (decoded == null) return sourcePath; - const int maxSide = 1080; - final int longest = math.max(decoded.width, decoded.height); - if (longest <= maxSide) { - return sourcePath; // déjà assez petite, rien à faire - } - - final double ratio = maxSide / longest; - final resized = img.copyResize( + // 1) Recadrage sur la zone du viseur (carré centré à 85 % du petit côté). + final int side = (math.min(decoded.width, decoded.height) * 0.85).round(); + final int cropX = + ((decoded.width - side) / 2).round().clamp(0, decoded.width - 1); + final int cropY = + ((decoded.height - side) / 2).round().clamp(0, decoded.height - 1); + img.Image result = img.copyCrop( decoded, - width: (decoded.width * ratio).round(), - height: (decoded.height * ratio).round(), - interpolation: img.Interpolation.linear, // rapide + x: cropX, + y: cropY, + width: math.min(side, decoded.width - cropX), + height: math.min(side, decoded.height - cropY), ); - File(outPath).writeAsBytesSync(img.encodeJpg(resized, quality: 70)); + // 2) Dégradation résolution si nécessaire. + const int maxSide = 1080; + final int longest = math.max(result.width, result.height); + if (longest > maxSide) { + final double ratio = maxSide / longest; + result = img.copyResize( + result, + width: (result.width * ratio).round(), + height: (result.height * ratio).round(), + interpolation: img.Interpolation.linear, // rapide + ); + } + + File(outPath).writeAsBytesSync(img.encodeJpg(result, quality: 70)); return outPath; }); } diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index cabcb378..6993f36e 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -101,17 +101,16 @@ class _CropScreenState extends State { ? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8))) : Column( children: [ - // Zone interactive de crop. Le cadre épouse le ratio réel de la photo - // (AspectRatio) → la photo le remplit bord à bord, sans bande noire de - // letterbox. La fenêtre de visée carrée reste centrée à l'intérieur. + // Zone interactive de crop, CARRÉE. La photo la remplit entièrement + // (BoxFit.cover) → aucun bord noir dans la zone. Le débord hors cadre + // est récupérable en déplaçant/zoomant. La sortie d'analyse reste + // carrée, donc la cible n'est pas déformée. Expanded( child: Padding( padding: const EdgeInsets.all(20), child: Center( child: AspectRatio( - aspectRatio: (_imageSize != null && _imageSize!.height > 0) - ? _imageSize!.width / _imageSize!.height - : 1.0, + aspectRatio: 1.0, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), @@ -249,21 +248,9 @@ class _CropScreenState extends State { builder: (context, constraints) { _viewportSize = Size(constraints.maxWidth, constraints.maxHeight); -// 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; + // La photo remplit toute la zone carrée (BoxFit.cover). La fenêtre de + // visée = toute la zone visible → aucun bord noir autour du cadre. + _cropSize = math.min(_viewportSize.width, _viewportSize.height); if (_scale == 1.0 && _offset == Offset.zero && _rotation == 0.0) { _initializeImagePosition(); @@ -285,7 +272,7 @@ class _CropScreenState extends State { alignment: Alignment.center, child: Image.file( File(widget.imagePath), - fit: BoxFit.contain, + fit: BoxFit.cover, width: _viewportSize.width, height: _viewportSize.height, ), @@ -385,14 +372,13 @@ class _CropScreenState extends State { Future _onCropConfirm() async { setState(() => _isLoading = true); try { - // Facteur d'échelle affichage/source (BoxFit.contain) — identique à - // celui utilisé pour afficher l'image dans l'aperçu. - final imageAspect = _imageSize!.width / _imageSize!.height; - final viewportAspect = _viewportSize.width / _viewportSize.height; - final double displayWidth = imageAspect > viewportAspect - ? _viewportSize.width - : _viewportSize.height * imageAspect; - final double displayPerSourcePx = displayWidth / _imageSize!.width; + // Facteur d'échelle affichage/source (BoxFit.cover) — identique à celui + // utilisé pour afficher l'image dans l'aperçu : l'image remplit la zone, + // le débord est rogné, donc l'échelle est le MAX des deux ratios d'axe. + final double displayPerSourcePx = math.max( + _viewportSize.width / _imageSize!.width, + _viewportSize.height / _imageSize!.height, + ); // Découpe calée sur la fenêtre de visée : le DÉPLACEMENT (pan) et la // ROTATION sont pris en compte, le ZOOM est ignoré, et les débordements