diff --git a/src/ImageSharp/Common/Helpers/ImageMaths.cs b/src/ImageSharp/Common/Helpers/ImageMaths.cs
index f07ccb03b4..45556741e6 100644
--- a/src/ImageSharp/Common/Helpers/ImageMaths.cs
+++ b/src/ImageSharp/Common/Helpers/ImageMaths.cs
@@ -22,7 +22,8 @@ namespace SixLabors.ImageSharp
/// The blue component.
/// The .
[MethodImpl(InliningOptions.ShortMethod)]
- public static byte Get8BitBT709Luminance(byte r, byte g, byte b) => (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f);
+ public static byte Get8BitBT709Luminance(byte r, byte g, byte b) =>
+ (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5f);
///
/// Gets the luminance from the rgb components using the formula as specified by ITU-R Recommendation BT.709.
@@ -32,7 +33,8 @@ namespace SixLabors.ImageSharp
/// The blue component.
/// The .
[MethodImpl(InliningOptions.ShortMethod)]
- public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F));
+ public static ushort Get16BitBT709Luminance(ushort r, ushort g, ushort b) =>
+ (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F));
///
/// Scales a value from a 16 bit to it's 8 bit equivalent.
@@ -128,6 +130,15 @@ namespace SixLabors.ImageSharp
return x & (m - 1);
}
+ ///
+ /// Converts degrees to radians
+ ///
+ [MethodImpl(InliningOptions.ShortMethod)]
+ public static float ToRadian(float degrees)
+ {
+ return degrees * ((float)Math.PI / 180f);
+ }
+
///
/// Returns the absolute value of a 32-bit signed integer. Uses bit shifting to speed up the operation.
///
diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs
index 003249d6ed..4d798f4396 100644
--- a/src/ImageSharp/Processing/AffineTransformBuilder.cs
+++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs
@@ -14,18 +14,15 @@ namespace SixLabors.ImageSharp.Processing
public class AffineTransformBuilder
{
private readonly List matrices = new List();
- private Rectangle rectangle;
+ private readonly Rectangle rectangle;
///
/// Initializes a new instance of the class.
///
/// The source image size.
public AffineTransformBuilder(Size sourceSize)
+ : this(new Rectangle(Point.Empty, sourceSize))
{
- Guard.MustBeGreaterThan(sourceSize.Width, 0, nameof(sourceSize));
- Guard.MustBeGreaterThan(sourceSize.Height, 0, nameof(sourceSize));
-
- this.Size = sourceSize;
}
///
@@ -33,13 +30,17 @@ namespace SixLabors.ImageSharp.Processing
///
/// The source rectangle.
public AffineTransformBuilder(Rectangle sourceRectangle)
- : this(sourceRectangle.Size)
- => this.rectangle = sourceRectangle;
+ {
+ Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle));
+ Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle));
+
+ this.rectangle = sourceRectangle;
+ }
///
/// Gets the source image size.
///
- internal Size Size { get; }
+ internal Size Size => this.rectangle.Size;
///
/// Prepends a centered rotation matrix using the given rotation in degrees.
@@ -49,13 +50,29 @@ namespace SixLabors.ImageSharp.Processing
public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees)
=> this.PrependMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size));
+ ///
+ /// Prepends a centered rotation matrix using the given rotation in radians.
+ ///
+ /// The amount of rotation, in radians.
+ /// The .
+ public AffineTransformBuilder PrependRotateMatrixRadians(float radians)
+ => this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size));
+
///
/// Appends a centered rotation matrix using the given rotation in degrees.
///
/// The amount of rotation, in degrees.
/// The .
public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees)
- => this.AppendMatrix(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size));
+ => this.AppendRotateMatrixRadians(ImageMaths.ToRadian(degrees));
+
+ ///
+ /// Appends a centered rotation matrix using the given rotation in radians.
+ ///
+ /// The amount of rotation, in radians.
+ /// The .
+ public AffineTransformBuilder AppendRotateMatrixRadians(float radians)
+ => this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size));
///
/// Prepends a scale matrix from the given vector scale.
diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
index f561d3513f..6cef38f8fe 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
@@ -23,6 +23,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
+ ///
+ /// Creates a centered rotation matrix using the given rotation in radians and the source size.
+ ///
+ /// The amount of rotation, in radians.
+ /// The source image size.
+ /// The .
+ public static Matrix3x2 CreateRotationMatrixRadians(float radians, Size size)
+ => CreateCenteredTransformMatrix(
+ new Rectangle(Point.Empty, size),
+ Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
+
///
/// Creates a centered skew matrix from the give angles in degrees and the source size.
///
diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
index eaa51b1296..1b4caee14f 100644
--- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
@@ -4,12 +4,13 @@
using System;
using System.Numerics;
using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
- public class AffineTransformBuilderTests
+ public class AffineTransformBuilderTests : TransformBuilderTestBase
{
[Fact]
public void ConstructorAssignsProperties()
@@ -23,57 +24,33 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public void ConstructorThrowsInvalid()
{
Assert.Throws(() =>
- {
- var s = new Size(0, 1);
- var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
- });
+ {
+ var s = new Size(0, 1);
+ var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
+ });
Assert.Throws(() =>
- {
- var s = new Size(1, 0);
- var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
- });
+ {
+ var s = new Size(1, 0);
+ var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
+ });
}
- [Fact]
- public void AppendPrependOpposite()
- {
- var rectangle = new Rectangle(-1, -1, 3, 3);
- var b1 = new AffineTransformBuilder(rectangle);
- var b2 = new AffineTransformBuilder(rectangle);
-
- const float pi = (float)Math.PI;
-
- // Forwards
- b1.AppendRotateMatrixDegrees(pi)
- .AppendSkewMatrixDegrees(pi, pi)
- .AppendScaleMatrix(new SizeF(pi, pi))
- .AppendTranslationMatrix(new PointF(pi, pi));
-
- // Backwards
- b2.PrependTranslationMatrix(new PointF(pi, pi))
- .PrependScaleMatrix(new SizeF(pi, pi))
- .PrependSkewMatrixDegrees(pi, pi)
- .PrependRotateMatrixDegrees(pi);
+ protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslationMatrix(translate);
+ protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScaleMatrix(scale);
+ protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendRotateMatrixRadians(radians);
- Assert.Equal(b1.BuildMatrix(), b2.BuildMatrix());
- }
+ protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate) => builder.PrependTranslationMatrix(translate);
+ protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScaleMatrix(scale);
+ protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependRotateMatrixRadians(radians);
- [Fact]
- public void BuilderCanClear()
+ protected override Vector2 Execute(
+ AffineTransformBuilder builder,
+ Rectangle rectangle,
+ Vector2 sourcePoint)
{
- var rectangle = new Rectangle(0, 0, 3, 3);
- var builder = new AffineTransformBuilder(rectangle);
- Matrix3x2 matrix = Matrix3x2.Identity;
- matrix.M31 = (float)Math.PI;
-
- Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix());
-
- builder.AppendMatrix(matrix);
- Assert.NotEqual(Matrix3x2.Identity, builder.BuildMatrix());
-
- builder.Clear();
- Assert.Equal(Matrix3x2.Identity, builder.BuildMatrix());
+ Matrix3x2 matrix = builder.BuildMatrix();
+ return Vector2.Transform(sourcePoint, matrix);
}
}
}
diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
new file mode 100644
index 0000000000..d109387cc4
--- /dev/null
+++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
@@ -0,0 +1,151 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Numerics;
+
+using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
+using SixLabors.Primitives;
+
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Processing.Transforms
+{
+ public abstract class TransformBuilderTestBase
+ {
+ private static readonly ApproximateFloatComparer Comparer = new ApproximateFloatComparer(1e-6f);
+
+ public static readonly TheoryData ScaleTranslate_Data =
+ new TheoryData
+ {
+ // scale, translate, source, expectedDest
+
+ { Vector2.One, Vector2.Zero, Vector2.Zero, Vector2.Zero },
+ { Vector2.One, Vector2.Zero, new Vector2(10, 20), new Vector2(10, 20) },
+ { Vector2.One, new Vector2(3, 1), new Vector2(10, 20), new Vector2(13, 21) },
+ { new Vector2(2, 0.5f), new Vector2(3, 1), new Vector2(10, 20), new Vector2(23, 11) },
+ };
+
+ [Theory]
+ [MemberData(nameof(ScaleTranslate_Data))]
+ public void _1Scale_2Translate(Vector2 scale, Vector2 translate, Vector2 source, Vector2 expectedDest)
+ {
+ // These operations should be size-agnostic:
+ var size = new Size(123, 321);
+ TBuilder builder = this.CreateBuilder(size);
+
+ this.AppendScale(builder, new SizeF(scale));
+ this.AppendTranslation(builder, translate);
+
+ Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source);
+ Assert.True(Comparer.Equals(expectedDest, actualDest));
+ }
+
+ public static readonly TheoryData TranslateScale_Data =
+ new TheoryData
+ {
+ // translate, scale, source, expectedDest
+
+ { Vector2.Zero, Vector2.One, Vector2.Zero, Vector2.Zero },
+ { Vector2.Zero, Vector2.One, new Vector2(10, 20), new Vector2(10, 20) },
+ { new Vector2(3, 1), new Vector2(2, 0.5f), new Vector2(10, 20), new Vector2(26, 10.5f) },
+ };
+
+ [Theory]
+ [MemberData(nameof(TranslateScale_Data))]
+ public void _1Translate_2Scale(Vector2 translate, Vector2 scale, Vector2 source, Vector2 expectedDest)
+ {
+ // Translate ans scale are size-agnostic:
+ var size = new Size(456, 432);
+ TBuilder builder = this.CreateBuilder(size);
+
+ this.AppendTranslation(builder, translate);
+ this.AppendScale(builder, new SizeF(scale));
+
+ Vector2 actualDest = this.Execute(builder, new Rectangle(Point.Empty, size), source);
+ Assert.Equal(expectedDest, actualDest, Comparer);
+ }
+
+ [Theory]
+ [InlineData(10, 20)]
+ [InlineData(-20, 10)]
+ public void LocationOffsetIsPrepended(int locationX, int locationY)
+ {
+ var rectangle = new Rectangle(locationX, locationY, 10, 10);
+ TBuilder builder = this.CreateBuilder(rectangle);
+
+ this.AppendScale(builder, new SizeF(2, 2));
+
+ Vector2 actual = this.Execute(builder, rectangle, Vector2.One);
+ Vector2 expected = new Vector2(-locationX + 1, -locationY + 1) * 2;
+
+ Assert.Equal(actual, expected, Comparer);
+ }
+
+ [Theory]
+ [InlineData(200, 100, 10, 42, 84)]
+ [InlineData(200, 100, 100, 42, 84)]
+ [InlineData(100, 200, -10, 42, 84)]
+ public void RotateDegrees_ShouldCreateCenteredMatrix(int width, int height, float deg, float x, float y)
+ {
+ var size = new Size(width, height);
+ TBuilder builder = this.CreateBuilder(size);
+
+ this.AppendRotationDegrees(builder, deg);
+
+ // TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness
+ Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(deg, size);
+
+ var position = new Vector2(x, y);
+ var expected = Vector2.Transform(position, matrix);
+ Vector2 actual = this.Execute(builder, new Rectangle(Point.Empty, size), position);
+
+ Assert.Equal(actual, expected, Comparer);
+ }
+
+ [Fact]
+ public void AppendPrependOpposite()
+ {
+ var rectangle = new Rectangle(-1, -1, 3, 3);
+ TBuilder b1 = this.CreateBuilder(rectangle);
+ TBuilder b2 = this.CreateBuilder(rectangle);
+
+ const float pi = (float)Math.PI;
+
+ // Forwards
+ this.AppendRotationRadians(b1, pi);
+ this.AppendScale(b1, new SizeF(2, 0.5f));
+ this.AppendTranslation(b1, new PointF(123, 321));
+
+ // Backwards
+ this.PrependTranslation(b2, new PointF(123, 321));
+ this.PrependScale(b2, new SizeF(2, 0.5f));
+ this.PrependRotationRadians(b2, pi);
+
+ Vector2 p1 = this.Execute(b1, rectangle, new Vector2(32, 65));
+ Vector2 p2 = this.Execute(b2, rectangle, new Vector2(32, 65));
+
+ Assert.Equal(p1, p2, Comparer);
+ }
+
+ protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size));
+
+ protected virtual TBuilder CreateBuilder(Rectangle rectangle) => (TBuilder)Activator.CreateInstance(typeof(TBuilder), rectangle);
+
+ protected abstract void AppendTranslation(TBuilder builder, PointF translate);
+ protected abstract void AppendScale(TBuilder builder, SizeF scale);
+ protected abstract void AppendRotationRadians(TBuilder builder, float radians);
+
+ protected abstract void PrependTranslation(TBuilder builder, PointF translate);
+ protected abstract void PrependScale(TBuilder builder, SizeF scale);
+ protected abstract void PrependRotationRadians(TBuilder builder, float radians);
+
+ protected virtual void AppendRotationDegrees(TBuilder builder, float degrees) =>
+ this.AppendRotationRadians(builder, ImageMaths.ToRadian(degrees));
+
+ protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint);
+
+ private static float Sqrt(float a) => (float)Math.Sqrt(a);
+ }
+}
\ No newline at end of file
diff --git a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs
index 854e57d8f5..47ca6cccb2 100644
--- a/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs
+++ b/tests/ImageSharp.Tests/TestUtilities/ApproximateFloatComparer.cs
@@ -11,7 +11,8 @@ namespace SixLabors.ImageSharp.Tests
///
internal readonly struct ApproximateFloatComparer :
IEqualityComparer,
- IEqualityComparer
+ IEqualityComparer,
+ IEqualityComparer
{
private readonly float Epsilon;
@@ -33,9 +34,17 @@ namespace SixLabors.ImageSharp.Tests
public int GetHashCode(float obj) => obj.GetHashCode();
///
- public bool Equals(Vector4 x, Vector4 y) => this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W);
+ public bool Equals(Vector4 a, Vector4 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y) && this.Equals(a.Z, b.Z) && this.Equals(a.W, b.W);
///
public int GetHashCode(Vector4 obj) => obj.GetHashCode();
+
+ ///
+ public bool Equals(Vector2 a, Vector2 b) => this.Equals(a.X, b.X) && this.Equals(a.Y, b.Y);
+
+ public int GetHashCode(Vector2 obj)
+ {
+ throw new System.NotImplementedException();
+ }
}
}
\ No newline at end of file