Skip to content

Commit

Permalink
Separate transform spaces
Browse files Browse the repository at this point in the history
  • Loading branch information
JimBobSquarePants committed Aug 13, 2024
1 parent e1555fd commit c579547
Show file tree
Hide file tree
Showing 75 changed files with 262 additions and 185 deletions.
36 changes: 29 additions & 7 deletions src/ImageSharp/Processing/AffineTransformBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ public class AffineTransformBuilder
{
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new();

/// <summary>
/// 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.
Expand All @@ -30,7 +52,7 @@ public AffineTransformBuilder PrependRotationDegrees(float degrees)
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size));
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));

/// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
Expand Down Expand Up @@ -66,7 +88,7 @@ public AffineTransformBuilder AppendRotationDegrees(float degrees)
/// <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.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size, this.TransformSpace));

/// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
Expand Down Expand Up @@ -141,7 +163,7 @@ public AffineTransformBuilder AppendScale(Vector2 scales)
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size));
=> this.PrependSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));

/// <summary>
/// Prepends a centered skew matrix from the give angles in radians.
Expand All @@ -150,7 +172,7 @@ public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
/// <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.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));

/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
Expand Down Expand Up @@ -179,7 +201,7 @@ public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY,
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size));
=> this.AppendSkewRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));

/// <summary>
/// Appends a centered skew matrix from the give angles in radians.
Expand All @@ -188,7 +210,7 @@ public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
/// <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.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size, this.TransformSpace));

/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
Expand Down Expand Up @@ -334,7 +356,7 @@ public Size GetTransformedSize(Rectangle sourceRectangle)
CheckDegenerate(matrix);
}

return TransformUtils.GetTransformedSize(matrix, size);
return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace);
}

private static void CheckDegenerate(Matrix3x2 matrix)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ public RotateProcessor(float degrees, Size sourceSize)
/// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize),
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize, TransformSpace.Pixel),
sampler,
sourceSize)
=> this.Degrees = degrees;

// Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize))
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize, TransformSpace.Pixel))
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public SkewProcessor(float degreesX, float degreesY, Size sourceSize)
/// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize),
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize, TransformSpace.Pixel),
sampler,
sourceSize)
{
Expand All @@ -40,7 +40,7 @@ public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size so

// Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize))
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize, TransformSpace.Pixel))
{
}

Expand Down
70 changes: 38 additions & 32 deletions src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,53 +83,60 @@ public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix)
/// </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)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size);
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size, TransformSpace transformSpace)
=> CreateRotationTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degrees), size, transformSpace);

/// <summary>
/// Creates a centered rotation transform matrix using the given rotation in radians and the 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)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size);
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size, transformSpace);

/// <summary>
/// Creates a centered skew transform matrix from the give angles in degrees and the 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)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size);
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size, TransformSpace transformSpace)
=> CreateSkewTransformMatrixRadians(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), size, transformSpace);

/// <summary>
/// Creates a centered skew transform matrix from the give angles in radians and the 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)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size);
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size, TransformSpace transformSpace)
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size, transformSpace);

/// <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)
public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
{
Size destinationSize = GetUnboundedTransformedSize(matrix, 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.
Expand All @@ -138,8 +145,10 @@ public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size siz
// 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.
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F);
float offset = transformSpace == TransformSpace.Pixel ? 1F : 0F;

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);

// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
Expand All @@ -161,12 +170,6 @@ public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner
{
Matrix4x4 matrix = Matrix4x4.Identity;

// The source size is provided using the Coordinate/Geometric space of the source image.
// However, the transform should always be applied in the Discrete/Pixel space to ensure
// that the transformation fully encompasses all pixels without clipping at the edges.
// To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space.
// size -= new Size(1, 1);

/*
* SkMatrix is laid out in the following manner:
*
Expand Down Expand Up @@ -280,11 +283,10 @@ public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
public static Size GetTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, true);
/// <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);

/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
Expand Down Expand Up @@ -355,22 +357,22 @@ public static Size GetTransformedSize(Matrix4x4 matrix, Size size)
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when calculating the size.</param>
/// <returns>The <see cref="Size"/>.</returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace)
=> GetTransformedSize(matrix, size, transformSpace, false);

/// <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="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, bool constrain)
private static Size GetTransformedSize(Matrix3x2 matrix, Size size, TransformSpace transformSpace, bool constrain)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");

Expand All @@ -381,9 +383,13 @@ private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constra

// Define an offset size to translate between coordinate space and pixel space.
// 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)
SizeF offsetSize = new(scaleX, scaleY);
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(scaleX, scaleY);
}

// Subtract the offset size to translate to the pixel space.
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
Expand Down
Loading

0 comments on commit c579547

Please sign in to comment.