using Microsoft.Extensions.Logging.Abstractions; using Omika.App.Local.Abstractions; using Omika.App.Services; using SkiaSharp; namespace Omika.Tests.Mobile.Local; /// /// Tests de l'optimisation réelle du poids des images (SkiaSharp). /// Vérifie que la limite de poids est respectée, que le format est conservé et /// que l'échec n'empêche jamais la création du document. /// public class SkiaImageOptimizerTests : IDisposable { private readonly string _directory; private readonly SkiaImageOptimizer _optimizer = new(NullLogger.Instance); public SkiaImageOptimizerTests() { _directory = Path.Combine(Path.GetTempPath(), "omika-image-tests", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_directory); } public void Dispose() { if (Directory.Exists(_directory)) { Directory.Delete(_directory, recursive: true); } GC.SuppressFinalize(this); } /// Bitmap à fort entropie : le JPEG reste volumineux, ce qui force l'optimisation. private static SKBitmap CreateNoisyBitmap(int width, int height, int seed = 12345) { var bitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul)); var random = new Random(seed); var pixels = new SKColor[width * height]; for (int i = 0; i < pixels.Length; i++) { byte value = (byte)random.Next(256); pixels[i] = new SKColor(value, (byte)(value ^ 0x5A), (byte)(255 - value)); } bitmap.Pixels = pixels; return bitmap; } private string WriteJpeg(string name, int width, int height) { using SKBitmap bitmap = CreateNoisyBitmap(width, height); using SKImage image = SKImage.FromBitmap(bitmap); using SKData data = image.Encode(SKEncodedImageFormat.Jpeg, quality: 100); string path = Path.Combine(_directory, name); File.WriteAllBytes(path, data.ToArray()); return path; } private string WritePng(string name, int width, int height) { using SKBitmap bitmap = CreateNoisyBitmap(width, height, seed: 777); using SKImage image = SKImage.FromBitmap(bitmap); using SKData data = image.Encode(SKEncodedImageFormat.Png, quality: 100); string path = Path.Combine(_directory, name); File.WriteAllBytes(path, data.ToArray()); return path; } [Fact] public async Task ImageSousLaLimite_AucuneModification() { // Arrange : limite très large, l'image est déjà conforme. string source = WriteJpeg("petite.jpg", 200, 150); string destination = Path.Combine(_directory, "out.jpg"); byte[] original = await File.ReadAllBytesAsync(source); // Act ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "petite.jpg", "image/jpeg", maxSizeKb: 10240); // Assert : fichier copié octet pour octet, rien n'est réencodé. Assert.False(result.WasOptimized); Assert.Equal(original, await File.ReadAllBytesAsync(destination)); } [Fact] public async Task ImageAuDessusDeLaLimite_OptimiseeSousLaLimite() { // Arrange : image volumineuse, limite serrée. string source = WriteJpeg("grande.jpg", 1200, 900); string destination = Path.Combine(_directory, "out.jpg"); long originalBytes = new FileInfo(source).Length; const int limiteKo = 120; // Prérequis du test : l'image dépasse bien la limite. Assert.True(originalBytes > limiteKo * 1024L, $"L'image de test devrait dépasser la limite ({originalBytes} octets)."); // Act ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "grande.jpg", "image/jpeg", limiteKo); // Assert : la limite de poids est respectée, et il y a bien eu réencodage. Assert.True(result.WasOptimized); Assert.True(result.FinalBytes <= limiteKo * 1024L, $"Taille finale {result.FinalBytes} octets > limite {limiteKo * 1024L} octets."); Assert.Equal(result.FinalBytes, new FileInfo(destination).Length); Assert.NotNull(result.Quality); // Le résultat reste une image JPEG valide. using SKBitmap? decoded = SKBitmap.Decode(destination); Assert.NotNull(decoded); Assert.True(decoded.Width > 0 && decoded.Height > 0); } [Fact] public async Task ImageAuDessusDeLaLimite_RapportDAspectConserve() { // Arrange : source 4:3. string source = WriteJpeg("ratio.jpg", 1200, 900); string destination = Path.Combine(_directory, "out.jpg"); // Act ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "ratio.jpg", "image/jpeg", 120); // Assert : l'orientation reste paysage, le rapport est préservé à l'arrondi près. Assert.True(result.FinalWidth >= result.FinalHeight); double ratioSource = 1200d / 900d; double ratioFinal = (double)result.FinalWidth / result.FinalHeight; Assert.True(Math.Abs(ratioSource - ratioFinal) < 0.05, $"Rapport d'aspect modifié : {ratioSource:0.###} → {ratioFinal:0.###}."); } [Fact] public async Task Png_RestePng() { // Arrange : un PNG ne doit jamais être converti en JPEG. string source = WritePng("image.png", 300, 200); string destination = Path.Combine(_directory, "out.png"); // Act await _optimizer.OptimizeFileAsync(source, destination, "image.png", "image/png", 40); // Assert : signature PNG conservée (89 50 4E 47). byte[] bytes = await File.ReadAllBytesAsync(destination); Assert.True(bytes.Length > 8); Assert.Equal(0x89, bytes[0]); Assert.Equal(0x50, bytes[1]); Assert.Equal(0x4E, bytes[2]); Assert.Equal(0x47, bytes[3]); } [Fact] public async Task FormatNonSupporte_ConserveTelQuel() { // Arrange : un HEIC n'est pas décodable par Skia : il doit être conservé. string source = Path.Combine(_directory, "photo.heic"); byte[] content = [1, 2, 3, 4, 5]; await File.WriteAllBytesAsync(source, content); string destination = Path.Combine(_directory, "out.heic"); // Act ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "photo.heic", "image/heic", 100); // Assert Assert.False(result.WasOptimized); Assert.True(result.FormatNotOptimizable); Assert.Equal(content, await File.ReadAllBytesAsync(destination)); } [Fact] public async Task AucuneLimite_FichierCopie() { // Arrange string source = WriteJpeg("libre.jpg", 300, 200); string destination = Path.Combine(_directory, "out.jpg"); byte[] original = await File.ReadAllBytesAsync(source); // Act ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "libre.jpg", "image/jpeg", maxSizeKb: 0); // Assert Assert.False(result.WasOptimized); Assert.Equal(original, await File.ReadAllBytesAsync(destination)); } [Fact] public async Task FichierIllisible_ConserveEtNeLevePas() { // Arrange : contenu corrompu portant une extension image. string source = Path.Combine(_directory, "corrompu.jpg"); byte[] content = [0xFF, 0x00, 0x11, 0x22, 0x33, 0x44]; await File.WriteAllBytesAsync(source, content); string destination = Path.Combine(_directory, "out.jpg"); // Act : ne doit jamais lever, l'utilisateur continue de travailler. ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "corrompu.jpg", "image/jpeg", 10); // Assert : le document reste exploitable avec le contenu d'origine. Assert.False(result.WasOptimized); Assert.True(File.Exists(destination)); Assert.Equal(content, await File.ReadAllBytesAsync(destination)); } [Fact] public async Task DestinationToujoursProduite_QuelQueSoitLeCas() { // Arrange : un cas par famille. string jpeg = WriteJpeg("a.jpg", 300, 200); string pdf = Path.Combine(_directory, "d.pdf"); await File.WriteAllBytesAsync(pdf, [1, 2, 3]); // Act / Assert : la destination existe dans tous les cas. foreach ((string source, string name, string mime) in new[] { (jpeg, "a.jpg", "image/jpeg"), (pdf, "d.pdf", "application/pdf") }) { string destination = Path.Combine(_directory, $"dest-{name}"); await _optimizer.OptimizeFileAsync(source, destination, name, mime, 50); Assert.True(File.Exists(destination), $"Destination absente pour {name}."); } } [Fact] public async Task FichierNonImage_NonOptimise() { // Arrange : extension non image, même avec un type MIME ambigu. string source = Path.Combine(_directory, "archive.zip"); byte[] content = [9, 8, 7]; await File.WriteAllBytesAsync(source, content); string destination = Path.Combine(_directory, "out.zip"); // Act ImageOptimizationResult result = await _optimizer.OptimizeFileAsync( source, destination, "archive.zip", "application/zip", 10); // Assert Assert.False(result.WasOptimized); Assert.True(result.FormatNotOptimizable); Assert.Equal(content, await File.ReadAllBytesAsync(destination)); } }