Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@ SixLabors.ImageSharp
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![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)

</div>

### **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.

Expand Down
12 changes: 12 additions & 0 deletions src/ImageSharp/Primitives/Point.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;

namespace SixLabors.ImageSharp;

Expand Down Expand Up @@ -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));

/// <summary>
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="point">The point to transform.</param>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="Point"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Transform(Point point, Matrix4x4 matrix)
=> Round(TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix));

/// <summary>
/// Deconstructs this point into two integers.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/ImageSharp/Primitives/PointF.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;

namespace SixLabors.ImageSharp;

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

/// <summary>
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="point">The point to transform.</param>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="PointF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix4x4 matrix)
=> TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
Comment on lines +258 to +259
Copy link
Member

@antonfirsov antonfirsov Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an established pattern so nothing we can do probably, but I keep wondering if these methods are expected to be used on hot path anywhere since this translates to 2 copies of Matrix4x4 which is relatively large.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's getting inlined as far as I can see

; Assembly listing for method SixLabors.ImageSharp.PointF:Transform(SixLabors.ImageSharp.PointF,System.Numerics.Matrix3x2):SixLabors.ImageSharp.PointF (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX512 - Windows
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 10 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
       push     rax
       vzeroupper 
       mov      qword ptr [rsp+0x10], rcx
 
G_M000_IG02:                ;; offset=0x0009
       vmovss   xmm0, dword ptr [rsp+0x10]
       vmovss   xmm1, dword ptr [rsp+0x14]
       vinsertps xmm0, xmm0, xmm1, 28
       vmovsd   xmm1, qword ptr [rdx]
       vmovsd   xmm2, qword ptr [rdx+0x08]
       vmovsd   xmm3, qword ptr [rdx+0x10]
       vmovaps  xmm4, xmm0
       vbroadcastss xmm4, xmm4
       vmulps   xmm1, xmm1, xmm4
       vmovshdup xmm0, xmm0
       vbroadcastss xmm0, xmm0
       vmulps   xmm0, xmm2, xmm0
       vaddps   xmm0, xmm1, xmm0
       vaddps   xmm0, xmm0, xmm3
       xor      eax, eax
       mov      qword ptr [rsp], rax
       vmovsd   qword ptr [rsp], xmm0
       mov      rax, qword ptr [rsp]
 
G_M000_IG03:                ;; offset=0x005A
       add      rsp, 8
       ret      
 
; Total bytes of code 95

; Assembly listing for method SixLabors.ImageSharp.PointF:Transform(SixLabors.ImageSharp.PointF,System.Numerics.Matrix4x4):SixLabors.ImageSharp.PointF (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX512 - Windows
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 10 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
       sub      rsp, 40
       vzeroupper 
       mov      qword ptr [rsp+0x30], rcx
 
G_M000_IG02:                ;; offset=0x000C
       vmovss   xmm0, dword ptr [rsp+0x30]
       vinsertps xmm0, xmm0, dword ptr [rsp+0x34], 20
       vinsertps xmm0, xmm0, dword ptr [reloc @RWD00], 48
       vmovups  xmm1, xmmword ptr [rdx]
       vmovups  xmm2, xmmword ptr [rdx+0x10]
       vmovups  xmm3, xmmword ptr [rdx+0x20]
       vmovups  xmm4, xmmword ptr [rdx+0x30]
       vmovaps  xmm5, xmm0
       vbroadcastss xmm5, xmm5
       vmulps   xmm1, xmm1, xmm5
       vmovshdup xmm5, xmm0
       vbroadcastss xmm5, xmm5
       vmulps   xmm2, xmm2, xmm5
       vaddps   xmm1, xmm1, xmm2
       vunpckhps xmm2, xmm0, xmm0
       vbroadcastss xmm2, xmm2
       vmulps   xmm2, xmm3, xmm2
       vaddps   xmm1, xmm1, xmm2
       vshufps  xmm0, xmm0, xmm0, -1
       vbroadcastss xmm0, xmm0
       vmulps   xmm0, xmm4, xmm0
       vaddps   xmm0, xmm1, xmm0
       vmovaps  xmmword ptr [rsp+0x10], xmm0
       vmovsd   xmm0, qword ptr [rsp+0x10]
       vmovups  xmm1, xmmword ptr [reloc @RWD16]
       vmaxss   xmm1, xmm1, dword ptr [rsp+0x1C]
       vbroadcastss xmm1, xmm1
       vdivps   xmm0, xmm0, xmm1
       xor      eax, eax
       mov      qword ptr [rsp+0x08], rax
       vmovsd   qword ptr [rsp+0x08], xmm0
       mov      rax, qword ptr [rsp+0x08]
 
G_M000_IG03:                ;; offset=0x00AD
       add      rsp, 40
       ret      
 
RWD00  	dq	3F8000003F800000h, 3F8000003F800000h
RWD16  	dq	0000000033D6BF95h, 0000000000000000h

; Total bytes of code 178

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's even inlined here. I think we're good.

/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed rectangle.</returns>
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));
}
; Assembly listing for method SixLabors.ImageSharp.Rectangle:Transform(SixLabors.ImageSharp.Rectangle,System.Numerics.Matrix3x2):SixLabors.ImageSharp.RectangleF (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX512 - Windows
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 70 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
       vzeroupper 
 
G_M000_IG02:                ;; offset=0x0003
       mov      eax, dword ptr [rdx]
       mov      r10d, dword ptr [rdx+0x04]
       mov      r9d, dword ptr [rdx+0x08]
       mov      edx, dword ptr [rdx+0x0C]
       vmovsd   xmm0, qword ptr [r8]
       vmovsd   xmm1, qword ptr [r8+0x08]
       vmovsd   xmm2, qword ptr [r8+0x10]
       add      r9d, eax
       add      edx, r10d
       vxorps   xmm3, xmm3, xmm3
       vcvtsi2ss xmm3, xmm3, r9d
       vxorps   xmm4, xmm4, xmm4
       vcvtsi2ss xmm4, xmm4, edx
       vinsertps xmm3, xmm3, xmm4, 28
       vmovaps  xmm4, xmm3
       vbroadcastss xmm4, xmm4
       vmulps   xmm4, xmm0, xmm4
       vmovshdup xmm3, xmm3
       vbroadcastss xmm3, xmm3
       vmulps   xmm3, xmm1, xmm3
       vaddps   xmm3, xmm4, xmm3
       vaddps   xmm3, xmm3, xmm2
       vmovaps  xmm4, xmm3
       vroundss xmm4, xmm4, xmm4, 4
       vcvttss2si  edx, xmm4
       vmovshdup xmm3, xmm3
       vroundss xmm3, xmm3, xmm3, 4
       vcvttss2si  r8d, xmm3
       vxorps   xmm3, xmm3, xmm3
       vcvtsi2ss xmm3, xmm3, edx
       vxorps   xmm4, xmm4, xmm4
       vcvtsi2ss xmm4, xmm4, r8d
       vxorps   xmm5, xmm5, xmm5
       vcvtsi2ss xmm5, xmm5, eax
       vxorps   xmm16, xmm16, xmm16
       vcvtsi2ss xmm16, xmm16, r10d
       vinsertps xmm5, xmm5, xmm16, 28
       vmovaps  xmm16, xmm5
       vbroadcastss xmm16, xmm16
       vmulps   xmm0, xmm0, xmm16
       vmovshdup xmm5, xmm5
       vbroadcastss xmm5, xmm5
       vmulps   xmm1, xmm1, xmm5
       vaddps   xmm0, xmm0, xmm1
       vaddps   xmm0, xmm0, xmm2
       vmovaps  xmm1, xmm0
       vroundss xmm1, xmm1, xmm1, 4
       vcvttss2si  eax, xmm1
       vmovshdup xmm0, xmm0
       vroundss xmm0, xmm0, xmm0, 4
       vcvttss2si  edx, xmm0
       vxorps   xmm0, xmm0, xmm0
       vcvtsi2ss xmm0, xmm0, eax
       vxorps   xmm1, xmm1, xmm1
       vcvtsi2ss xmm1, xmm1, edx
       vsubss   xmm2, xmm3, xmm0
       vsubss   xmm3, xmm4, xmm1
       vmovss   dword ptr [rcx], xmm0
       vmovss   dword ptr [rcx+0x04], xmm1
       vmovss   dword ptr [rcx+0x08], xmm2
       vmovss   dword ptr [rcx+0x0C], xmm3
       mov      rax, rcx
 
G_M000_IG03:                ;; offset=0x0119
       ret      
 
; Total bytes of code 282

; Assembly listing for method SixLabors.ImageSharp.Rectangle:Transform(SixLabors.ImageSharp.Rectangle,System.Numerics.Matrix4x4):SixLabors.ImageSharp.RectangleF (FullOpts)
; Emitting BLENDED_CODE for X64 with AVX512 - Windows
; FullOpts code
; optimized code
; rsp based frame
; partially interruptible
; No PGO data
; 0 inlinees with PGO data; 71 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                ;; offset=0x0000
       sub      rsp, 72
       vzeroupper 
 
G_M000_IG02:                ;; offset=0x0007
       mov      eax, dword ptr [rdx]
       mov      r10d, dword ptr [rdx+0x04]
       mov      r9d, dword ptr [rdx+0x08]
       mov      edx, dword ptr [rdx+0x0C]
       vmovups  xmm0, xmmword ptr [r8]
       vmovups  xmm1, xmmword ptr [r8+0x10]
       vmovups  xmm2, xmmword ptr [r8+0x20]
       vmovups  xmm3, xmmword ptr [r8+0x30]
       add      r9d, eax
       vxorps   xmm4, xmm4, xmm4
       vcvtsi2ss xmm4, xmm4, r9d
       add      edx, r10d
       vxorps   xmm5, xmm5, xmm5
       vcvtsi2ss xmm5, xmm5, edx
       vinsertps xmm4, xmm4, xmm5, 20
       vmovss   xmm5, dword ptr [reloc @RWD00]
       vinsertps xmm4, xmm4, xmm5, 48
       vmovaps  xmm16, xmm4
       vbroadcastss xmm16, xmm16
       vmulps   xmm16, xmm0, xmm16
       vmovshdup xmm17, xmm4
       vbroadcastss xmm17, xmm17
       vmulps   xmm17, xmm1, xmm17
       vaddps   xmm16, xmm16, xmm17
       vunpckhps xmm17, xmm4, xmm4
       vbroadcastss xmm17, xmm17
       vmulps   xmm17, xmm2, xmm17
       vaddps   xmm16, xmm16, xmm17
       vshufps  xmm4, xmm4, xmm4, -1
       vbroadcastss xmm4, xmm4
       vmulps   xmm4, xmm3, xmm4
       vaddps   xmm4, xmm16, xmm4
       vmovaps  xmmword ptr [rsp+0x30], xmm4
       vmovups  xmm4, xmmword ptr [reloc @RWD16]
       vmaxss   xmm16, xmm4, dword ptr [rsp+0x3C]
       vbroadcastss xmm16, xmm16
       vmovsd   xmm17, qword ptr [rsp+0x30]
       vdivps   xmm16, xmm17, xmm16
       xor      edx, edx
       mov      qword ptr [rsp+0x28], rdx
       vmovsd   qword ptr [rsp+0x28], xmm16
       vmovss   xmm16, dword ptr [rsp+0x28]
       vmovss   xmm17, dword ptr [rsp+0x2C]
       vxorps   xmm18, xmm18, xmm18
       vcvtsi2ss xmm18, xmm18, eax
       vxorps   xmm19, xmm19, xmm19
       vcvtsi2ss xmm19, xmm19, r10d
       vinsertps xmm18, xmm18, xmm19, 20
       vinsertps xmm5, xmm18, xmm5, 48
       vmovaps  xmm18, xmm5
       vbroadcastss xmm18, xmm18
       vmulps   xmm0, xmm0, xmm18
       vmovshdup xmm18, xmm5
       vbroadcastss xmm18, xmm18
       vmulps   xmm1, xmm1, xmm18
       vaddps   xmm0, xmm0, xmm1
       vunpckhps xmm1, xmm5, xmm5
       vbroadcastss xmm1, xmm1
       vmulps   xmm1, xmm2, xmm1
       vaddps   xmm0, xmm0, xmm1
       vshufps  xmm1, xmm5, xmm5, -1
       vbroadcastss xmm1, xmm1
       vmulps   xmm1, xmm3, xmm1
       vaddps   xmm0, xmm0, xmm1
 
G_M000_IG03:                ;; offset=0x0166
       vmovaps  xmmword ptr [rsp+0x10], xmm0
       vmaxss   xmm0, xmm4, dword ptr [rsp+0x1C]
       vbroadcastss xmm0, xmm0
       vmovsd   xmm1, qword ptr [rsp+0x10]
       vdivps   xmm0, xmm1, xmm0
 
G_M000_IG04:                ;; offset=0x0181
       mov      qword ptr [rsp+0x08], rdx
       vmovsd   qword ptr [rsp+0x08], xmm0
       vmovss   xmm0, dword ptr [rsp+0x08]
       vmovss   xmm1, dword ptr [rsp+0x0C]
       vsubss   xmm2, xmm16, xmm0
       vsubss   xmm3, xmm17, xmm1
       vmovss   dword ptr [rcx], xmm0
       vmovss   dword ptr [rcx+0x04], xmm1
       vmovss   dword ptr [rcx+0x08], xmm2
       vmovss   dword ptr [rcx+0x0C], xmm3
       mov      rax, rcx
 
G_M000_IG05:                ;; offset=0x01BA
       add      rsp, 72
       ret      
 
RWD00  	dd	3F800000h		;         1
RWD04  	dd	00000000h, 00000000h, 00000000h
RWD16  	dq	0000000033D6BF95h, 0000000000000000h

; Total bytes of code 447


/// <summary>
/// Deconstructs this point into two floats.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/ImageSharp/Primitives/Rectangle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,20 @@ public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix)
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}

/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed rectangle.</returns>
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));
}

/// <summary>
/// Converts a <see cref="RectangleF"/> to a <see cref="Rectangle"/> by performing a truncate operation on all the coordinates.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/ImageSharp/Primitives/RectangleF.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,20 @@ public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix)
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}

/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed <see cref="RectangleF"/>.</returns>
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));
}

/// <summary>
/// Creates a rectangle that represents the union between <paramref name="a"/> and <paramref name="b"/>.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/ImageSharp/Processing/AffineTransformBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ public SizeF GetTransformedSize(Rectangle sourceRectangle)
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);

/// <summary>
/// Clears all accumulated transform matrices, resetting the builder to its initial state.
/// </summary>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder Clear()
{
this.transformMatrixFactories.Clear();
return this;
}

private static void CheckDegenerate(Matrix3x2 matrix)
{
if (TransformUtilities.IsDegenerate(matrix))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,16 @@ public SizeF GetTransformedSize(Rectangle sourceRectangle)
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);

/// <summary>
/// Clears all accumulated transform matrices, resetting the builder to its initial state.
/// </summary>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder Clear()
{
this.transformMatrixFactories.Clear();
return this;
}

private static void CheckDegenerate(Matrix4x4 matrix)
{
if (TransformUtilities.IsDegenerate(matrix))
Expand Down
63 changes: 63 additions & 0 deletions tests/ImageSharp.Tests/Primitives/PointFTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
45 changes: 45 additions & 0 deletions tests/ImageSharp.Tests/Primitives/PointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
43 changes: 43 additions & 0 deletions tests/ImageSharp.Tests/Primitives/RectangleFTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

using System.Globalization;
using System.Numerics;

namespace SixLabors.ImageSharp.Tests;

Expand Down Expand Up @@ -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()
{
Expand Down
33 changes: 33 additions & 0 deletions tests/ImageSharp.Tests/Primitives/RectangleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

using System.Globalization;
using System.Numerics;

namespace SixLabors.ImageSharp.Tests;

Expand Down Expand Up @@ -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()
{
Expand Down
Loading
Loading