manager-app/lib/Helpers/ImageCompressor.dart

85 lines
3.2 KiB
Dart

import 'dart:typed_data';
import 'package:image/image.dart' as img;
/// Compression des images à l'upload.
///
/// Un seul point d'entrée, appelé par les **deux** chemins d'upload de
/// `resources_screen`. Côté serveur, ces deux chemins avaient divergé sur
/// `StoragePath` puis sur le contrôle de quota (C1 et C3) : la même erreur
/// coûterait ici des images compressées d'un côté et pas de l'autre.
class ImageCompressor {
/// Côté le plus long. Au-delà, une photo n'apporte rien à l'écran d'un
/// visiteur et coûte du stockage, du transfert et du temps de visite hors ligne.
static const int maxDimension = 2560;
static const int jpegQuality = 82;
static const List<String> _compressibleExtensions = ['jpg', 'jpeg', 'png'];
static bool handles(String? extension) =>
extension != null && _compressibleExtensions.contains(extension.toLowerCase());
/// Renvoie les octets à téléverser et le type MIME correspondant.
///
/// L'original est renvoyé tel quel si le format n'est pas gérable, si le
/// décodage échoue, ou si la compression n'a rien gagné — une image déjà
/// petite et bien encodée peut grossir en repassant par un ré-encodage.
static CompressedImage compress(Uint8List bytes, String? extension, String fallbackMimeType) {
if (!handles(extension)) {
return CompressedImage(bytes, fallbackMimeType, false);
}
try {
final decoded = img.decodeImage(bytes);
if (decoded == null) {
return CompressedImage(bytes, fallbackMimeType, false);
}
final resized = (decoded.width > maxDimension || decoded.height > maxDimension)
? img.copyResize(
decoded,
width: decoded.width >= decoded.height ? maxDimension : null,
height: decoded.height > decoded.width ? maxDimension : null,
interpolation: img.Interpolation.average,
)
: decoded;
// Un PNG à canal alpha reste un PNG : le passer en JPEG remplirait la
// transparence de noir, ce qui abîmerait les logos et les filigranes.
final keepsAlpha = decoded.hasAlpha;
final encoded = keepsAlpha
? Uint8List.fromList(img.encodePng(resized))
: Uint8List.fromList(img.encodeJpg(resized, quality: jpegQuality));
if (encoded.length >= bytes.length) {
return CompressedImage(bytes, fallbackMimeType, false,
width: decoded.width, height: decoded.height);
}
return CompressedImage(encoded, keepsAlpha ? 'image/png' : 'image/jpeg', true,
width: resized.width, height: resized.height);
} catch (e) {
print("ERROR ImageCompressor.compress");
print(e);
return CompressedImage(bytes, fallbackMimeType, false);
}
}
}
class CompressedImage {
final Uint8List bytes;
final String mimeType;
final bool wasCompressed;
/// Dimensions de l'image telle qu'elle sera stockee. Nulles quand le format
/// n'est pas decodable ici : la Mediatheque n'affiche alors pas de dimensions.
final int? width;
final int? height;
CompressedImage(this.bytes, this.mimeType, this.wasCompressed,
{this.width, this.height});
int get sizeBytes => bytes.length;
}