253 lines
9.6 KiB
C#
253 lines
9.6 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Omika.App.Local.Abstractions;
|
|
using Omika.App.Services;
|
|
using SkiaSharp;
|
|
|
|
namespace Omika.Tests.Mobile.Local;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class SkiaImageOptimizerTests : IDisposable
|
|
{
|
|
private readonly string _directory;
|
|
private readonly SkiaImageOptimizer _optimizer = new(NullLogger<SkiaImageOptimizer>.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);
|
|
}
|
|
|
|
/// <summary>Bitmap à fort entropie : le JPEG reste volumineux, ce qui force l'optimisation.</summary>
|
|
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));
|
|
}
|
|
}
|