diff --git a/README.md b/README.md
index dc30734792..4d4375dbf5 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,14 @@ SixLabors.ImageSharp
[](https://github.com/SixLabors/ImageSharp/actions)
[](https://codecov.io/gh/SixLabors/ImageSharp)
[](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
-[](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
-### **ImageSharp** is a new, fully featured, fully managed, cross-platform, 2D graphics API.
+### **ImageSharp** is a high-performance, fully managed, cross-platform 2D graphics API.
-ImageSharp is a new, fully featured, fully managed, cross-platform, 2D graphics library.
-Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
+ImageSharp is a mature, fully featured, high-performance image processing and graphics library for .NET, built for workloads across device, cloud, and embedded/IoT scenarios.
-ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations.
+Designed from the ground up to balance performance, portability, and ease of use, ImageSharp provides a powerful yet approachable API for common image processing tasks, along with the low-level building blocks needed to extend the library for specialized workflows.
Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp can be used in device, cloud, and embedded/IoT scenarios.
diff --git a/src/ImageSharp/Primitives/Point.cs b/src/ImageSharp/Primitives/Point.cs
index 8627fe980a..7660855479 100644
--- a/src/ImageSharp/Primitives/Point.cs
+++ b/src/ImageSharp/Primitives/Point.cs
@@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp;
@@ -234,6 +235,17 @@ public Point(Size size)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix));
+ ///
+ /// Transforms a point by a specified 4x4 matrix, applying a projective transform
+ /// flattened into 2D space.
+ ///
+ /// The point to transform.
+ /// The transformation matrix used.
+ /// The transformed .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Transform(Point point, Matrix4x4 matrix)
+ => Round(TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix));
+
///
/// Deconstructs this point into two integers.
///
diff --git a/src/ImageSharp/Primitives/PointF.cs b/src/ImageSharp/Primitives/PointF.cs
index 35a506bb41..7979c0af06 100644
--- a/src/ImageSharp/Primitives/PointF.cs
+++ b/src/ImageSharp/Primitives/PointF.cs
@@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
+using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp;
@@ -246,6 +247,17 @@ public PointF(SizeF size)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix);
+ ///
+ /// Transforms a point by a specified 4x4 matrix, applying a projective transform
+ /// flattened into 2D space.
+ ///
+ /// The point to transform.
+ /// The transformation matrix used.
+ /// The transformed .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Transform(PointF point, Matrix4x4 matrix)
+ => TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
+
///
/// Deconstructs this point into two floats.
///
diff --git a/src/ImageSharp/Primitives/Rectangle.cs b/src/ImageSharp/Primitives/Rectangle.cs
index e2ae5071ef..6494964412 100644
--- a/src/ImageSharp/Primitives/Rectangle.cs
+++ b/src/ImageSharp/Primitives/Rectangle.cs
@@ -266,6 +266,20 @@ public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix)
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}
+ ///
+ /// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
+ /// flattened into 2D space.
+ ///
+ /// The source rectangle.
+ /// The transformation matrix.
+ /// A transformed rectangle.
+ public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix)
+ {
+ PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
+ PointF topLeft = PointF.Transform(new PointF(rectangle.Location.X, rectangle.Location.Y), matrix);
+ return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
+ }
+
///
/// Converts a to a by performing a truncate operation on all the coordinates.
///
diff --git a/src/ImageSharp/Primitives/RectangleF.cs b/src/ImageSharp/Primitives/RectangleF.cs
index 68add77d09..a66e3fcad6 100644
--- a/src/ImageSharp/Primitives/RectangleF.cs
+++ b/src/ImageSharp/Primitives/RectangleF.cs
@@ -241,6 +241,20 @@ public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix)
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}
+ ///
+ /// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
+ /// flattened into 2D space.
+ ///
+ /// The source rectangle.
+ /// The transformation matrix.
+ /// A transformed .
+ public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix)
+ {
+ PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
+ PointF topLeft = PointF.Transform(rectangle.Location, matrix);
+ return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
+ }
+
///
/// Creates a rectangle that represents the union between and .
///
diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs
index c1e11a1d42..84d0d2ea1d 100644
--- a/src/ImageSharp/Processing/AffineTransformBuilder.cs
+++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs
@@ -349,6 +349,16 @@ public SizeF GetTransformedSize(Rectangle sourceRectangle)
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
+ ///
+ /// Clears all accumulated transform matrices, resetting the builder to its initial state.
+ ///
+ /// The .
+ public AffineTransformBuilder Clear()
+ {
+ this.transformMatrixFactories.Clear();
+ return this;
+ }
+
private static void CheckDegenerate(Matrix3x2 matrix)
{
if (TransformUtilities.IsDegenerate(matrix))
diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
index 17bdeadde1..5c7de17100 100644
--- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
+++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
@@ -69,11 +69,17 @@ public static bool IsNaN(Matrix4x4 matrix)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix)
{
- // The w component (v4.W) resulting from the transformation can be less than 0 in certain cases,
- // such as when the point is transformed behind the camera in a perspective projection.
- // However, in many 2D contexts, negative w values are not meaningful and could cause issues
- // like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure
- // we don't divide by a very small or negative number, effectively treating any negative w as epsilon.
+ // Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and
+ // performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space.
+ //
+ // For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide
+ // is a no-op, producing the same result as Vector2.Transform(v, Matrix4x4).AsVector2()
+ // (the approach used by .NET 10+).
+ //
+ // For projective matrices (taper, quad distortion) W varies per point and the divide
+ // is essential for correct perspective mapping. W <= 0 means the point has crossed the
+ // vanishing line of the projection; clamping to epsilon avoids division by zero or
+ // negative values that would flip/mirror the output.
const float epsilon = 0.0000001F;
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon);
diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
index e40a307a3d..74da440401 100644
--- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
+++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
@@ -397,6 +397,16 @@ public SizeF GetTransformedSize(Rectangle sourceRectangle)
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
+ ///
+ /// Clears all accumulated transform matrices, resetting the builder to its initial state.
+ ///
+ /// The .
+ public ProjectiveTransformBuilder Clear()
+ {
+ this.transformMatrixFactories.Clear();
+ return this;
+ }
+
private static void CheckDegenerate(Matrix4x4 matrix)
{
if (TransformUtilities.IsDegenerate(matrix))
diff --git a/tests/ImageSharp.Tests/Primitives/PointFTests.cs b/tests/ImageSharp.Tests/Primitives/PointFTests.cs
index 8b574daf0d..bea0f802fc 100644
--- a/tests/ImageSharp.Tests/Primitives/PointFTests.cs
+++ b/tests/ImageSharp.Tests/Primitives/PointFTests.cs
@@ -133,6 +133,69 @@ public void SkewTest()
Assert.Equal(new PointF(30, 30), pout);
}
+ [Fact]
+ public void TransformMatrix4x4_AffineMatchesMatrix3x2()
+ {
+ PointF p = new(13, 17);
+ Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty);
+ Matrix4x4 m4 = new(m3);
+
+ PointF r3 = PointF.Transform(p, m3);
+ PointF r4 = PointF.Transform(p, m4);
+
+ Assert.Equal(r3.X, r4.X, ApproximateFloatComparer);
+ Assert.Equal(r3.Y, r4.Y, ApproximateFloatComparer);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Identity()
+ {
+ PointF p = new(42.5F, -17.3F);
+ PointF result = PointF.Transform(p, Matrix4x4.Identity);
+
+ Assert.Equal(p.X, result.X, ApproximateFloatComparer);
+ Assert.Equal(p.Y, result.Y, ApproximateFloatComparer);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Translation()
+ {
+ PointF p = new(10, 20);
+ Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
+ PointF result = PointF.Transform(p, m);
+
+ Assert.Equal(15F, result.X, ApproximateFloatComparer);
+ Assert.Equal(17F, result.Y, ApproximateFloatComparer);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Scale()
+ {
+ PointF p = new(10, 20);
+ Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
+ PointF result = PointF.Transform(p, m);
+
+ Assert.Equal(20F, result.X, ApproximateFloatComparer);
+ Assert.Equal(60F, result.Y, ApproximateFloatComparer);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Projective()
+ {
+ // A taper matrix with M14 != 0 produces W != 1, requiring perspective divide.
+ PointF p = new(100, 50);
+ Matrix4x4 m = Matrix4x4.Identity;
+ m.M14 = 0.005F; // perspective component
+
+ PointF result = PointF.Transform(p, m);
+
+ // W = x*M14 + M44 = 100*0.005 + 1 = 1.5
+ // X = x*M11 + M41 = 100, Y = y*M22 + M42 = 50
+ // result = (100/1.5, 50/1.5)
+ Assert.Equal(100F / 1.5F, result.X, ApproximateFloatComparer);
+ Assert.Equal(50F / 1.5F, result.Y, ApproximateFloatComparer);
+ }
+
[Theory]
[InlineData(float.MaxValue, float.MinValue)]
[InlineData(float.MinValue, float.MaxValue)]
diff --git a/tests/ImageSharp.Tests/Primitives/PointTests.cs b/tests/ImageSharp.Tests/Primitives/PointTests.cs
index 3ad2a83b3d..22c18a1470 100644
--- a/tests/ImageSharp.Tests/Primitives/PointTests.cs
+++ b/tests/ImageSharp.Tests/Primitives/PointTests.cs
@@ -174,6 +174,51 @@ public void SkewTest()
Assert.Equal(new Point(30, 30), pout);
}
+ [Fact]
+ public void TransformMatrix4x4_AffineMatchesMatrix3x2()
+ {
+ Point p = new(13, 17);
+ Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
+ Matrix4x4 m4 = new(m3);
+
+ Point r3 = Point.Transform(p, m3);
+ Point r4 = Point.Transform(p, m4);
+
+ Assert.Equal(r3, r4);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Identity()
+ {
+ Point p = new(42, -17);
+ Point result = Point.Transform(p, Matrix4x4.Identity);
+
+ Assert.Equal(p, result);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Translation()
+ {
+ Point p = new(10, 20);
+ Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
+ Point result = Point.Transform(p, m);
+
+ Assert.Equal(new Point(15, 17), result);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Projective()
+ {
+ Point p = new(100, 50);
+ Matrix4x4 m = Matrix4x4.Identity;
+ m.M14 = 0.005F;
+
+ Point result = Point.Transform(p, m);
+
+ // W = 100*0.005 + 1 = 1.5 => (100/1.5, 50/1.5) => rounded
+ Assert.Equal(Point.Round(new PointF(100F / 1.5F, 50F / 1.5F)), result);
+ }
+
[Theory]
[InlineData(int.MaxValue, int.MinValue)]
[InlineData(int.MinValue, int.MinValue)]
diff --git a/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs b/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs
index 4122daaa52..e3a13618bd 100644
--- a/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs
+++ b/tests/ImageSharp.Tests/Primitives/RectangleFTests.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Globalization;
+using System.Numerics;
namespace SixLabors.ImageSharp.Tests;
@@ -243,6 +244,48 @@ public void OffsetTest(float x, float y, float width, float height)
Assert.Equal(expectedRect, r1);
}
+ [Fact]
+ public void TransformMatrix4x4_AffineMatchesMatrix3x2()
+ {
+ RectangleF rect = new(10, 20, 100, 50);
+ Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
+ Matrix4x4 m4 = new(m3);
+
+ RectangleF r3 = RectangleF.Transform(rect, m3);
+ RectangleF r4 = RectangleF.Transform(rect, m4);
+
+ Assert.Equal(r3, r4);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Identity()
+ {
+ RectangleF rect = new(10, 20, 100, 50);
+ RectangleF result = RectangleF.Transform(rect, Matrix4x4.Identity);
+
+ Assert.Equal(rect, result);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Translation()
+ {
+ RectangleF rect = new(10, 20, 100, 50);
+ Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
+ RectangleF result = RectangleF.Transform(rect, m);
+
+ Assert.Equal(new RectangleF(15, 17, 100, 50), result);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Scale()
+ {
+ RectangleF rect = new(10, 20, 100, 50);
+ Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
+ RectangleF result = RectangleF.Transform(rect, m);
+
+ Assert.Equal(new RectangleF(20, 60, 200, 150), result);
+ }
+
[Fact]
public void ToStringTest()
{
diff --git a/tests/ImageSharp.Tests/Primitives/RectangleTests.cs b/tests/ImageSharp.Tests/Primitives/RectangleTests.cs
index 2800852afd..104bce7541 100644
--- a/tests/ImageSharp.Tests/Primitives/RectangleTests.cs
+++ b/tests/ImageSharp.Tests/Primitives/RectangleTests.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Globalization;
+using System.Numerics;
namespace SixLabors.ImageSharp.Tests;
@@ -294,6 +295,38 @@ public void OffsetTest(int x, int y, int width, int height)
Assert.Equal(expectedRect, r1);
}
+ [Fact]
+ public void TransformMatrix4x4_AffineMatchesMatrix3x2()
+ {
+ Rectangle rect = new(10, 20, 100, 50);
+ Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
+ Matrix4x4 m4 = new(m3);
+
+ RectangleF r3 = Rectangle.Transform(rect, m3);
+ RectangleF r4 = Rectangle.Transform(rect, m4);
+
+ Assert.Equal(r3, r4);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Identity()
+ {
+ Rectangle rect = new(10, 20, 100, 50);
+ RectangleF result = Rectangle.Transform(rect, Matrix4x4.Identity);
+
+ Assert.Equal(new RectangleF(10, 20, 100, 50), result);
+ }
+
+ [Fact]
+ public void TransformMatrix4x4_Translation()
+ {
+ Rectangle rect = new(10, 20, 100, 50);
+ Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
+ RectangleF result = Rectangle.Transform(rect, m);
+
+ Assert.Equal(new RectangleF(15, 17, 100, 50), result);
+ }
+
[Fact]
public void ToStringTest()
{
diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
index 232571d4d0..d0e4cf5663 100644
--- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
@@ -58,6 +58,9 @@ protected override void PrependSkewRadians(AffineTransformBuilder builder, float
protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate)
=> builder.PrependTranslation(translate);
+ protected override void ClearBuilder(AffineTransformBuilder builder)
+ => builder.Clear();
+
protected override Vector2 Execute(
AffineTransformBuilder builder,
Rectangle rectangle,
diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs
index a0380033fd..dcc6533163 100644
--- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs
@@ -55,6 +55,9 @@ protected override void PrependTranslation(ProjectiveTransformBuilder builder, P
protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) =>
builder.PrependRotationRadians(radians, origin);
+ protected override void ClearBuilder(ProjectiveTransformBuilder builder)
+ => builder.Clear();
+
protected override Vector2 Execute(
ProjectiveTransformBuilder builder,
Rectangle rectangle,
diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
index 73215b1c6f..12f8326fc7 100644
--- a/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
+++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
@@ -248,6 +248,48 @@ public void ThrowsForInvalidMatrix()
});
}
+ [Fact]
+ public void Clear_ResetsBuilderToIdentity()
+ {
+ Size size = new(100, 100);
+ Rectangle rectangle = new(Point.Empty, size);
+ Vector2 source = new(10, 20);
+
+ TBuilder builder = this.CreateBuilder();
+
+ // Apply a transform that changes the point.
+ this.AppendScale(builder, new SizeF(2, 3));
+ this.AppendTranslation(builder, new PointF(50, 50));
+ Vector2 transformed = this.Execute(builder, rectangle, source);
+ Assert.NotEqual(source, transformed, Comparer);
+
+ // Clear and verify the builder produces identity behavior.
+ this.ClearBuilder(builder);
+ Vector2 afterClear = this.Execute(builder, rectangle, source);
+ Assert.Equal(source, afterClear, Comparer);
+ }
+
+ [Fact]
+ public void Clear_AllowsReuse()
+ {
+ Size size = new(100, 100);
+ Rectangle rectangle = new(Point.Empty, size);
+ Vector2 source = new(10, 20);
+
+ TBuilder builder = this.CreateBuilder();
+
+ // First transform: scale by 2.
+ this.AppendScale(builder, new SizeF(2, 2));
+ Vector2 scaled = this.Execute(builder, rectangle, source);
+ Assert.Equal(new Vector2(20, 40), scaled, Comparer);
+
+ // Clear and apply a different transform: translate.
+ this.ClearBuilder(builder);
+ this.AppendTranslation(builder, new PointF(5, 10));
+ Vector2 translated = this.Execute(builder, rectangle, source);
+ Assert.Equal(new Vector2(15, 30), translated, Comparer);
+ }
+
protected abstract TBuilder CreateBuilder();
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees);
@@ -282,5 +324,7 @@ public void ThrowsForInvalidMatrix()
protected abstract void PrependTranslation(TBuilder builder, PointF translate);
+ protected abstract void ClearBuilder(TBuilder builder);
+
protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint);
}