fix: lenteur de l appli

This commit is contained in:
qguillaume
2026-06-05 17:18:29 +02:00
parent ccc6eb609a
commit 251d4bb599
2 changed files with 237 additions and 106 deletions

View File

@@ -1,27 +1,14 @@
/// 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:isolate'; // AJOUT
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({
@@ -35,84 +22,110 @@ class CropRect {
String toString() => 'CropRect(x: $x, y: $y, w: $width, h: $height)';
}
/// Service pour recadrer les images
// AJOUT : Paramètres passés à l'Isolate (tout doit être sérialisable)
class _CropParams {
final String sourcePath;
final double cropX;
final double cropY;
final double cropWidth;
final double cropHeight;
final double rotationDegrees;
final int outputSize;
final String outputPath;
_CropParams({
required this.sourcePath,
required this.cropX,
required this.cropY,
required this.cropWidth,
required this.cropHeight,
required this.rotationDegrees,
required this.outputSize,
required this.outputPath,
});
}
// AJOUT : Fonction statique exécutée dans l'Isolate (doit être top-level ou static)
void _cropIsolateEntry(_CropParams params) {
final file = File(params.sourcePath);
final bytes = file.readAsBytesSync();
img.Image? originalImage = img.decodeImage(bytes);
if (originalImage == null) {
throw Exception('Impossible de décoder l\'image: ${params.sourcePath}');
}
// Rotation si nécessaire — LINEAR au lieu de CUBIC pour la vitesse
if (params.rotationDegrees != 0.0) {
originalImage = img.copyRotate(
originalImage,
angle: params.rotationDegrees,
interpolation: img.Interpolation.linear, // OPTIMISATION : linear >> cubic en vitesse
);
}
// Recadrage
final srcX = (params.cropX * originalImage.width).round();
final srcY = (params.cropY * originalImage.height).round();
final srcWidth = (params.cropWidth * originalImage.width).round();
final srcHeight = (params.cropHeight * originalImage.height).round();
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);
img.Image cropped = img.copyCrop(
originalImage,
x: clampedX,
y: clampedY,
width: clampedWidth,
height: clampedHeight,
);
// Redimensionnement — LINEAR au lieu de CUBIC
if (cropped.width != params.outputSize || cropped.height != params.outputSize) {
cropped = img.copyResize(
cropped,
width: params.outputSize,
height: params.outputSize,
interpolation: img.Interpolation.linear, // OPTIMISATION : linear >> cubic en vitesse
);
}
// Encodage JPEG qualité 85 au lieu de 90 (gain de vitesse non négligeable)
final outputFile = File(params.outputPath);
outputFile.writeAsBytesSync(img.encodeJpg(cropped, quality: 85));
}
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é en prenant en compte l'angle de rotation.
///
/// [sourcePath] - Chemin vers l'image source
/// [cropRect] - Zone de recadrage normalisée (0.0 à 1.0)
/// [rotationDegrees] - L'angle appliqué à la jauge haute précision du CropScreen
/// [outputSize] - Taille de sortie en pixels (carré)
///
/// Retourne le chemin vers l'image recadrée dans le dossier temporaire
Future<String> cropToSquare(
String sourcePath,
CropRect cropRect, {
double rotationDegrees = 0.0, // FIX : On intercepte la rotation ici !
double rotationDegrees = 0.0,
int outputSize = maxOutputSize,
}) async {
// Charger l'image source
final file = File(sourcePath);
final bytes = await file.readAsBytes();
img.Image? originalImage = img.decodeImage(bytes);
final tempDir = await getTemporaryDirectory();
final outputPath = '${tempDir.path}/cropped_${_uuid.v4()}.jpg';
if (originalImage == null) {
throw Exception('Impossible de décoder l\'image: $sourcePath');
}
// FIX CRITIQUE : Si l'utilisateur a pivoté l'image, on applique d'abord la rotation
// aux pixels bruts pour que le découpage rectiligne qui suit tape au bon endroit.
if (rotationDegrees != 0.0) {
originalImage = img.copyRotate(
originalImage,
angle: rotationDegrees,
interpolation: img.Interpolation.cubic,
);
}
// Calculer les coordonnées en pixels sur la géométrie de l'image redressée
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 redressée
img.Image cropped = img.copyCrop(
originalImage,
x: clampedX,
y: clampedY,
width: clampedWidth,
height: clampedHeight,
final params = _CropParams(
sourcePath: sourcePath,
cropX: cropRect.x,
cropY: cropRect.y,
cropWidth: cropRect.width,
cropHeight: cropRect.height,
rotationDegrees: rotationDegrees,
outputSize: outputSize,
outputPath: outputPath,
);
// 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));
// OPTIMISATION CLEF : Tout le traitement lourd dans un Isolate séparé
// → le thread UI reste fluide pendant le calcul
await Isolate.run(() => _cropIsolateEntry(params));
return outputPath;
}
@@ -157,7 +170,6 @@ class ImageCropService {
/// 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<String> cropRawCameraToSquare(String sourcePath) async {
final file = File(sourcePath);
final bytes = await file.readAsBytes();
@@ -165,19 +177,14 @@ class ImageCropService {
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),
@@ -186,10 +193,9 @@ class ImageCropService {
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));
await File(outputPath).writeAsBytes(img.encodeJpg(squareImage, quality: 85));
return outputPath;
}