Browse Source

Merge pull request #3011 from SixLabors/js/fix-3000

Fix off-by-one errors when transforming images.
pull/3020/head
James Jackson-South 3 months ago
committed by GitHub
parent
commit
24576efda9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  2. 12
      src/ImageSharp/Primitives/Point.cs
  3. 12
      src/ImageSharp/Primitives/PointF.cs
  4. 12
      src/ImageSharp/Primitives/SizeF.cs
  5. 43
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  6. 8
      src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs
  7. 2
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs
  8. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  9. 2
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs
  10. 8
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  11. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
  12. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
  13. 2
      src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs
  14. 376
      src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
  15. 66
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  16. 26
      src/ImageSharp/Processing/TransformSpace.cs
  17. 40
      tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs
  18. 8
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs
  19. 2
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  20. 8
      tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs
  21. 4
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
  22. 3
      tests/ImageSharp.Tests/TestImages.cs
  23. 3
      tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-3-3.png
  24. 3
      tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-4-4.png
  25. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png
  26. 3
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48__original.png
  27. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png
  28. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(1,2)_T(0,0).png
  29. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(2,1)_T(0,0).png
  30. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png
  31. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png
  32. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png
  33. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png
  34. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png
  35. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png
  36. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png
  37. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png
  38. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png
  39. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png
  40. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png
  41. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png
  42. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png
  43. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png
  44. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png
  45. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png
  46. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png
  47. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png
  48. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png
  49. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png
  50. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png
  51. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png
  52. 4
      tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png
  53. 4
      tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png
  54. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png
  55. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png
  56. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png
  57. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png
  58. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png
  59. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png
  60. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png
  61. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png
  62. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png
  63. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png
  64. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png
  65. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png
  66. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png
  67. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png
  68. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png
  69. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png
  70. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png
  71. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png
  72. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png
  73. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png
  74. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png
  75. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png
  76. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png
  77. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png
  78. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png
  79. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png
  80. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png
  81. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png
  82. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png
  83. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png
  84. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png
  85. 4
      tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png
  86. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png
  87. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png
  88. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png
  89. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png
  90. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png
  91. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png
  92. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png
  93. 4
      tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png
  94. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png
  95. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png
  96. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png
  97. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png
  98. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_WorksWithAllResamplers_ducky_Bicubic.png
  99. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_WorksWithAllResamplers_ducky_Box.png
  100. 4
      tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_WorksWithAllResamplers_ducky_CatmullRom.png

14
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -318,7 +318,7 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
{
if (location.Value?.Length == 2)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
Vector2 point = TransformUtilities.ProjectiveTransform2D(location.Value[0], location.Value[1], matrix);
// Ensure the point is within the image dimensions.
point = Vector2.Clamp(point, Vector2.Zero, new Vector2(width - 1, height - 1));
@ -340,18 +340,18 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
if (area.Value?.Length == 4)
{
RectangleF rectangle = new(area.Value[0], area.Value[1], area.Value[2], area.Value[3]);
if (!TransformUtils.TryGetTransformedRectangle(rectangle, matrix, out Rectangle bounds))
if (!TransformUtilities.TryGetTransformedRectangle(rectangle, matrix, out RectangleF bounds))
{
return;
}
// Ensure the bounds are within the image dimensions.
bounds = Rectangle.Intersect(bounds, new Rectangle(0, 0, width, height));
bounds = RectangleF.Intersect(bounds, new Rectangle(0, 0, width, height));
area.Value[0] = (ushort)bounds.X;
area.Value[1] = (ushort)bounds.Y;
area.Value[2] = (ushort)bounds.Width;
area.Value[3] = (ushort)bounds.Height;
area.Value[0] = (ushort)MathF.Floor(bounds.X);
area.Value[1] = (ushort)MathF.Floor(bounds.Y);
area.Value[2] = (ushort)MathF.Ceiling(bounds.Width);
area.Value[3] = (ushort)MathF.Ceiling(bounds.Height);
this.SetValue(ExifTag.SubjectArea, area.Value);
}
else

12
src/ImageSharp/Primitives/Point.cs

@ -69,7 +69,7 @@ public struct Point : IEquatable<Point>
/// Gets a value indicating whether this <see cref="Point"/> is empty.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
public readonly bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Creates a <see cref="PointF"/> with the coordinates of the specified <see cref="Point"/>.
@ -239,7 +239,7 @@ public struct Point : IEquatable<Point>
/// </summary>
/// <param name="x">The out value for X.</param>
/// <param name="y">The out value for Y.</param>
public void Deconstruct(out int x, out int y)
public readonly void Deconstruct(out int x, out int y)
{
x = this.X;
y = this.Y;
@ -268,17 +268,17 @@ public struct Point : IEquatable<Point>
public void Offset(Point point) => this.Offset(point.X, point.Y);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <inheritdoc/>
public override string ToString() => $"Point [ X={this.X}, Y={this.Y} ]";
public override readonly string ToString() => $"Point [ X={this.X}, Y={this.Y} ]";
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Point other && this.Equals(other);
public override readonly bool Equals(object? obj) => obj is Point other && this.Equals(other);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
public readonly bool Equals(Point other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
private static short HighInt16(int n) => unchecked((short)((n >> 16) & 0xffff));

12
src/ImageSharp/Primitives/PointF.cs

@ -58,7 +58,7 @@ public struct PointF : IEquatable<PointF>
/// Gets a value indicating whether this <see cref="PointF"/> is empty.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
public readonly bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@ -251,7 +251,7 @@ public struct PointF : IEquatable<PointF>
/// </summary>
/// <param name="x">The out value for X.</param>
/// <param name="y">The out value for Y.</param>
public void Deconstruct(out float x, out float y)
public readonly void Deconstruct(out float x, out float y)
{
x = this.X;
y = this.Y;
@ -277,15 +277,15 @@ public struct PointF : IEquatable<PointF>
public void Offset(PointF point) => this.Offset(point.X, point.Y);
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y);
/// <inheritdoc/>
public override string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]";
public override readonly string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]";
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF);
public override readonly bool Equals(object? obj) => obj is PointF pointF && this.Equals(pointF);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
public readonly bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
}

12
src/ImageSharp/Primitives/SizeF.cs

@ -67,7 +67,7 @@ public struct SizeF : IEquatable<SizeF>
/// Gets a value indicating whether this <see cref="SizeF"/> is empty.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsEmpty => this.Equals(Empty);
public readonly bool IsEmpty => this.Equals(Empty);
/// <summary>
/// Creates a <see cref="Vector2"/> with the coordinates of the specified <see cref="PointF"/>.
@ -201,24 +201,24 @@ public struct SizeF : IEquatable<SizeF>
/// </summary>
/// <param name="width">The out value for the width.</param>
/// <param name="height">The out value for the height.</param>
public void Deconstruct(out float width, out float height)
public readonly void Deconstruct(out float width, out float height)
{
width = this.Width;
height = this.Height;
}
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Width, this.Height);
public override readonly int GetHashCode() => HashCode.Combine(this.Width, this.Height);
/// <inheritdoc/>
public override string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]";
public override readonly string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]";
/// <inheritdoc/>
public override bool Equals(object? obj) => obj is SizeF && this.Equals((SizeF)obj);
public override readonly bool Equals(object? obj) => obj is SizeF sizeF && this.Equals(sizeF);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
public readonly bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
/// <summary>
/// Multiplies <see cref="SizeF"/> by a <see cref="float"/> producing <see cref="SizeF"/>.

43
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -17,24 +17,9 @@ public class AffineTransformBuilder
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
public AffineTransformBuilder()
: this(TransformSpace.Pixel)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the affine transform.
/// </param>
public AffineTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the affine transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary>
/// Prepends a rotation matrix using the given rotation angle in degrees
/// and the image center point as rotation center.
@ -52,7 +37,7 @@ public class AffineTransformBuilder
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
/// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@ -88,7 +73,7 @@ public class AffineTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));
=> this.Append(size => TransformUtilities.CreateRotationTransformMatrixRadians(radians, size));
/// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
@ -172,7 +157,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
=> this.Prepend(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -210,7 +195,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));
=> this.Append(size => TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
@ -344,15 +329,29 @@ public class AffineTransformBuilder
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle)
public SizeF GetTransformedSize(Rectangle sourceRectangle)
{
Matrix3x2 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
return GetTransformedSize(sourceRectangle, matrix);
}
/// <summary>
/// Returns the size of a rectangle large enough to contain the transformed source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <exception cref="DegenerateTransformException">
/// The resultant matrix is degenerate containing one or more values equivalent
/// to <see cref="float.NaN"/> or a zero determinant and therefore cannot be used
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
private static void CheckDegenerate(Matrix3x2 matrix)
{
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
}

8
src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs

@ -7,8 +7,8 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp.Processing;
/// <summary>
/// Defines extensions that allow the application of composable transform operations on an <see cref="Image"/>
/// using Mutate/Clone.
/// Defines extensions that allow the application of composable transform operations
/// on an <see cref="IImageProcessingContext"/> using Mutate/Clone.
/// </summary>
public static class TransformExtensions
{
@ -51,7 +51,7 @@ public static class TransformExtensions
IResampler sampler)
{
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = builder.GetTransformedSize(sourceRectangle);
Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
}
@ -113,7 +113,7 @@ public static class TransformExtensions
IResampler sampler)
{
Matrix4x4 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = builder.GetTransformedSize(sourceRectangle);
Size targetDimensions = TransformUtilities.GetTransformedCanvasSize(transform, sourceRectangle.Size);
return source.Transform(sourceRectangle, transform, targetDimensions, sampler);
}

2
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs

@ -21,7 +21,7 @@ public class AffineTransformProcessor : CloningImageProcessor
Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeValueType(sampler);
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
}

4
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -77,7 +77,9 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
return;
}
// Convert from screen to world space.
// All matrices are defined in normalized coordinate space so we need to convert to pixel space.
// After normalization we need to invert the matrix for correct sampling.
matrix = TransformUtilities.NormalizeToPixel(matrix);
Matrix3x2.Invert(matrix, out matrix);
if (sampler is NearestNeighborResampler)

2
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs

@ -21,7 +21,7 @@ public sealed class ProjectiveTransformProcessor : CloningImageProcessor
Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeValueType(sampler);
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
}

8
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -75,7 +75,9 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
return;
}
// Convert from screen to world space.
// All matrices are defined in normalized coordinate space so we need to convert to pixel space.
// After normalization we need to invert the matrix for correct sampling.
matrix = TransformUtilities.NormalizeToPixel(matrix);
Matrix4x4.Invert(matrix, out matrix);
if (sampler is NearestNeighborResampler)
@ -135,7 +137,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
for (int x = 0; x < destinationRowSpan.Length; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
@ -207,7 +209,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
for (int x = 0; x < span.Length; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix);
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, matrix);
float pY = point.Y;
float pX = point.X;

4
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs

@ -28,14 +28,14 @@ public sealed class RotateProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel),
TransformUtilities.CreateRotationTransformMatrixDegrees(degrees, sourceSize),
sampler,
sourceSize)
=> this.Degrees = degrees;
// Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel))
: base(rotationMatrix, sampler, TransformUtilities.GetTransformedCanvasSize(rotationMatrix, sourceSize))
{
}

4
src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs

@ -30,7 +30,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
/// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel),
TransformUtilities.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize),
sampler,
sourceSize)
{
@ -40,7 +40,7 @@ public sealed class SkewProcessor : AffineTransformProcessor
// Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel))
: base(skewMatrix, sampler, TransformUtilities.GetTransformedCanvasSize(skewMatrix, sourceSize))
{
}

2
src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs

@ -24,7 +24,7 @@ internal class SwizzleProcessor<TSwizzler, TPixel> : TransformProcessor<TPixel>
// Calculate the transform matrix from the swizzle operation to allow us
// to update any metadata that represents pixel coordinates in the source image.
this.transformMatrix = new ProjectiveTransformBuilder()
.AppendMatrix(TransformUtils.GetSwizzlerMatrix(swizzler, sourceRectangle))
.AppendMatrix(TransformUtilities.GetSwizzlerMatrix(swizzler, sourceRectangle))
.BuildMatrix(sourceRectangle);
}

376
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs → src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms;
/// <summary>
/// Contains utility methods for working with transforms.
/// </summary>
internal static class TransformUtils
internal static class TransformUtilities
{
/// <summary>
/// Returns a value that indicates whether the specified matrix is degenerate
@ -80,79 +80,69 @@ internal static class TransformUtils
}
/// <summary>
/// Creates a centered rotation transform matrix using the given rotation in degrees and the source size.
/// Creates a centered rotation transform matrix using the given rotation in degrees and the original source size.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace)
=> CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace);
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size)
=> CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size);
/// <summary>
/// Creates a centered rotation transform matrix using the given rotation in radians and the source size.
/// Creates a centered rotation transform matrix using the given rotation in radians and the original source size.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace);
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size);
/// <summary>
/// Creates a centered skew transform matrix from the give angles in degrees and the source size.
/// Creates a centered skew transform matrix from the give angles in degrees and the original source size.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace)
=> CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace);
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size);
/// <summary>
/// Creates a centered skew transform matrix from the give angles in radians and the source size.
/// Creates a centered skew transform matrix from the give angles in radians and the original source size.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="size">The source image size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the centered matrix.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace);
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size);
/// <summary>
/// Gets the centered transform matrix based upon the source rectangle.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source image size.</param>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when creating the centered matrix.
/// </param>
/// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size)
{
Size transformSize = GetUnboundedTransformedSize(matrix, size, transformSpace);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
// 1) Unbounded size.
SizeF ts = GetRawTransformedSize(matrix, size);
// The source size is provided using the coordinate space of the source image.
// however the transform should always be applied in the pixel space.
// To account for this we offset by the size - 1 to translate to the pixel space.
float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F;
// 2) Invert the content transform for screen->world.
Matrix3x2.Invert(matrix, out Matrix3x2 inv);
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(transformSize.Width - offset), -(transformSize.Height - offset)) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - offset, size.Height - offset) * .5F);
// 3) Translate target (canvas) so its center is at the origin,
// translate source so its center is at the origin, then undo the content transform.
Matrix3x2 toTarget = Matrix3x2.CreateTranslation(new Vector2(-ts.Width, -ts.Height) * 0.5f);
Matrix3x2 toSource = Matrix3x2.CreateTranslation(new Vector2(size.Width, size.Height) * 0.5f);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
// 4) World->screen.
Matrix3x2.Invert(toTarget * inv * toSource, out Matrix3x2 centered);
return centered;
}
@ -287,7 +277,6 @@ internal static class TransformUtils
/// <param name="topRight">The top-right point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the matrix.</param>
/// <returns>The computed projection matrix for the quad distortion.</returns>
/// <remarks>
/// This method is based on the algorithm described in the following article:
@ -298,8 +287,7 @@ internal static class TransformUtils
PointF topLeft,
PointF topRight,
PointF bottomRight,
PointF bottomLeft,
TransformSpace transformSpace)
PointF bottomLeft)
{
PointF p1 = new(rectangle.X, rectangle.Y);
PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y);
@ -345,46 +333,94 @@ internal static class TransformUtils
(float)b[2], (float)b[5], 0, 1);
#pragma warning restore SA1117
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
if (transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(projectionMatrix))
{
if (projectionMatrix.M41 != 0)
{
projectionMatrix.M41--;
}
if (projectionMatrix.M42 != 0)
{
projectionMatrix.M42--;
}
}
return projectionMatrix;
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// Calculates the size of a destination canvas large enough to contain
/// the fully transformed source content, including any translation offsets.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <returns>The <see cref="Size"/>.</returns>
public static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
=> GetTransformedSize(matrix, size, transformSpace, true);
/// <param name="size">The original source size.</param>
/// <returns>
/// A <see cref="SizeF"/> representing the dimensions of the destination
/// canvas required to fully contain the transformed source, including
/// any positive or negative translation offsets.
/// </returns>
/// <remarks>
/// <para>
/// This method ensures that the transformed content remains fully visible
/// on the destination canvas by expanding its size to include translations
/// in all directions.
/// </para>
/// <para>
/// It behaves identically to calling
/// <see cref="GetTransformedSize(Matrix3x2, Size, bool)"/> with
/// <c>preserveCanvas</c> set to <see langword="true"/>.
/// </para>
/// <para>
/// The resulting canvas size represents the total area required to display
/// the transformed image without clipping, not merely the geometric bounds
/// of the transformed source.
/// </para>
/// </remarks>
public static Size GetTransformedCanvasSize(Matrix3x2 matrix, Size size)
=> Size.Ceiling(GetTransformedSize(matrix, size, true));
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// Calculates the size of a destination canvas large enough to contain
/// the fully transformed source content, including any translation offsets.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> used when generating the matrix.</param>
/// <param name="size">The original source size.</param>
/// <returns>
/// The <see cref="Size"/>.
/// A <see cref="SizeF"/> representing the dimensions of the destination
/// canvas required to fully contain the transformed source, including
/// any positive or negative translation offsets.
/// </returns>
/// <remarks>
/// <para>
/// This method ensures that the transformed content remains fully visible
/// on the destination canvas by expanding its size to include translations
/// in all directions.
/// </para>
/// <para>
/// It behaves identically to calling
/// <see cref="GetTransformedSize(Matrix3x2, Size, bool)"/> with
/// <c>preserveCanvas</c> set to <see langword="true"/>.
/// </para>
/// <para>
/// The resulting canvas size represents the total area required to display
/// the transformed image without clipping, not merely the geometric bounds
/// of the transformed source.
/// </para>
/// </remarks>
public static Size GetTransformedCanvasSize(Matrix4x4 matrix, Size size)
=> Size.Ceiling(GetTransformedSize(matrix, size, true));
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The original source size.</param>
/// <returns>The <see cref="Size"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SizeF GetRawTransformedSize(Matrix4x4 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <summary>
/// Returns the size of the transformed source. When <paramref name="preserveCanvas"/> is true,
/// the size is expanded to include translation so the full moved content remains visible.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The original source size.</param>
/// <param name="preserveCanvas">
/// If <see langword="true"/>, expand the size to account for translation (left/up as well as right/down).
/// If <see langword="false"/>, return only the transformed span without translation expansion.
/// </param>
/// <returns>The <see cref="SizeF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size GetTransformedSize(Matrix4x4 matrix, Size size, TransformSpace transformSpace)
private static SizeF GetTransformedSize(Matrix4x4 matrix, Size size, bool preserveCanvas)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -393,27 +429,9 @@ internal static class TransformUtils
return size;
}
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
bool usePixelSpace = transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(matrix);
// Define an offset size to translate between pixel space and coordinate space.
// When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates.
// When not using pixel space, use SizeF.Empty as the offset.
// Compute scaling factors from the matrix
float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
// Apply the offset relative to the scale
SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty;
// Subtract the offset size to translate to the appropriate space (pixel or coordinate).
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds))
{
// Add the offset size back to translate the transformed bounds to the correct space.
return Size.Ceiling(ConstrainSize(bounds) + offsetSize);
return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size;
}
return size;
@ -438,30 +456,31 @@ internal static class TransformUtils
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Top)),
swizzler.Transform(new Point(sourceRectangle.Right, sourceRectangle.Bottom)),
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)),
TransformSpace.Pixel);
swizzler.Transform(new Point(sourceRectangle.Left, sourceRectangle.Bottom)));
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <param name="size">The original source size.</param>
/// <returns>The <see cref="Size"/>.</returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
=> GetTransformedSize(matrix, size, transformSpace, false);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SizeF GetRawTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// Returns the size of the transformed source. When <paramref name="preserveCanvas"/> is true,
/// the size is expanded to include translation so the full moved content remains visible.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <param name="constrain">Whether to constrain the size to ensure that the dimensions are positive.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain)
/// <param name="size">The original source size.</param>
/// <param name="preserveCanvas">
/// If <see langword="true"/>, expand the size to account for translation (left/up as well as right/down).
/// If <see langword="false"/>, return only the transformed span without translation expansion.
/// </param>
/// <returns>The <see cref="SizeF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static SizeF GetTransformedSize(Matrix3x2 matrix, Size size, bool preserveCanvas)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -470,21 +489,9 @@ internal static class TransformUtils
return size;
}
// Define an offset size to translate between coordinate space and pixel space.
// Compute scaling factors from the matrix
SizeF offsetSize = SizeF.Empty;
if (transformSpace == TransformSpace.Pixel)
{
float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
offsetSize = new SizeF(scaleX, scaleY);
}
// Subtract the offset size to translate to the pixel space.
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size), matrix, out RectangleF bounds))
{
// Add the offset size back to translate the transformed bounds to the coordinate space.
return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize);
return preserveCanvas ? GetPreserveCanvasSize(bounds) : bounds.Size;
}
return size;
@ -499,7 +506,8 @@ internal static class TransformUtils
/// <returns>
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns>
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out RectangleF bounds)
{
if (matrix.IsIdentity || rectangle.Equals(default))
{
@ -526,7 +534,7 @@ internal static class TransformUtils
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
internal static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out RectangleF bounds)
{
if (matrix.IsIdentity || rectangle.Equals(default))
{
@ -543,15 +551,61 @@ internal static class TransformUtils
return true;
}
/// <summary>
/// Calculates the size of a destination canvas large enough to contain the full
/// transformed content of a source rectangle while preserving any translation offsets.
/// </summary>
/// <param name="rectangle">
/// The <see cref="RectangleF"/> representing the transformed bounds of the source content
/// in destination (output) space.
/// </param>
/// <returns>
/// A <see cref="SizeF"/> that describes the canvas dimensions required to fully
/// contain the transformed content while accounting for any positive or negative translation.
/// </returns>
/// <remarks>
/// <para>
/// This method expands the output canvas to ensure that translated content remains visible.
/// </para>
/// <para>
/// If the transformation produces a positive translation, the method extends the canvas
/// on the positive side (right or bottom).
/// If the transformation produces a negative translation (the content moves left or up),
/// the method extends the canvas on the negative side to include that offset.
/// </para>
/// <para>
/// The result is equivalent to taking the union of:
/// <list type="bullet">
/// <item>
/// <description>The original, untransformed rectangle at the origin [0..Width] × [0..Height].</description>
/// </item>
/// <item>
/// <description>The translated rectangle defined by <paramref name="rectangle"/>.</description>
/// </item>
/// </list>
/// This ensures the entire translated image fits within the resulting canvas,
/// without trimming any portion caused by translation.
/// </para>
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Size ConstrainSize(Rectangle rectangle)
private static SizeF GetPreserveCanvasSize(RectangleF rectangle)
{
// We want to resize the canvas here taking into account any translations.
int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom);
int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right);
// If location in either direction is translated to a negative value equal to or exceeding the
// dimensions in either direction we need to reassign the dimension.
// Compute the required height.
// If the top is negative, expand upward by that amount (rectangle.Bottom already includes height).
// Otherwise, take the larger of the transformed height or the bottom offset.
float height = rectangle.Top < 0
? rectangle.Bottom
: MathF.Max(rectangle.Height, rectangle.Bottom);
// Compute the required width.
// If the left is negative, expand leftward by that amount (rectangle.Right already includes width).
// Otherwise, take the larger of the transformed width or the right offset.
float width = rectangle.Left < 0
? rectangle.Right
: MathF.Max(rectangle.Width, rectangle.Right);
// Guard: if translation exceeds or cancels dimensions,
// ensure non-zero positive size using the base rectangle dimensions.
if (height <= 0)
{
height = rectangle.Height;
@ -562,63 +616,63 @@ internal static class TransformUtils
width = rectangle.Width;
}
return new Size(width, height);
// Return the final size that preserves the full visible region of the transformed content.
return new SizeF(width, height);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
private static RectangleF GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors
float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X)));
float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y)));
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
// Clamp the values to the nearest whole pixel.
return Rectangle.FromLTRB(
(int)Math.Floor(left),
(int)Math.Floor(top),
(int)Math.Ceiling(right),
(int)Math.Ceiling(bottom));
return RectangleF.FromLTRB(left, top, right, bottom);
}
private static bool IsAffineRotationOrSkew(Matrix4x4 matrix)
/// <summary>
/// Normalizes an affine 2D matrix so that it operates in pixel space.
/// Applies the row-vector conjugation <c>T(+0.5,+0.5) * M * T(-0.5,-0.5)</c>
/// to align the transform with pixel centers.
/// </summary>
/// <param name="matrix">The affine matrix.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 NormalizeToPixel(Matrix3x2 matrix)
{
const float epsilon = 1e-6f;
const float dx = 0.5f, dy = 0.5f;
// Check if the matrix is affine (last column should be [0, 0, 0, 1])
if (Math.Abs(matrix.M14) > epsilon ||
Math.Abs(matrix.M24) > epsilon ||
Math.Abs(matrix.M34) > epsilon ||
Math.Abs(matrix.M44 - 1f) > epsilon)
{
return false;
}
// Translation component (M41, m42) are allowed, others are not.
if (Math.Abs(matrix.M43) > epsilon)
{
return false;
}
// Extract the linear (rotation and skew) part of the matrix
// Upper-left 3x3 matrix
float m11 = matrix.M11, m12 = matrix.M12, m13 = matrix.M13;
float m21 = matrix.M21, m22 = matrix.M22, m23 = matrix.M23;
float m31 = matrix.M31, m32 = matrix.M32, m33 = matrix.M33;
matrix.M31 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21));
matrix.M32 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22));
return matrix;
}
// Compute the determinant of the linear part
float determinant = (m11 * ((m22 * m33) - (m23 * m32))) -
(m12 * ((m21 * m33) - (m23 * m31))) +
(m13 * ((m21 * m32) - (m22 * m31)));
/// <summary>
/// Normalizes a projective 4×4 matrix so that it operates in pixel space.
/// Applies the row-vector conjugation <c>T(+0.5,+0.5,0) * M * T(-0.5,-0.5,0)</c>
/// to align the transform with pixel centers.
/// </summary>
/// <param name="matrix">The projective matrix.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix4x4 NormalizeToPixel(Matrix4x4 matrix)
{
const float dx = 0.5f, dy = 0.5f;
// Check if the determinant is approximately ±1 (no scaling)
if (Math.Abs(Math.Abs(determinant) - 1f) > epsilon)
// Fast path: affine (no perspective)
if (matrix.M14 == 0f && matrix.M24 == 0f && matrix.M34 == 0f && matrix.M44 == 1f)
{
return false;
// t' = t + (-d + d·L)
matrix.M41 += (-dx) + ((dx * matrix.M11) + (dy * matrix.M21));
matrix.M42 += (-dy) + ((dx * matrix.M12) + (dy * matrix.M22));
return matrix;
}
// All checks passed; the matrix represents rotation and/or skew (with possible translation)
return true;
Matrix4x4 tPos = Matrix4x4.Identity;
tPos.M41 = dx;
tPos.M42 = dy;
Matrix4x4 tNeg = Matrix4x4.Identity;
tNeg.M41 = -dx;
tNeg.M42 = -dy;
return tPos * matrix * tNeg;
}
}

66
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -17,24 +17,9 @@ public class ProjectiveTransformBuilder
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
public ProjectiveTransformBuilder()
: this(TransformSpace.Pixel)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
/// </summary>
/// <param name="transformSpace">
/// The <see cref="TransformSpace"/> to use when applying the projective transform.
/// </param>
public ProjectiveTransformBuilder(TransformSpace transformSpace)
=> this.TransformSpace = transformSpace;
/// <summary>
/// Gets the <see cref="TransformSpace"/> to use when applying the projective transform.
/// </summary>
public TransformSpace TransformSpace { get; }
/// <summary>
/// Prepends a matrix that performs a tapering projective transform.
/// </summary>
@ -43,7 +28,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
=> this.Prepend(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction));
/// <summary>
/// Appends a matrix that performs a tapering projective transform.
@ -53,7 +38,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
=> this.Append(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
@ -69,7 +54,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)));
=> this.Prepend(size => new Matrix4x4(TransformUtilities.CreateRotationTransformMatrixRadians(radians, size)));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -87,7 +72,8 @@ public class ProjectiveTransformBuilder
/// <param name="origin">The rotation origin point.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
internal ProjectiveTransformBuilder PrependRotationRadians(float radians, Vector2 origin)
=> this.PrependMatrix(Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0)));
=> this.PrependMatrix(
Matrix4x4.CreateRotationZ(radians, new Vector3(origin, 0)));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees.
@ -103,7 +89,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace)));
=> this.Append(size => new Matrix4x4(TransformUtilities.CreateRotationTransformMatrixRadians(radians, size)));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -187,7 +173,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)));
=> this.Prepend(size => new Matrix4x4(TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -225,7 +211,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace)));
=> this.Append(size => new Matrix4x4(TransformUtilities.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
@ -288,8 +274,12 @@ public class ProjectiveTransformBuilder
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Prepend(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
=> this.Prepend(size => TransformUtilities.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size),
topLeft,
topRight,
bottomRight,
bottomLeft));
/// <summary>
/// Appends a quad distortion matrix using the specified corner points.
@ -300,8 +290,12 @@ public class ProjectiveTransformBuilder
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
=> this.Append(size => TransformUtils.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace));
=> this.Append(size => TransformUtilities.CreateQuadDistortionMatrix(
new Rectangle(Point.Empty, size),
topLeft,
topRight,
bottomRight,
bottomLeft));
/// <summary>
/// Prepends a raw matrix.
@ -383,15 +377,29 @@ public class ProjectiveTransformBuilder
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
public Size GetTransformedSize(Rectangle sourceRectangle)
public SizeF GetTransformedSize(Rectangle sourceRectangle)
{
Matrix4x4 matrix = this.BuildMatrix(sourceRectangle);
return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace);
return GetTransformedSize(sourceRectangle, matrix);
}
/// <summary>
/// Returns the size of a rectangle large enough to contain the transformed source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <exception cref="DegenerateTransformException">
/// The resultant matrix is degenerate containing one or more values equivalent
/// to <see cref="float.NaN"/> or a zero determinant and therefore cannot be used
/// for linear transforms.
/// </exception>
/// <returns>The <see cref="Size"/>.</returns>
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
private static void CheckDegenerate(Matrix4x4 matrix)
{
if (TransformUtils.IsDegenerate(matrix))
if (TransformUtilities.IsDegenerate(matrix))
{
throw new DegenerateTransformException("Matrix is degenerate. Check input values.");
}

26
src/ImageSharp/Processing/TransformSpace.cs

@ -1,26 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Processing;
/// <summary>
/// Represents the different spaces used in transformation operations.
/// </summary>
public enum TransformSpace
{
/// <summary>
/// Coordinate space is a continuous, mathematical grid where objects and positions
/// are defined with precise, often fractional values. This space allows for fine-grained
/// transformations like scaling, rotation, and translation with high precision.
/// In coordinate space, an image can span from (0,0) to (4,4) for a 4x4 image, including the boundaries.
/// </summary>
Coordinate,
/// <summary>
/// Pixel space is a discrete grid where each position corresponds to a specific pixel on the screen.
/// In this space, positions are defined by whole numbers, with no fractional values.
/// A 4x4 image in pixel space covers exactly 4 pixels wide and 4 pixels tall, ranging from (0,0) to (3,3).
/// Pixel space is used when rendering images to ensure that everything aligns with the actual pixels on the screen.
/// </summary>
Pixel
}

40
tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs

@ -234,7 +234,23 @@ public class AffineTransformTests
image.DebugSave(provider);
Assert.Equal(4, image.Width);
Assert.Equal(8, image.Height);
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<TPixel>(TestImageProvider<TPixel> provider, float x, float y)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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]
@ -267,31 +283,41 @@ public class AffineTransformTests
image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians);
}
[Fact]
public void TransformRotationDoesNotOffset()
[Theory]
[WithSolidFilledImages(100, 100, "DimGray", PixelTypes.Rgba32)]
public void TransformRotationDoesNotOffset<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 background = Color.DimGray.ToPixel<Rgba32>();
Rgba32 marker = Color.Aqua.ToPixel<Rgba32>();
TPixel marker = Color.Aqua.ToPixel<TPixel>();
using Image<TPixel> canvas = provider.GetImage();
using Image<Rgba32> img = new(100, 100, background);
using Image<TPixel> img = canvas.Clone();
img[0, 0] = marker;
img.Mutate(c => c.Rotate(180));
Assert.Equal(marker, img[99, 99]);
using Image<Rgba32> img2 = new(100, 100, background);
img.DebugSave(provider, "Rotate180");
using Image<TPixel> img2 = canvas.Clone();
img2[0, 0] = marker;
img2.Mutate(
c =>
c.Transform(new AffineTransformBuilder().AppendRotationDegrees(180), KnownResamplers.NearestNeighbor));
using Image<Rgba32> img3 = new(100, 100, background);
img.DebugSave(provider, "AffineRotate180NN");
using Image<TPixel> 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);
}

8
tests/ImageSharp.Tests/Processing/Processors/Transforms/ProjectiveTransformTests.cs

@ -249,17 +249,17 @@ public class ProjectiveTransformTests
image.Mutate(ctx => ctx.Transform(builder));
// A 180-degree rotation inverts both axes around the image center.
// The subject location (5, 15) becomes (imageWidth - 5 - 1, imageHeight - 15 - 1) = (94, 84)
// The subject location (5, 15) becomes (imageWidth - 5, imageHeight - 15) = (95, 85)
Assert.Equal(
[94, 84],
[95, 85],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width
// New Y = imageHeight - originalY - height
// (5, 15, 50, 50) becomes (44, 34, 50, 50)
// (5, 15, 50, 50) becomes (45, 35, 50, 50)
Assert.Equal(
[44, 34, 50, 50],
[45, 35, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}

2
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs

@ -652,7 +652,7 @@ public class ResizeTests
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
Assert.Equal(
[2, 7, 11, 11],
[2, 7, 10, 10],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

8
tests/ImageSharp.Tests/Processing/Processors/Transforms/RotateTests.cs

@ -55,17 +55,17 @@ public class RotateTests
image.Mutate(ctx => ctx.Rotate(180));
// A 180-degree rotation inverts both axes around the image center.
// The subject location (5, 15) becomes (imageWidth - 5 - 1, imageHeight - 15 - 1) = (94, 84)
// The subject location (5, 15) becomes (imageWidth - 5, imageHeight - 15) = (95, 85)
Assert.Equal(
[94, 84],
[95, 85],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectLocation).Value);
// The subject area is also mirrored around the center.
// New X = imageWidth - originalX - width
// New Y = imageHeight - originalY - height
// (5, 15, 50, 50) becomes (44, 34, 50, 50)
// (5, 15, 50, 50) becomes (45, 35, 50, 50)
Assert.Equal(
[44, 34, 50, 50],
[45, 35, 50, 50],
image.Metadata.ExifProfile.GetValue(ExifTag.SubjectArea).Value);
}
}

4
tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs

@ -98,7 +98,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
this.AppendRotationDegrees(builder, degrees);
// TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness
Matrix3x2 matrix = TransformUtils.CreateRotationTransformMatrixDegrees(degrees, size, TransformSpace.Pixel);
Matrix3x2 matrix = TransformUtilities.CreateRotationTransformMatrixDegrees(degrees, size);
Vector2 position = new(x, y);
Vector2 expected = Vector2.Transform(position, matrix);
@ -152,7 +152,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
this.AppendSkewDegrees(builder, degreesX, degreesY);
Matrix3x2 matrix = TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size, TransformSpace.Pixel);
Matrix3x2 matrix = TransformUtilities.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size);
Vector2 position = new(x, y);
Vector2 expected = Vector2.Transform(position, matrix);

3
tests/ImageSharp.Tests/TestImages.cs

@ -163,6 +163,9 @@ public static class TestImages
// Issue 2924: https://github.com/SixLabors/ImageSharp/issues/2924
public const string Issue2924 = "Png/issues/Issue_2924.png";
// Issue 3000: https://github.com/SixLabors/ImageSharp/issues/3000
public const string Issue3000 = "Png/issues/issue_3000.png";
public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";

3
tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-3-3.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d75205909d532dc98da52389c804ff99cb3b796b5657afb521659fe221c2b8f0
size 122

3
tests/Images/External/ReferenceOutput/AffineTransformTests/Issue3000_Rgba32_issue_3000_p-4-4.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a75beaec77378de4abb09317afa56b8e99ecba0d1c8571cad31aa790afb1a687
size 123

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbe1ffaf7b801fd92724438cc810fd0c5506e0a907b970c4f0bf5bec3627ca2a
size 551
oid sha256:60b050406fda4ff347660e71cb28a9dfceb4b39532f62ee96cb61d2671d3cf00
size 340

3
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48__original.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74bf3b8655c7548f28c25b1e467992f691dc429f4b06e85cfd04a3b541825811
size 478

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b45933471a1af1b6d4112240e1bc6b6187065a872043ddbf917200ce9e8cc84b
size 371
oid sha256:fbfb3143d96070c58c949e8d1e8d9ddbcf1e7863514489ea2defc65863c84e73
size 276

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(1,2)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b66a5f9d8a7f3f2a78b868bec6c7d1deea927b82d81aa6d1677e0461a3920dc9
size 3800
oid sha256:120b661bef4adac64d362d8c987b3427cd8140ccac7404d09a16765ba1199434
size 5191

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(0)_S(2,1)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5fdc46ee866e088e0ec3221145a3d2d954a0bcb6d25cbb4d538978272f34949
size 4871
oid sha256:0d668ebe5f8857fd21d7eb9ae86860751a6f3061f6c9f76705ff49216dc07870
size 6215

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(-20,-10).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ae57ca0658b1ffa7aca9031f4ec065ab5a9813fb8a9c5acd221526df6a4f729
size 9747
oid sha256:2fb676b3af585e7cbe2efdb893157d5f4e152cf810d0693cafb81596e941e121
size 9697

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5
size 10470
oid sha256:afe7ddbff155b918a4eff91af31e01100355c146cb9c8a12ab2496da8b22821d
size 10446

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1,1)_T(20,10).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e4cc16c2f1b439f8780dead04db01fed95f8e20b68270ae8e7a988af999e3db
size 10561
oid sha256:ad76301984e5b54eae374adfe130693667053fbed181847b4c68688fb74c9818
size 10518

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.1,1.3)_T(30,-20).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06e3966550f1c3ae72796e5522f7829cf1f86daca469c479acf49e6fae72e3d0
size 13227
oid sha256:fbd57af1fa982f9090f57d820a9b927f894914e5f54774e9cd6fdcfe14e5f761
size 13139

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScaleTranslate_Rgba32_TestPattern100x50_R(50)_S(1.5,1.5)_T(0,0).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ce5fefe04cc2a036fddcfcf038901a7a09b4ea5d0621a1e0d3abc8430953ae3
size 20778
oid sha256:c4bbc28c203550baf885cefba95c48a3f91dfb5242c09acbf3a8509b7258048e
size 20768

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_RotateScale_ManuallyCentered_Rgba32_TestPattern96x96_R(50)_S(0.8).png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b653c0fe761d351cb15b09f35da578a954d103dea7507e2c1d7c4ebf3bdac49a
size 10943
oid sha256:566e85b1a527f48c953bcc7bc6c58ebd1fe0b14972c38edd596b025e0dd48624
size 10940

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8
size 13536
oid sha256:aa5b0d5de93f26c0a7a03b57a00d4a49cda62f4a4b98b6d374261467c03a8357
size 13500

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b8970378312c0d479d618e4d5b8da54175c127db517fbe54f9057188d02cc735
size 4165
oid sha256:62267d8d56af3e1452f0e25144f2cfe352b88def98af28e819a3a6982040a4ca
size 4102

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a17bb1653cc6d6ecc292ce0670c651bfea032f61c6a0e84636205bde53a86f8
size 13536
oid sha256:878d5c53b84af4d133825206a327fd4cd02a43831ecabf5c61c5d89181c5a107
size 13499

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9bbf7ef00f98b410f309b3bf70ce87d3c6455666a26e89cd004744145a10408a
size 12559
oid sha256:f0aa3c19852632e603ec425aeecc5243d4c6c24a1ac6e3906d29913bf7ead2df
size 12535

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f9ab86abad276d58bb029bd8e2c2aaffac5618322788cb3619577c7643e10d2
size 14223
oid sha256:ec648c2e8006d478ace4a78d2434a4ef7f10d4a3502468cd8b9e2b1f154620b6
size 14278

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05c4dc9af1fef422fd5ada2fa1459c26253e0fb5e5a13226fa2e7445ece32272
size 17927
oid sha256:6cb06152d5a0765ad86e8005d6ddac469914ccced89d5ee37d77e7d030b97c9e
size 17281

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:82b47e1cad2eea417b99a2e4b68a5ba1a6cd6703f360e8402f3dca8b92373ecc
size 18945
oid sha256:38ea8596a682be0075bb31ed176b1fe04b518eb887235d551a574e338d45880b
size 18869

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b15ce5a201ee6b946de485a58d3d8e779b6841457e096b2bd7a92968a122f9af
size 20844
oid sha256:965f42f021c63a0f2ccc691723c4ad7f92119294aec407c7ffd46a6238c8f268
size 20792

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1622a48b3f4790d66b229ed29acd18504cedf68d0a548832665c28d47ea663b
size 13857
oid sha256:86f1b9e8f1e38070ce862d87c927313904ceaa9e6080f5acead90e82d164738c
size 13879

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74df7b82e2148cfc8dae7e05c96009c0d70c09bf39cdc5ef9d727063d2a8cb3f
size 4154
oid sha256:270f9c2bf5d15fcb21796b3b9eb393e0cc00d9c337036637295ad1efb56781b1
size 4114

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc740ccd76910e384ad84a780591652ac7ee0ea30abf7fd7f5b146f8ff380f07
size 13991
oid sha256:d413162a83c223124a2f29f8154e4bdc08d94bd3e15569ec6cffaa13bdda72c8
size 13953

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ccdc54e814604d4d339f6083091abf852aae65052ceb731af998208faddb5b0b
size 13744
oid sha256:941ea7b4d1f2c187f58920546e2f19fc275505929439cc389edcc59e652e8787
size 13777

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cd24e0a52c7743ab7d3ed255e3757c2d5495b3f56198556a157df589b1fb67ca
size 14889
oid sha256:cc5e6a607ef2343cb74c5227dbc7861840db956951f1fc4703fe53dbccda0974
size 14808

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:878f1aab39b0b2405498c24146b8f81248b37b974e5ea7882e96174a034b645f
size 12374
oid sha256:2ac06a9ba2b2c8bef7e0117ac52fbb790101c0f89313dc49feb1f5a1d929ab02
size 12381

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dcc2bf4f7e0ab3d56ee71ac1e1855dababeb2e4ec167fd5dc264efdc9e727328
size 17027
oid sha256:ad0f483fa7fda620860858c4f330ba914480fba15d70b408fb1aa3fed52dbfc1
size 16839

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6c733878f4c0cc6075a01fbe7cb471f8b3e91c2c5eaf89309ea3c073d9cc4921
size 854
oid sha256:ba501a7fc32a68f8989965aa6457b3860ec42947e2bcd4526c7570ff743f38fc
size 841

4
tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af872886136893938aee82b1ac73e7a1820666a9a5f4bbf34159c09b3283169a
size 5520
oid sha256:8265c5b2e8edd5eaf0aeeccf86cac486e7beec581e696d3b4f4cfee8f4be9b2b
size 5554

4
tests/Images/External/ReferenceOutput/Drawing/DrawImageTests/DrawTransformed.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ba180567e820b145a13c9b26db9c777e95126adfe8e8cacec0ffe1060dcfe8d
size 184124
oid sha256:7f8a4db4facce1d68b363a3b59ea40c9da9fa3c989c736d97a703c84d8230660
size 184595

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abce6af307a81a8ebac8e502142b00b2615403b5570c8dbe7b6895cfdd1a6d60
size 66879
oid sha256:ac986987f25d25ab964a5bef710fe81166cb643d85511906218b4f0e72e9e840
size 30532

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4cda265a50aa26711efafdbcd947c9a01eff872611df5298920583f9a3d4224
size 26458
oid sha256:e0ada2a4d32a3a757b803dbf08148f113f5d358b31af79a77e97c660ce96c302
size 1608

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:278a488a858b8eda141493fe00c617eb1f664196853da8341d7e5b7f231ddce4
size 24645
oid sha256:ffc30373989ec6857797b460931f011b30baaec633b095b6fc3d8fd5d43c77ec
size 2467

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e03e79e6fab3a9e43041e54640a04c7cc3677709e7d879f9f410cf8afc7547a7
size 42691
oid sha256:9828ef0faf1a6709673cfe39028ed4202920d346bcc172bda6683bb3d1d0a7a3
size 36577

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Bicubic.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206
size 10951
oid sha256:6bff913e6e67129325203fae91278ca17407b10d99c4e4f571e6cfe3b5b7f93c
size 10889

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Box.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d0cf291ebf5d8cebab1cd76e2830e5e2d2e0d9a050f7187da72680ead39110c
size 2757
oid sha256:54b761b76d03216e7aa6238eee92755c03f7b016bffd1400be66ecf136b29c26
size 2747

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_CatmullRom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:543dbf5376386bf518830850645d69934e2ca17ab208ce3fd5274a6a172f5206
size 10951
oid sha256:16da371a29269dade522b3d602beee8f769723c5712a348d960805b75619376d
size 10889

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Hermite.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57698b6666029a55edf8bd35a7ba96f68d224988cf01308a3af1c6606ae9d0b1
size 10174
oid sha256:b25b190603828131be8d82a27e019353c9bf80dcb38536e325abc5aa065762ed
size 10230

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc7c9da04142a679887c714c43f1838eba0092a869140d234fce3412673207c6
size 13575
oid sha256:0cc07a20532c52151388c42d7add4f9749913c4dd7629253400a51d40760df23
size 13566

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8b973f41f8afa39b94c71b307b7eb393953e2d083d56e1f0e8f43d6ab1f342a
size 16821
oid sha256:64aae32ec91233b6a139d2f515db4a3e609fa3ab6c660cb53b05d672e7f70e6f
size 16795

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos5.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:122c1501e09516244f0db36e1cca373ff68514a18e84f57ed3072d52d6112e36
size 17022
oid sha256:c6167a1fb585b49167f4c8fa1f19111f10c825ea7d41ae4e266680d22b2bb28e
size 17094

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Lanczos8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:12181516bce69c9302f15bba928fd530796449599cb9744a2411cc796788ee3b
size 18066
oid sha256:131e831cc2e2d8eb4f5860d0e685b31006ab846433e38440b6a85a445aed1a12
size 17890

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_MitchellNetravali.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1eb5accc5ada5b963ecef6ac15bfb1845f481e51aef63e06a522ea73bbeab945
size 11194
oid sha256:aedcc9342e0b37d60759330f62db446646c31da172e21d931ee8e8451ee720ae
size 11193

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_NearestNeighbor.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0418f0ea38ec19b407f2b5046d7ff0ed207189ad71db1e50e82c419d83620543
size 2759
oid sha256:34f21056cac1ec3f1bd37a6c50466210e7ca7d8263963d2c503535b40e5b31d8
size 2752

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Robidoux.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1233a9ab2c4b0b17b0248c3d40050c466330c22095287dfbdb8bf7dfbda4ff1f
size 11212
oid sha256:b4e0cbe71672de45880111fb45c7b544203f67154060fa0707ba9216dfd6d883
size 11217

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_RobidouxSharp.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e2912d4e42c7b76d9ff48a49921d6472e351662597d69b88bc3708683c7933e3
size 11221
oid sha256:e8208cf34114bc87c6244f83a73e7c9dd4455da2fb6d25c34e32ed2fef3cfc9a
size 11214

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Spline.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:51b05c38647e0c1d88cc722e4109a882305073a065d2a27ccd3bee82f727127d
size 11775
oid sha256:c4ee4328adcc71b1d9b3786ab2c03906aa725fefadd1d521206d5a04af421d8d
size 11711

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Triangle.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b260e816b23a43d7efb7507897ba2f5dbb6a596dd67a5abd4c9a0c005e926ee0
size 9748
oid sha256:fbc694ac18a702c127c588bb9184bcc39a01c1b8be5ceecadeaab4477260afec
size 9984

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithSampler_Rgba32_TestPattern150x150_Welch.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:50b03d627bb53048f5e56380500f116da4c503f5bb6a1b1d3c0d67ee4256d8f6
size 15977
oid sha256:8e85f331d7c4304fcd8ea8788da04982d9a5e43951be642bd7dbacd8907c3151
size 15784

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-Both.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96454548849147d7c7f0ca507c8521a7d5eaa7771f9f383cc836858870b52c57
size 280
oid sha256:92153056f19a20cc1d6ff65dd36ffd215eb50509cc3544e338e76c8d5665fb27
size 278

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-LeftOrTop.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e94d224fdb284b6f1ba21b8caa66174edd7e6a3027f9dd03f4757e08296e6508
size 212
oid sha256:0db75a869ae36fbca7f57daa4495f2c16050b226474d203aba98cb8e0766d3fb
size 249

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Bottom-RightOrBottom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d1162be9fa1f31bee8d3cba05c1422a1945621a412be11cce13d376efd5c679c
size 173
oid sha256:09a80b11d888da121313d5f00ab0ec79ccf7bc49800135aa5eb411bd15fc6b86
size 204

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-Both.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0ed262e9b885af773a4a40a4506e678630670e208bf7f9ec10307e943b166bed
size 258
oid sha256:1ff446c4bb62d4492fc561a9dd48c4c0d95d8f4bcd9bbadf1675b2621baf9fa1
size 212

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-LeftOrTop.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a24f2cfc225d01294b8bbc5ca7d7f1738fb0b79217046eb9edf04e4c4c01851
size 201
oid sha256:2d76e8055ddbdaaa8f21117ab5e06d3e7d0f5da9d21dd2a992d82e284f028606
size 235

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Left-RightOrBottom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:938186fb3d0f468176988a9530efd22e66241a1361fff027005ec8a8ae323ff3
size 197
oid sha256:c7e594fd12ea7863d297d66852d3f80d5d3645636cdd39611c7b6fae068a6dc9
size 230

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-Both.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4bc4b8ea7e7f10676d8de612fe6bc5144e100b95ff3fe7a1e3d4066a7684ce4d
size 239
oid sha256:50f7f407d040b1071f7f6fbad96d6cfb2907d87060c671e74f6122ac5d381c30
size 208

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-LeftOrTop.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:345337f7dffa48d95251503ee2ae8e91db98b5cbe06b579d73c38a018c781544
size 182
oid sha256:f305e35f0f4fac1c099b5d9f0b2775c1bb17f382aba13a068dedff45eda4632d
size 215

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Right-RightOrBottom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de4e2b71dade9dfb750a2c614a684963d6962958db79145c87fd23d9f0f8c005
size 180
oid sha256:c41784431d5a50746f66b3c34e966cb21fa8a088de797825633349cb077b4f97
size 212

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-Both.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d8b651663366e7543211635f337c229e2f88f1142886ea3a9b69587daaada97
size 288
oid sha256:7a6b2c8e072993c00a96692f10c7ebe6fcc6f4cfdcb9dff2d0aa6c65db54e1c4
size 267

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-LeftOrTop.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ab8df31f1716c05bb8687f79c7d1154f6cc6f65e3917abe60ecc42d0df173dc
size 215
oid sha256:018fe6af5dd7ed2edd163bf3da5e22f8333d3e3287629a2065ebfd15b7a2a8b3
size 256

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithTaperMatrix_Rgba32_Solid30x30_(255,0,0,255)_Top-RightOrBottom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a1671da9ea7702a37a866fabfb3ca0d746233ee108594198f23cb563af43ae6
size 180
oid sha256:530501bed8301799b68ba3dc8d50c877ed3f58073ab1a8a283e8f776e761cd28
size 200

4
tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e7dedec16ccd66543a1d44052a104957ba62099ba2f2ccc72285c233c2ae3fa
size 4411
oid sha256:73a18d217d0db5e6ed55cbf52801b7f083e083ca2f0a6ac20d320fd29961e6e0
size 4399

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-170.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3594547265b23603b1a76ff6bc6f0eab4af55d6e0070e53356123dfc7ae256f8
size 9034
oid sha256:881daeef5b8db99af8b89e7d4e968fb4c43e13c904e936aefa1d0156b767803e
size 9051

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_-50.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ae9ef073f3338b71d2a40fcf2e89d9b6ab62204d6de9b6a1f75f4705ee197f0
size 10704
oid sha256:86032d5b7e49574655c1cd54886ac57d6385714481ba6bd72176d858f884cd1a
size 10720

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_170.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:994dda7da034595aa77d107652bea06c86077d24ef8a6883b18f1f509bb19928
size 8906
oid sha256:cb823498eacf5bab12f7607d230523a3488e26fcc10fe1b3ab51de900a9dff97
size 8835

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern100x50_50.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0fced9def2b41cbbf215a49ea6ef6baf4c3c041fd180671eb209db5c6e7177e5
size 10470
oid sha256:afe7ddbff155b918a4eff91af31e01100355c146cb9c8a12ab2496da8b22821d
size 10446

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-170.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:29c5f48f1ece0b12854b4c44fba84fdfc9ac5751cdf564a32478dcdaed43b2a4
size 9798
oid sha256:be885eca2d9771be335a29d3da533a17376efa4c563996ac4cb37138fd1899eb
size 9826

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_-50.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7de58474c3f386c4ec31a9088d561a513f82c08d1157132d735169b847b9680
size 11579
oid sha256:afea3d7ec03c945d695b3cd85e9ea1d79cb35745256efe1e32273eb08789e5ce
size 11476

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_170.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3ef9b7051d7a5733dfe2534fddefdc28dfbc49d087355f46c4d945b04f0e3936
size 9672
oid sha256:096eba716663179933fe6eb526822ac9dedf83c3633d43df328ed33fb16900f6
size 9687

4
tests/Images/External/ReferenceOutput/Transforms/RotateTests/Rotate_WithAngle_TestPattern50x100_50.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:825770c9b2e9f265d834eab6b40604df5508bf9bc5b4f82f5d3effd6d5a26935
size 11434
oid sha256:c7367063704e10827bb81f46ebb56b6fe87a816eb8ec258ca95d8e26d9276f0f
size 11408

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_-20_-10.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e283463b0f450dd72cf303acccf3dd1ff7a31fe401ff0f288d67c4baefca240
size 8742
oid sha256:37acdbbcebe56ab98e17b025312e5860c471d807603c2ce6f4a50fd5f219c0f7
size 8732

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Bgra32_TestPattern100x50_20_10.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:485d9d9ef955a04af43d17e6bc3952e9bf65a9752b6cf8ba9cbbe8f772f05a18
size 8995
oid sha256:722d191e930331e296a548641a80ac163430bdb4017e3d68e4638fbb1d6ed45c
size 9021

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_-20_-10.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3d749ac365764051ea16bc39d1aff84c06faf282359805b58bb97c9eed7f0bb
size 6400
oid sha256:ce2bbe927b718a1b4de05b2baad7016b69490d1b5dfb085420192b7ac6c0ec5d
size 6367

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_IsNotBoundToSinglePixelType_Rgb24_TestPattern100x50_20_10.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d82f2a15502b0a29aa4df1077ec90c88f9211f283fdc0edd7b059ed9b387441
size 6334
oid sha256:d79a7044e95a1ca032124366e4705bd93a866609547ebb489ff7d2228547cea5
size 6330

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_WorksWithAllResamplers_ducky_Bicubic.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e8afa56c5abb0e4b5895f35415db1178d041120d9f8306902f554cfaaada88d
size 26540
oid sha256:677e4419a1cac8692da2d852f6e169c4a66ef8f164fffa65550fccb71bb54563
size 26606

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_WorksWithAllResamplers_ducky_Box.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2c174ef54b68f025352e25800f655fe3b94a0d3f75cb48bd2ac0e8d6931faf8
size 24827
oid sha256:0f700e854d2d4ee9c12150ef300e0041fa4706ae595eeea6869894a1e2947eaf
size 24963

4
tests/Images/External/ReferenceOutput/Transforms/SkewTests/Skew_WorksWithAllResamplers_ducky_CatmullRom.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6b56ceae2f350a1402beecc5b5e2930be1011a95fbf224cccf73b96f3931b646
size 26531
oid sha256:965248b773ea4b3f8e870ff598ccf88784dd700d9aa063b70b0146a00fc806bc
size 26613

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save