Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user