forked from Manuel/meeting-assistant
110 lines
4.5 KiB
C#
110 lines
4.5 KiB
C#
#if WINDOWS
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.Runtime.InteropServices;
|
|
using MeetingAssistant.Screenshots;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace MeetingAssistant.Tests;
|
|
|
|
public sealed class WindowsScreenshotImageCropperTests(ITestOutputHelper output)
|
|
{
|
|
[WindowsFact("System.Drawing screenshot cropping")]
|
|
public Task WindowsCropUsesOcrPixelCoordinates() =>
|
|
AssertCropAsync(new ScreenshotCropCoordinates(0, 0, 2, 2));
|
|
|
|
[WindowsFact("System.Drawing screenshot cropping")]
|
|
public Task WindowsCropPreservesExpectedPixelsAtNonzeroOrigin() =>
|
|
AssertCropAsync(new ScreenshotCropCoordinates(1, 1, 3, 2));
|
|
|
|
[WindowsFact("System.Drawing screenshot cropping")]
|
|
public async Task WindowsCropRejectsInvalidBounds()
|
|
{
|
|
using var fixture = new ImageFixture();
|
|
await using var factory = new IsolatedApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
var cropper = GetWindowsCropper(factory);
|
|
ScreenshotCropCoordinates[] invalidBounds =
|
|
[
|
|
new(-1, 0, 1, 1), new(0, -1, 1, 1),
|
|
new(0, 0, 0, 1), new(0, 0, 1, 0),
|
|
new(0, 0, -1, 1), new(0, 0, 1, -1),
|
|
new(4, 0, 1, 1), new(0, 3, 1, 1),
|
|
new(3, 0, 2, 1), new(0, 2, 1, 2)
|
|
];
|
|
|
|
foreach (var bounds in invalidBounds)
|
|
{
|
|
var failure = await Assert.ThrowsAsync<InvalidDataException>(() =>
|
|
cropper.SaveCroppedScreenshotAsync(fixture.InputPath, bounds, CancellationToken.None));
|
|
Assert.Contains("outside image bounds 4x3", failure.Message);
|
|
Assert.False(File.Exists(fixture.CroppedPath));
|
|
output.WriteLine($"Rejected {bounds.X},{bounds.Y},{bounds.Width},{bounds.Height}; no output created.");
|
|
}
|
|
}
|
|
|
|
private async Task AssertCropAsync(ScreenshotCropCoordinates bounds)
|
|
{
|
|
using var fixture = new ImageFixture();
|
|
await using var factory = new IsolatedApplicationFactory();
|
|
using var client = factory.CreateClient();
|
|
var cropper = GetWindowsCropper(factory);
|
|
|
|
var path = await cropper.SaveCroppedScreenshotAsync(
|
|
fixture.InputPath, bounds, CancellationToken.None);
|
|
|
|
Assert.Equal(fixture.CroppedPath, path);
|
|
using var cropped = new Bitmap(path!);
|
|
Assert.Equal(bounds.Width, cropped.Width);
|
|
Assert.Equal(bounds.Height, cropped.Height);
|
|
for (var y = 0; y < cropped.Height; y++)
|
|
{
|
|
for (var x = 0; x < cropped.Width; x++)
|
|
{
|
|
Assert.Equal(ImageFixture.PixelAt(bounds.X + x, bounds.Y + y).ToArgb(),
|
|
cropped.GetPixel(x, y).ToArgb());
|
|
}
|
|
}
|
|
output.WriteLine($"Verified every pixel of {bounds.Width}x{bounds.Height} at original origin {bounds.X},{bounds.Y}.");
|
|
}
|
|
|
|
private IScreenshotImageCropper GetWindowsCropper(IsolatedApplicationFactory factory)
|
|
{
|
|
var cropper = factory.Services.GetRequiredService<IScreenshotImageCropper>();
|
|
Assert.IsType<WindowsScreenshotImageCropper>(cropper);
|
|
output.WriteLine($"Registered cropper: {cropper.GetType().Name}; host: {RuntimeInformation.OSDescription}; " +
|
|
$"architecture: {RuntimeInformation.ProcessArchitecture}; runtime: {RuntimeInformation.FrameworkDescription}; " +
|
|
"target: net10.0-windows10.0.19041.0.");
|
|
return cropper;
|
|
}
|
|
|
|
private sealed class ImageFixture : IDisposable
|
|
{
|
|
private readonly string root = Path.Combine(Path.GetTempPath(), $"meeting-assistant-windows-crop-{Guid.NewGuid():N}");
|
|
|
|
public ImageFixture()
|
|
{
|
|
Directory.CreateDirectory(root);
|
|
using var image = new Bitmap(4, 3, PixelFormat.Format32bppArgb);
|
|
for (var y = 0; y < image.Height; y++)
|
|
{
|
|
for (var x = 0; x < image.Width; x++)
|
|
{
|
|
image.SetPixel(x, y, PixelAt(x, y));
|
|
}
|
|
}
|
|
image.Save(InputPath, ImageFormat.Png);
|
|
}
|
|
|
|
public string InputPath => Path.Combine(root, "input.png");
|
|
public string CroppedPath => Path.Combine(root, "input-cropped.png");
|
|
|
|
// Each original coordinate has a distinct opaque color; no resampling tolerance hides a shifted origin.
|
|
public static Color PixelAt(int x, int y) => Color.FromArgb(255, 30 + x * 50, 20 + y * 70, 10 + x * 15 + y * 5);
|
|
|
|
public void Dispose() => Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
#endif
|