// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using System.Reflection; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Processing.Transforms; [Trait("Category", "Processors")] public class AffineTransformTests { private readonly ITestOutputHelper output; private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.033F, 3); /// /// angleDeg, sx, sy, tx, ty /// public static readonly TheoryData TransformValues = new() { { 0, 1, 1, 0, 0 }, { 50, 1, 1, 0, 0 }, { 0, 1, 1, 20, 10 }, { 50, 1, 1, 20, 10 }, { 0, 1, 1, -20, -10 }, { 50, 1, 1, -20, -10 }, { 50, 1.5f, 1.5f, 0, 0 }, { 50, 1.1F, 1.3F, 30, -20 }, { 0, 2f, 1f, 0, 0 }, { 0, 1f, 2f, 0, 0 }, }; public static readonly TheoryData ResamplerNames = new() { nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Box), nameof(KnownResamplers.CatmullRom), nameof(KnownResamplers.Hermite), nameof(KnownResamplers.Lanczos2), nameof(KnownResamplers.Lanczos3), nameof(KnownResamplers.Lanczos5), nameof(KnownResamplers.Lanczos8), nameof(KnownResamplers.MitchellNetravali), nameof(KnownResamplers.NearestNeighbor), nameof(KnownResamplers.Robidoux), nameof(KnownResamplers.RobidouxSharp), nameof(KnownResamplers.Spline), nameof(KnownResamplers.Triangle), nameof(KnownResamplers.Welch), }; public static readonly TheoryData Transform_DoesNotCreateEdgeArtifacts_ResamplerNames = new() { nameof(KnownResamplers.NearestNeighbor), nameof(KnownResamplers.Triangle), nameof(KnownResamplers.Bicubic), nameof(KnownResamplers.Lanczos8), }; public AffineTransformTests(ITestOutputHelper output) => this.output = output; /// /// The output of an "all white" image should be "all white" or transparent, regardless of the transformation and the resampler. /// /// The pixel type of the image. [Theory] [WithSolidFilledImages(nameof(Transform_DoesNotCreateEdgeArtifacts_ResamplerNames), 5, 5, 255, 255, 255, 255, PixelTypes.Rgba32)] public void Transform_DoesNotCreateEdgeArtifacts(TestImageProvider provider, string resamplerName) where TPixel : unmanaged, IPixel { IResampler resampler = GetResampler(resamplerName); using Image image = provider.GetImage(); AffineTransformBuilder builder = new AffineTransformBuilder() .AppendRotationDegrees(30); image.Mutate(c => c.Transform(builder, resampler)); image.DebugSave(provider, resamplerName); VerifyAllPixelsAreWhiteOrTransparent(image); } [Theory] [WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] public void Transform_RotateScaleTranslate( TestImageProvider provider, float angleDeg, float sx, float sy, float tx, float ty) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder() .AppendRotationDegrees(angleDeg) .AppendScale(new SizeF(sx, sy)) .AppendTranslation(new PointF(tx, ty)); this.PrintMatrix(builder.BuildMatrix(image.Size)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); FormattableString testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"; image.DebugSave(provider, testOutputDetails); image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails); } [Theory] [WithTestPatternImages(96, 96, PixelTypes.Rgba32, 50, 0.8f)] public void Transform_RotateScale_ManuallyCentered(TestImageProvider provider, float angleDeg, float s) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); AffineTransformBuilder builder = new AffineTransformBuilder() .AppendRotationDegrees(angleDeg) .AppendScale(new SizeF(s, s)); image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic)); FormattableString testOutputDetails = $"R({angleDeg})_S({s})"; image.DebugSave(provider, testOutputDetails); image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails); } public static readonly TheoryData Transform_IntoRectangle_Data = new() { { 0, 0, 10, 10 }, { 0, 0, 5, 10 }, { 0, 0, 10, 5 }, { 5, 0, 5, 10 }, { -5, -5, 20, 20 } }; /// /// Testing transforms using custom source rectangles: /// https://github.com/SixLabors/ImageSharp/pull/386#issuecomment-357104963 /// /// The pixel type of the image. [Theory] [WithTestPatternImages(96, 48, PixelTypes.Rgba32)] public void Transform_FromSourceRectangle1(TestImageProvider provider) where TPixel : unmanaged, IPixel { Rectangle rectangle = new(48, 0, 48, 24); using Image image = provider.GetImage(); image.DebugSave(provider, $"_original"); AffineTransformBuilder builder = new AffineTransformBuilder() .AppendScale(new SizeF(2, 1.5F)); image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); } [Theory] [WithTestPatternImages(96, 48, PixelTypes.Rgba32)] public void Transform_FromSourceRectangle2(TestImageProvider provider) where TPixel : unmanaged, IPixel { Rectangle rectangle = new(0, 24, 48, 24); using Image image = provider.GetImage(); AffineTransformBuilder builder = new AffineTransformBuilder() .AppendScale(new SizeF(1F, 2F)); image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); } [Theory] [WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] public void Transform_WithSampler(TestImageProvider provider, string resamplerName) where TPixel : unmanaged, IPixel { IResampler sampler = GetResampler(resamplerName); using Image image = provider.GetImage(); AffineTransformBuilder builder = new AffineTransformBuilder() .AppendRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); image.Mutate(i => i.Transform(builder, sampler)); image.DebugSave(provider, resamplerName); image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); } [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 21)] public void WorksWithDiscoBuffers(TestImageProvider provider, int bufferCapacityInPixelRows) where TPixel : unmanaged, IPixel { AffineTransformBuilder builder = new AffineTransformBuilder() .AppendRotationDegrees(50) .AppendScale(new SizeF(.6F, .6F)); provider.RunBufferCapacityLimitProcessorTest( bufferCapacityInPixelRows, c => c.Transform(builder)); } [Fact] public void Issue1911() { using Image image = new(100, 100); image.Mutate(x => x = x.Transform(new Rectangle(0, 0, 99, 100), Matrix3x2.Identity, new Size(99, 100), KnownResamplers.Lanczos2)); Assert.Equal(99, image.Width); Assert.Equal(100, image.Height); } [Theory] [WithSolidFilledImages(4, 4, nameof(Color.Red), PixelTypes.Rgba32)] public void Issue2753(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(270, new Vector2(3.5f, 3.5f)); image.Mutate(x => x.BackgroundColor(Color.Red)); image.Mutate(x => x = x.Transform(builder)); image.DebugSave(provider); Assert.Equal(4, image.Width); Assert.Equal(7, image.Height); } [Theory] [WithFile(TestImages.Png.Issue3000, PixelTypes.Rgba32, 3, 3)] [WithFile(TestImages.Png.Issue3000, PixelTypes.Rgba32, 4, 4)] public void Issue3000(TestImageProvider provider, float x, float y) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); image.Mutate(c => c .Transform(new AffineTransformBuilder().AppendRotationDegrees(90, new Vector2(x, y)))); string details = $"p-{x}-{y}"; image.DebugSave(provider, testOutputDetails: details); image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: details); } [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] public void Identity(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); Matrix3x2 m = Matrix3x2.Identity; Rectangle r = new(25, 25, 50, 50); image.Mutate(x => x.Transform(r, m, new Size(100, 100), KnownResamplers.Bicubic)); image.DebugSave(provider); image.CompareToReferenceOutput(ValidatorComparer, provider); } [Theory] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0.0001F)] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 57F)] [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0F)] public void Transform_With_Custom_Dimensions(TestImageProvider provider, float radians) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(); Matrix3x2 m = Matrix3x2.CreateRotation(radians, new Vector2(50, 50)); Rectangle r = new(25, 25, 50, 50); image.Mutate(x => x.Transform(r, m, new Size(100, 100), KnownResamplers.Bicubic)); image.DebugSave(provider, testOutputDetails: radians); image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians); } [Theory] [WithSolidFilledImages(100, 100, "DimGray", PixelTypes.Rgba32)] public void TransformRotationDoesNotOffset(TestImageProvider provider) where TPixel : unmanaged, IPixel { Rgba32 background = Color.DimGray.ToPixel(); TPixel marker = Color.Aqua.ToPixel(); using Image canvas = provider.GetImage(); using Image img = canvas.Clone(); img[0, 0] = marker; img.Mutate(c => c.Rotate(180)); Assert.Equal(marker, img[99, 99]); img.DebugSave(provider, "Rotate180"); using Image img2 = canvas.Clone(); img2[0, 0] = marker; img2.Mutate( c => c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180), KnownResamplers.NearestNeighbor)); img.DebugSave(provider, "AffineRotate180NN"); using Image img3 = canvas.Clone(); img3[0, 0] = marker; img3.Mutate(c => c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180))); img3.DebugSave(provider, "AffineRotate180Bicubic"); ImageComparer.Exact.VerifySimilarity(img, img2); ImageComparer.Exact.VerifySimilarity(img, img3); } private static IResampler GetResampler(string name) { PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name) ?? throw new InvalidOperationException($"No resampler named {name}"); return (IResampler)property.GetValue(null); } private static void VerifyAllPixelsAreWhiteOrTransparent(Image image) where TPixel : unmanaged, IPixel { Assert.True(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory data)); Rgb24 white = new(255, 255, 255); foreach (TPixel pixel in data.Span) { Rgba32 rgba = pixel.ToRgba32(); if (rgba.A == 0) { continue; } Assert.Equal(white, rgba.Rgb); } } private void PrintMatrix(Matrix3x2 a) { string s = $"{a.M11:F10},{a.M12:F10},{a.M21:F10},{a.M22:F10},{a.M31:F10},{a.M32:F10}"; this.output.WriteLine(s); } }