diff --git a/lib/Helpers/ImageCompressor.dart b/lib/Helpers/ImageCompressor.dart new file mode 100644 index 0000000..4a9cb7a --- /dev/null +++ b/lib/Helpers/ImageCompressor.dart @@ -0,0 +1,76 @@ +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 _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); + } + + return CompressedImage(encoded, keepsAlpha ? 'image/png' : 'image/jpeg', true); + } catch (e) { + print("ERROR ImageCompressor.compress"); + print(e); + return CompressedImage(bytes, fallbackMimeType, false); + } + } +} + +class CompressedImage { + final Uint8List bytes; + final String mimeType; + final bool wasCompressed; + + CompressedImage(this.bytes, this.mimeType, this.wasCompressed); + + int get sizeBytes => bytes.length; +} diff --git a/lib/Screens/Resources/resources_screen.dart b/lib/Screens/Resources/resources_screen.dart index 3d3e8cc..d4192ac 100644 --- a/lib/Screens/Resources/resources_screen.dart +++ b/lib/Screens/Resources/resources_screen.dart @@ -6,6 +6,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:manager_app/Components/common_loader.dart'; +import 'package:manager_app/Helpers/ImageCompressor.dart'; import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/Models/managerContext.dart'; @@ -207,7 +208,11 @@ Future?> create(ResourceDTO resourceDTO, List? files, L print("Trying to create resource"); // ADD LOADING resourceDTO.dateCreation = DateTime.now(); - resourceDTO.sizeBytes = platformFile.size; + // Compresser AVANT resourceCreate : le contrôle de quota du serveur (C3) + // se fait sur sizeBytes à la création, il doit voir la taille réellement + // téléversée et non celle du fichier d'origine. + final compressed = ImageCompressor.compress(platformFile.bytes!, platformFile.extension, mimeType); + resourceDTO.sizeBytes = compressed.sizeBytes; ResourceDTO? newResource = await managerAppContext.clientAPI!.resourceApi!.resourceCreate(resourceDTO); print("created resource"); print(newResource); @@ -216,15 +221,16 @@ Future?> create(ResourceDTO resourceDTO, List? files, L Reference ref = storage.ref().child('pictures/${appContext.getContext().instanceId}/${Path.basename(newResource.id!.toString())}'); var metadata = SettableMetadata( - contentType: mimeType, // ou le type MIME approprié pour votre fichier + // Le MIME suit la compression : un PNG sans alpha ressort en JPEG. + contentType: compressed.mimeType, ); - UploadTask uploadTask = ref.putData(platformFile.bytes!, metadata); + UploadTask uploadTask = ref.putData(compressed.bytes, metadata); uploadTask.then((res) { res.ref.getDownloadURL().then((urlRessource) { showNotification(Colors.green, kWhite, AppLocalizations.of(context)!.resourceCreatedSuccess, context, null); newResource.url = urlRessource; - newResource.sizeBytes = platformFile.size; + newResource.sizeBytes = compressed.sizeBytes; (appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceUpdate(newResource); createdResources.add(newResource); index++; @@ -303,11 +309,21 @@ Future?> create(ResourceDTO resourceDTO, List? files, L FirebaseStorage storage = FirebaseStorage.instance; Reference ref = storage.ref().child('pictures/${appContext.getContext().instanceId}/${Path.basename(newResource.id!.toString())}'); - UploadTask uploadTask = ref.putData(platformFile.bytes!); + final compressed = ImageCompressor.compress( + platformFile.bytes!, + platformFile.extension, + 'application/octet-stream', + ); + + UploadTask uploadTask = ref.putData( + compressed.bytes, + SettableMetadata(contentType: compressed.mimeType), + ); uploadTask.then((res) { res.ref.getDownloadURL().then((urlImage) { showNotification(Colors.green, kWhite, 'La ressource a été créée avec succès', context, null); newResource.url = urlImage; + newResource.sizeBytes = compressed.sizeBytes; (appContext.getContext() as ManagerAppContext).clientAPI!.resourceApi!.resourceUpdate(newResource); return newResource; }); diff --git a/pubspec.lock b/pubspec.lock index e2d5c5d..9c5446b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -822,7 +822,7 @@ packages: source: hosted version: "1.0.1" image: - dependency: transitive + dependency: "direct main" description: name: image sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" diff --git a/pubspec.yaml b/pubspec.yaml index 103d50d..0d4c32d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: collection: any #filepicker_windows: ^2.0.0 file_picker: ^6.1.1 + image: ^4.2.0 # compression des images à l'upload (C4) — était déjà là en transitif flare_flutter: ^3.0.1 flutter_svg: ^2.2.1 #dart_vlc: ^0.0.6