/// Service de recadrage d'images. /// /// Permet de recadrer une image en format carré (1:1) et de la sauvegarder /// dans un fichier temporaire pour utilisation ultérieure. library; import 'dart:io'; import 'dart:math' as math; import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; /// Représente une zone de recadrage normalisée (0.0 à 1.0) class CropRect { /// Position X du coin supérieur gauche (0.0 à 1.0) final double x; /// Position Y du coin supérieur gauche (0.0 à 1.0) final double y; /// Largeur de la zone (0.0 à 1.0) final double width; /// Hauteur de la zone (0.0 à 1.0) final double height; const CropRect({ required this.x, required this.y, required this.width, required this.height, }); @override String toString() => 'CropRect(x: $x, y: $y, w: $width, h: $height)'; } /// Service pour recadrer les images class ImageCropService { final Uuid _uuid = const Uuid(); /// Taille de sortie maximale pour les images recadrées static const int maxOutputSize = 1024; /// Recadre une image en format carré /// /// [sourcePath] - Chemin vers l'image source /// [cropRect] - Zone de recadrage normalisée (0.0 à 1.0) /// [outputSize] - Taille de sortie en pixels (carré) /// /// Retourne le chemin vers l'image recadrée dans le dossier temporaire Future cropToSquare( String sourcePath, CropRect cropRect, { int outputSize = maxOutputSize, }) async { // Charger l'image source final file = File(sourcePath); final bytes = await file.readAsBytes(); final originalImage = img.decodeImage(bytes); if (originalImage == null) { throw Exception('Impossible de décoder l\'image: $sourcePath'); } // Calculer les coordonnées en pixels final srcX = (cropRect.x * originalImage.width).round(); final srcY = (cropRect.y * originalImage.height).round(); final srcWidth = (cropRect.width * originalImage.width).round(); final srcHeight = (cropRect.height * originalImage.height).round(); // S'assurer que les dimensions sont valides final clampedX = srcX.clamp(0, originalImage.width - 1); final clampedY = srcY.clamp(0, originalImage.height - 1); final clampedWidth = math.min(srcWidth, originalImage.width - clampedX); final clampedHeight = math.min(srcHeight, originalImage.height - clampedY); // Recadrer l'image img.Image cropped = img.copyCrop( originalImage, x: clampedX, y: clampedY, width: clampedWidth, height: clampedHeight, ); // Redimensionner à la taille de sortie si nécessaire if (cropped.width != outputSize || cropped.height != outputSize) { cropped = img.copyResize( cropped, width: outputSize, height: outputSize, interpolation: img.Interpolation.cubic, ); } // Sauvegarder dans le dossier temporaire final tempDir = await getTemporaryDirectory(); final fileName = 'cropped_${_uuid.v4()}.jpg'; final outputPath = '${tempDir.path}/$fileName'; final outputFile = File(outputPath); await outputFile.writeAsBytes(img.encodeJpg(cropped, quality: 90)); return outputPath; } /// Calcule la zone de recadrage carrée maximale centrée sur l'image /// /// [imageWidth] - Largeur de l'image en pixels /// [imageHeight] - Hauteur de l'image en pixels /// /// Retourne un CropRect normalisé pour un carré centré CropRect getDefaultSquareCrop(int imageWidth, int imageHeight) { final aspectRatio = imageWidth / imageHeight; if (aspectRatio > 1) { // Image plus large que haute - centrer horizontalement final squareWidth = imageHeight / imageWidth; final x = (1 - squareWidth) / 2; return CropRect(x: x, y: 0, width: squareWidth, height: 1); } else if (aspectRatio < 1) { // Image plus haute que large - centrer verticalement final squareHeight = imageWidth / imageHeight; final y = (1 - squareHeight) / 2; return CropRect(x: 0, y: y, width: 1, height: squareHeight); } else { // Déjà carré return const CropRect(x: 0, y: 0, width: 1, height: 1); } } /// Nettoie les fichiers temporaires de crop anciens Future cleanupOldCrops({Duration maxAge = const Duration(hours: 24)}) async { try { final tempDir = await getTemporaryDirectory(); final dir = Directory(tempDir.path); final now = DateTime.now(); await for (final entity in dir.list()) { if (entity is File && entity.path.contains('cropped_')) { final stat = await entity.stat(); final age = now.difference(stat.modified); if (age > maxAge) { await entity.delete(); } } } } catch (e) { // Ignorer les erreurs de nettoyage } } }