85 lines
3.2 KiB
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;
|
|
}
|