fix: lenteur de l appli
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user