Skip to content

Commit 5842434

Browse files
Merge branch 'main' into bp/fixIssue3078
2 parents 60c556c + 0d53f96 commit 5842434

File tree

15 files changed

+320
-10
lines changed

15 files changed

+320
-10
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@ SixLabors.ImageSharp
1010
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
1111
[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
1212
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
13-
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](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)
1413

1514
</div>
1615

17-
### **ImageSharp** is a new, fully featured, fully managed, cross-platform, 2D graphics API.
16+
### **ImageSharp** is a high-performance, fully managed, cross-platform 2D graphics API.
1817

19-
ImageSharp is a new, fully featured, fully managed, cross-platform, 2D graphics library.
20-
Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
18+
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.
2119

22-
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.
20+
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.
2321

2422
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.
2523

src/ImageSharp/Primitives/Point.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.ComponentModel;
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
7+
using SixLabors.ImageSharp.Processing.Processors.Transforms;
78

89
namespace SixLabors.ImageSharp;
910

@@ -234,6 +235,17 @@ public Point(Size size)
234235
[MethodImpl(MethodImplOptions.AggressiveInlining)]
235236
public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix));
236237

238+
/// <summary>
239+
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
240+
/// flattened into 2D space.
241+
/// </summary>
242+
/// <param name="point">The point to transform.</param>
243+
/// <param name="matrix">The transformation matrix used.</param>
244+
/// <returns>The transformed <see cref="Point"/>.</returns>
245+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
246+
public static Point Transform(Point point, Matrix4x4 matrix)
247+
=> Round(TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix));
248+
237249
/// <summary>
238250
/// Deconstructs this point into two integers.
239251
/// </summary>

src/ImageSharp/Primitives/PointF.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.ComponentModel;
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
7+
using SixLabors.ImageSharp.Processing.Processors.Transforms;
78

89
namespace SixLabors.ImageSharp;
910

@@ -246,6 +247,17 @@ public PointF(SizeF size)
246247
[MethodImpl(MethodImplOptions.AggressiveInlining)]
247248
public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix);
248249

250+
/// <summary>
251+
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
252+
/// flattened into 2D space.
253+
/// </summary>
254+
/// <param name="point">The point to transform.</param>
255+
/// <param name="matrix">The transformation matrix used.</param>
256+
/// <returns>The transformed <see cref="PointF"/>.</returns>
257+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
258+
public static PointF Transform(PointF point, Matrix4x4 matrix)
259+
=> TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
260+
249261
/// <summary>
250262
/// Deconstructs this point into two floats.
251263
/// </summary>

src/ImageSharp/Primitives/Rectangle.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,20 @@ public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix)
266266
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
267267
}
268268

269+
/// <summary>
270+
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
271+
/// flattened into 2D space.
272+
/// </summary>
273+
/// <param name="rectangle">The source rectangle.</param>
274+
/// <param name="matrix">The transformation matrix.</param>
275+
/// <returns>A transformed rectangle.</returns>
276+
public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix)
277+
{
278+
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
279+
PointF topLeft = PointF.Transform(new PointF(rectangle.Location.X, rectangle.Location.Y), matrix);
280+
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
281+
}
282+
269283
/// <summary>
270284
/// Converts a <see cref="RectangleF"/> to a <see cref="Rectangle"/> by performing a truncate operation on all the coordinates.
271285
/// </summary>

src/ImageSharp/Primitives/RectangleF.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,20 @@ public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix)
241241
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
242242
}
243243

244+
/// <summary>
245+
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
246+
/// flattened into 2D space.
247+
/// </summary>
248+
/// <param name="rectangle">The source rectangle.</param>
249+
/// <param name="matrix">The transformation matrix.</param>
250+
/// <returns>A transformed <see cref="RectangleF"/>.</returns>
251+
public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix)
252+
{
253+
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
254+
PointF topLeft = PointF.Transform(rectangle.Location, matrix);
255+
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
256+
}
257+
244258
/// <summary>
245259
/// Creates a rectangle that represents the union between <paramref name="a"/> and <paramref name="b"/>.
246260
/// </summary>

src/ImageSharp/Processing/AffineTransformBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,16 @@ public SizeF GetTransformedSize(Rectangle sourceRectangle)
349349
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
350350
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
351351

352+
/// <summary>
353+
/// Clears all accumulated transform matrices, resetting the builder to its initial state.
354+
/// </summary>
355+
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
356+
public AffineTransformBuilder Clear()
357+
{
358+
this.transformMatrixFactories.Clear();
359+
return this;
360+
}
361+
352362
private static void CheckDegenerate(Matrix3x2 matrix)
353363
{
354364
if (TransformUtilities.IsDegenerate(matrix))

src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,17 @@ public static bool IsNaN(Matrix4x4 matrix)
6969
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7070
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix)
7171
{
72-
// The w component (v4.W) resulting from the transformation can be less than 0 in certain cases,
73-
// such as when the point is transformed behind the camera in a perspective projection.
74-
// However, in many 2D contexts, negative w values are not meaningful and could cause issues
75-
// like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure
76-
// we don't divide by a very small or negative number, effectively treating any negative w as epsilon.
72+
// Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and
73+
// performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space.
74+
//
75+
// For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide
76+
// is a no-op, producing the same result as Vector2.Transform(v, Matrix4x4).AsVector2()
77+
// (the approach used by .NET 10+).
78+
//
79+
// For projective matrices (taper, quad distortion) W varies per point and the divide
80+
// is essential for correct perspective mapping. W <= 0 means the point has crossed the
81+
// vanishing line of the projection; clamping to epsilon avoids division by zero or
82+
// negative values that would flip/mirror the output.
7783
const float epsilon = 0.0000001F;
7884
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
7985
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon);

src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,16 @@ public SizeF GetTransformedSize(Rectangle sourceRectangle)
397397
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
398398
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
399399

400+
/// <summary>
401+
/// Clears all accumulated transform matrices, resetting the builder to its initial state.
402+
/// </summary>
403+
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
404+
public ProjectiveTransformBuilder Clear()
405+
{
406+
this.transformMatrixFactories.Clear();
407+
return this;
408+
}
409+
400410
private static void CheckDegenerate(Matrix4x4 matrix)
401411
{
402412
if (TransformUtilities.IsDegenerate(matrix))

tests/ImageSharp.Tests/Primitives/PointFTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,69 @@ public void SkewTest()
133133
Assert.Equal(new PointF(30, 30), pout);
134134
}
135135

136+
[Fact]
137+
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
138+
{
139+
PointF p = new(13, 17);
140+
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty);
141+
Matrix4x4 m4 = new(m3);
142+
143+
PointF r3 = PointF.Transform(p, m3);
144+
PointF r4 = PointF.Transform(p, m4);
145+
146+
Assert.Equal(r3.X, r4.X, ApproximateFloatComparer);
147+
Assert.Equal(r3.Y, r4.Y, ApproximateFloatComparer);
148+
}
149+
150+
[Fact]
151+
public void TransformMatrix4x4_Identity()
152+
{
153+
PointF p = new(42.5F, -17.3F);
154+
PointF result = PointF.Transform(p, Matrix4x4.Identity);
155+
156+
Assert.Equal(p.X, result.X, ApproximateFloatComparer);
157+
Assert.Equal(p.Y, result.Y, ApproximateFloatComparer);
158+
}
159+
160+
[Fact]
161+
public void TransformMatrix4x4_Translation()
162+
{
163+
PointF p = new(10, 20);
164+
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
165+
PointF result = PointF.Transform(p, m);
166+
167+
Assert.Equal(15F, result.X, ApproximateFloatComparer);
168+
Assert.Equal(17F, result.Y, ApproximateFloatComparer);
169+
}
170+
171+
[Fact]
172+
public void TransformMatrix4x4_Scale()
173+
{
174+
PointF p = new(10, 20);
175+
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
176+
PointF result = PointF.Transform(p, m);
177+
178+
Assert.Equal(20F, result.X, ApproximateFloatComparer);
179+
Assert.Equal(60F, result.Y, ApproximateFloatComparer);
180+
}
181+
182+
[Fact]
183+
public void TransformMatrix4x4_Projective()
184+
{
185+
// A taper matrix with M14 != 0 produces W != 1, requiring perspective divide.
186+
PointF p = new(100, 50);
187+
Matrix4x4 m = Matrix4x4.Identity;
188+
m.M14 = 0.005F; // perspective component
189+
190+
PointF result = PointF.Transform(p, m);
191+
192+
// W = x*M14 + M44 = 100*0.005 + 1 = 1.5
193+
// X = x*M11 + M41 = 100, Y = y*M22 + M42 = 50
194+
// result = (100/1.5, 50/1.5)
195+
Assert.Equal(100F / 1.5F, result.X, ApproximateFloatComparer);
196+
Assert.Equal(50F / 1.5F, result.Y, ApproximateFloatComparer);
197+
}
198+
136199
[Theory]
137200
[InlineData(float.MaxValue, float.MinValue)]
138201
[InlineData(float.MinValue, float.MaxValue)]

tests/ImageSharp.Tests/Primitives/PointTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,51 @@ public void SkewTest()
174174
Assert.Equal(new Point(30, 30), pout);
175175
}
176176

177+
[Fact]
178+
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
179+
{
180+
Point p = new(13, 17);
181+
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
182+
Matrix4x4 m4 = new(m3);
183+
184+
Point r3 = Point.Transform(p, m3);
185+
Point r4 = Point.Transform(p, m4);
186+
187+
Assert.Equal(r3, r4);
188+
}
189+
190+
[Fact]
191+
public void TransformMatrix4x4_Identity()
192+
{
193+
Point p = new(42, -17);
194+
Point result = Point.Transform(p, Matrix4x4.Identity);
195+
196+
Assert.Equal(p, result);
197+
}
198+
199+
[Fact]
200+
public void TransformMatrix4x4_Translation()
201+
{
202+
Point p = new(10, 20);
203+
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
204+
Point result = Point.Transform(p, m);
205+
206+
Assert.Equal(new Point(15, 17), result);
207+
}
208+
209+
[Fact]
210+
public void TransformMatrix4x4_Projective()
211+
{
212+
Point p = new(100, 50);
213+
Matrix4x4 m = Matrix4x4.Identity;
214+
m.M14 = 0.005F;
215+
216+
Point result = Point.Transform(p, m);
217+
218+
// W = 100*0.005 + 1 = 1.5 => (100/1.5, 50/1.5) => rounded
219+
Assert.Equal(Point.Round(new PointF(100F / 1.5F, 50F / 1.5F)), result);
220+
}
221+
177222
[Theory]
178223
[InlineData(int.MaxValue, int.MinValue)]
179224
[InlineData(int.MinValue, int.MinValue)]

0 commit comments

Comments
 (0)