diff --git a/src/SixLabors.Core/Constants.cs b/src/SixLabors.Core/Constants.cs
new file mode 100644
index 0000000000..b8699e2d15
--- /dev/null
+++ b/src/SixLabors.Core/Constants.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors
+{
+ ///
+ /// Common constants used throughout the project.
+ ///
+ internal static class Constants
+ {
+ ///
+ /// The epsilon for comparing floating point numbers.
+ ///
+ public static readonly float Epsilon = 0.001f;
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/GeometryUtilities.cs b/src/SixLabors.Core/GeometryUtilities.cs
new file mode 100644
index 0000000000..43c46f1812
--- /dev/null
+++ b/src/SixLabors.Core/GeometryUtilities.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors
+{
+ ///
+ /// Utility class for common geometric functions.
+ ///
+ public static class GeometryUtilities
+ {
+ ///
+ /// Converts a degree (360-periodic) angle to a radian (2*Pi-periodic) angle.
+ ///
+ /// The angle in degrees.
+ ///
+ /// The representing the degree as radians.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float DegreeToRadian(float degree) => degree * (MathF.PI / 180F);
+
+ ///
+ /// Converts a radian (2*Pi-periodic) angle to a degree (360-periodic) angle.
+ ///
+ /// The angle in radians.
+ ///
+ /// The representing the degree as radians.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float RadianToDegree(float radian) => radian / (MathF.PI / 180F);
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/AllocationOptions.cs b/src/SixLabors.Core/Memory/AllocationOptions.cs
new file mode 100644
index 0000000000..5eda00505e
--- /dev/null
+++ b/src/SixLabors.Core/Memory/AllocationOptions.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Options for allocating buffers.
+ ///
+ public enum AllocationOptions
+ {
+ ///
+ /// Indicates that the buffer should just be allocated.
+ ///
+ None,
+
+ ///
+ /// Indicates that the allocated buffer should be cleaned following allocation.
+ ///
+ Clean
+ }
+}
diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs
new file mode 100644
index 0000000000..5676ab23fc
--- /dev/null
+++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.Buffer{T}.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using SixLabors.Memory.Internals;
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Contains and .
+ ///
+ public partial class ArrayPoolMemoryAllocator
+ {
+ ///
+ /// The buffer implementation of .
+ ///
+ private class Buffer : ManagedBufferBase
+ where T : struct
+ {
+ ///
+ /// The length of the buffer.
+ ///
+ private readonly int length;
+
+ ///
+ /// A weak reference to the source pool.
+ ///
+ ///
+ /// By using a weak reference here, we are making sure that array pools and their retained arrays are always GC-ed
+ /// after a call to , regardless of having buffer instances still being in use.
+ ///
+ private WeakReference> sourcePoolReference;
+
+ public Buffer(byte[] data, int length, ArrayPool sourcePool)
+ {
+ this.Data = data;
+ this.length = length;
+ this.sourcePoolReference = new WeakReference>(sourcePool);
+ }
+
+ ///
+ /// Gets the buffer as a byte array.
+ ///
+ protected byte[] Data { get; private set; }
+
+ ///
+ public override Span GetSpan() => MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (!disposing || this.Data is null || this.sourcePoolReference is null)
+ {
+ return;
+ }
+
+ if (this.sourcePoolReference.TryGetTarget(out ArrayPool pool))
+ {
+ pool.Return(this.Data);
+ }
+
+ this.sourcePoolReference = null;
+ this.Data = null;
+ }
+
+ protected override object GetPinnableObject() => this.Data;
+ }
+
+ ///
+ /// The implementation of .
+ ///
+ private sealed class ManagedByteBuffer : Buffer, IManagedByteBuffer
+ {
+ public ManagedByteBuffer(byte[] data, int length, ArrayPool sourcePool)
+ : base(data, length, sourcePool)
+ {
+ }
+
+ ///
+ public byte[] Array => this.Data;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs
new file mode 100644
index 0000000000..dd6e9a8f0b
--- /dev/null
+++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Contains common factory methods and configuration constants.
+ ///
+ public partial class ArrayPoolMemoryAllocator
+ {
+ ///
+ /// The default value for: maximum size of pooled arrays in bytes.
+ /// Currently set to 24MB, which is equivalent to 8 megapixels of raw RGBA32 data.
+ ///
+ internal const int DefaultMaxPooledBufferSizeInBytes = 24 * 1024 * 1024;
+
+ ///
+ /// The value for: The threshold to pool arrays in which has less buckets for memory safety.
+ ///
+ private const int DefaultBufferSelectorThresholdInBytes = 8 * 1024 * 1024;
+
+ ///
+ /// The default bucket count for .
+ ///
+ private const int DefaultLargePoolBucketCount = 6;
+
+ ///
+ /// The default bucket count for .
+ ///
+ private const int DefaultNormalPoolBucketCount = 16;
+
+ ///
+ /// This is the default. Should be good for most use cases.
+ ///
+ /// The memory manager.
+ public static ArrayPoolMemoryAllocator CreateDefault()
+ {
+ return new ArrayPoolMemoryAllocator(
+ DefaultMaxPooledBufferSizeInBytes,
+ DefaultBufferSelectorThresholdInBytes,
+ DefaultLargePoolBucketCount,
+ DefaultNormalPoolBucketCount);
+ }
+
+ ///
+ /// For environments with very limited memory capabilities, only small buffers like image rows are pooled.
+ ///
+ /// The memory manager.
+ public static ArrayPoolMemoryAllocator CreateWithMinimalPooling()
+ {
+ return new ArrayPoolMemoryAllocator(64 * 1024, 32 * 1024, 8, 24);
+ }
+
+ ///
+ /// For environments with limited memory capabilities, only small array requests are pooled, which can result in reduced throughput.
+ ///
+ /// The memory manager.
+ public static ArrayPoolMemoryAllocator CreateWithModeratePooling()
+ {
+ return new ArrayPoolMemoryAllocator(1024 * 1024, 32 * 1024, 16, 24);
+ }
+
+ ///
+ /// For environments where memory capabilities are not an issue, the maximum amount of array requests are pooled which results in optimal throughput.
+ ///
+ /// The memory manager.
+ public static ArrayPoolMemoryAllocator CreateWithAggressivePooling()
+ {
+ return new ArrayPoolMemoryAllocator(128 * 1024 * 1024, 32 * 1024 * 1024, 16, 32);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs
new file mode 100644
index 0000000000..0905948e00
--- /dev/null
+++ b/src/SixLabors.Core/Memory/ArrayPoolMemoryAllocator.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Implements by allocating memory from .
+ ///
+ public sealed partial class ArrayPoolMemoryAllocator : MemoryAllocator
+ {
+ private readonly int maxArraysPerBucketNormalPool;
+
+ private readonly int maxArraysPerBucketLargePool;
+
+ ///
+ /// The for small-to-medium buffers which is not kept clean.
+ ///
+ private ArrayPool normalArrayPool;
+
+ ///
+ /// The for huge buffers, which is not kept clean.
+ ///
+ private ArrayPool largeArrayPool;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ArrayPoolMemoryAllocator()
+ : this(DefaultMaxPooledBufferSizeInBytes, DefaultBufferSelectorThresholdInBytes)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated.
+ public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes)
+ : this(maxPoolSizeInBytes, GetLargeBufferThresholdInBytes(maxPoolSizeInBytes))
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated.
+ /// Arrays over this threshold will be pooled in which has less buckets for memory safety.
+ public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes)
+ : this(maxPoolSizeInBytes, poolSelectorThresholdInBytes, DefaultLargePoolBucketCount, DefaultNormalPoolBucketCount)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated.
+ /// The threshold to pool arrays in which has less buckets for memory safety.
+ /// Max arrays per bucket for the large array pool.
+ /// Max arrays per bucket for the normal array pool.
+ public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes, int maxArraysPerBucketLargePool, int maxArraysPerBucketNormalPool)
+ {
+ Guard.MustBeGreaterThan(maxPoolSizeInBytes, 0, nameof(maxPoolSizeInBytes));
+ Guard.MustBeLessThanOrEqualTo(poolSelectorThresholdInBytes, maxPoolSizeInBytes, nameof(poolSelectorThresholdInBytes));
+
+ this.MaxPoolSizeInBytes = maxPoolSizeInBytes;
+ this.PoolSelectorThresholdInBytes = poolSelectorThresholdInBytes;
+ this.maxArraysPerBucketLargePool = maxArraysPerBucketLargePool;
+ this.maxArraysPerBucketNormalPool = maxArraysPerBucketNormalPool;
+
+ this.InitArrayPools();
+ }
+
+ ///
+ /// Gets the maximum size of pooled arrays in bytes.
+ ///
+ public int MaxPoolSizeInBytes { get; }
+
+ ///
+ /// Gets the threshold to pool arrays in which has less buckets for memory safety.
+ ///
+ public int PoolSelectorThresholdInBytes { get; }
+
+ ///
+ public override void ReleaseRetainedResources()
+ {
+ this.InitArrayPools();
+ }
+
+ ///
+ public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
+ int itemSizeBytes = Unsafe.SizeOf();
+ int bufferSizeInBytes = length * itemSizeBytes;
+ if (bufferSizeInBytes < 0)
+ {
+ throw new ArgumentOutOfRangeException(
+ nameof(length),
+ $"{nameof(ArrayPoolMemoryAllocator)} can not allocate {length} elements of {typeof(T).Name}.");
+ }
+
+ ArrayPool pool = this.GetArrayPool(bufferSizeInBytes);
+ byte[] byteArray = pool.Rent(bufferSizeInBytes);
+
+ var buffer = new Buffer(byteArray, length, pool);
+ if (options == AllocationOptions.Clean)
+ {
+ buffer.GetSpan().Clear();
+ }
+
+ return buffer;
+ }
+
+ ///
+ public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
+
+ ArrayPool pool = this.GetArrayPool(length);
+ byte[] byteArray = pool.Rent(length);
+
+ var buffer = new ManagedByteBuffer(byteArray, length, pool);
+ if (options == AllocationOptions.Clean)
+ {
+ buffer.GetSpan().Clear();
+ }
+
+ return buffer;
+ }
+
+ private static int GetLargeBufferThresholdInBytes(int maxPoolSizeInBytes)
+ {
+ return maxPoolSizeInBytes / 4;
+ }
+
+ private ArrayPool GetArrayPool(int bufferSizeInBytes)
+ {
+ return bufferSizeInBytes <= this.PoolSelectorThresholdInBytes ? this.normalArrayPool : this.largeArrayPool;
+ }
+
+ private void InitArrayPools()
+ {
+ this.largeArrayPool = ArrayPool.Create(this.MaxPoolSizeInBytes, this.maxArraysPerBucketLargePool);
+ this.normalArrayPool = ArrayPool.Create(this.PoolSelectorThresholdInBytes, this.maxArraysPerBucketNormalPool);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/IManagedByteBuffer.cs b/src/SixLabors.Core/Memory/IManagedByteBuffer.cs
new file mode 100644
index 0000000000..b6d956c102
--- /dev/null
+++ b/src/SixLabors.Core/Memory/IManagedByteBuffer.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Buffers;
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Represents a byte buffer backed by a managed array. Useful for interop with classic .NET API-s.
+ ///
+ public interface IManagedByteBuffer : IMemoryOwner
+ {
+ ///
+ /// Gets the managed array backing this buffer instance.
+ ///
+ byte[] Array { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/Internals/BasicArrayBuffer.cs b/src/SixLabors.Core/Memory/Internals/BasicArrayBuffer.cs
new file mode 100644
index 0000000000..c0d36afd55
--- /dev/null
+++ b/src/SixLabors.Core/Memory/Internals/BasicArrayBuffer.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Memory.Internals
+{
+ ///
+ /// Wraps an array as an instance.
+ ///
+ ///
+ internal class BasicArrayBuffer : ManagedBufferBase
+ where T : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The array.
+ /// The length of the buffer.
+ public BasicArrayBuffer(T[] array, int length)
+ {
+ DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length));
+ this.Array = array;
+ this.Length = length;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The array.
+ public BasicArrayBuffer(T[] array)
+ : this(array, array.Length)
+ {
+ }
+
+ ///
+ /// Gets the array.
+ ///
+ public T[] Array { get; }
+
+ ///
+ /// Gets the length.
+ ///
+ public int Length { get; }
+
+ ///
+ public override Span GetSpan() => this.Array.AsSpan(0, this.Length);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ }
+
+ ///
+ protected override object GetPinnableObject()
+ {
+ return this.Array;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/Internals/BasicByteBuffer.cs b/src/SixLabors.Core/Memory/Internals/BasicByteBuffer.cs
new file mode 100644
index 0000000000..fa6a5de4c7
--- /dev/null
+++ b/src/SixLabors.Core/Memory/Internals/BasicByteBuffer.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+namespace SixLabors.Memory.Internals
+{
+ ///
+ /// Provides an based on .
+ ///
+ internal sealed class BasicByteBuffer : BasicArrayBuffer, IManagedByteBuffer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The byte array.
+ internal BasicByteBuffer(byte[] array)
+ : base(array)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/Internals/ManagedBufferBase.cs b/src/SixLabors.Core/Memory/Internals/ManagedBufferBase.cs
new file mode 100644
index 0000000000..e1f131693c
--- /dev/null
+++ b/src/SixLabors.Core/Memory/Internals/ManagedBufferBase.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Buffers;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.Memory.Internals
+{
+ ///
+ /// Provides a base class for implementations by implementing pinning logic for adaption.
+ ///
+ /// The element type.
+ internal abstract class ManagedBufferBase : MemoryManager
+ where T : struct
+ {
+ private GCHandle pinHandle;
+
+ ///
+ public override unsafe MemoryHandle Pin(int elementIndex = 0)
+ {
+ if (!this.pinHandle.IsAllocated)
+ {
+ this.pinHandle = GCHandle.Alloc(this.GetPinnableObject(), GCHandleType.Pinned);
+ }
+
+ void* ptr = (void*)this.pinHandle.AddrOfPinnedObject();
+ return new MemoryHandle(ptr, this.pinHandle);
+ }
+
+ ///
+ public override void Unpin()
+ {
+ if (this.pinHandle.IsAllocated)
+ {
+ this.pinHandle.Free();
+ }
+ }
+
+ ///
+ /// Gets the object that should be pinned.
+ ///
+ /// The pinnable .
+ protected abstract object GetPinnableObject();
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Memory/MemoryAllocator.cs b/src/SixLabors.Core/Memory/MemoryAllocator.cs
new file mode 100644
index 0000000000..24ed7bef38
--- /dev/null
+++ b/src/SixLabors.Core/Memory/MemoryAllocator.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Buffers;
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Memory managers are used to allocate memory for image processing operations.
+ ///
+ public abstract class MemoryAllocator
+ {
+ ///
+ /// Allocates an , holding a of length .
+ ///
+ /// Type of the data stored in the buffer.
+ /// Size of the buffer to allocate.
+ /// The allocation options.
+ /// A buffer of values of type .
+ public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ where T : struct;
+
+ ///
+ /// Allocates an .
+ ///
+ /// The requested buffer length.
+ /// The allocation options.
+ /// The .
+ public abstract IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None);
+
+ ///
+ /// Releases all retained resources not being in use.
+ /// Eg: by resetting array pools and letting GC to free the arrays.
+ ///
+ public virtual void ReleaseRetainedResources()
+ {
+ }
+ }
+}
diff --git a/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs b/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs
new file mode 100644
index 0000000000..acf17ad63d
--- /dev/null
+++ b/src/SixLabors.Core/Memory/SimpleGcMemoryAllocator.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using SixLabors.Memory.Internals;
+
+namespace SixLabors.Memory
+{
+ ///
+ /// Implements by newing up arrays by the GC on every allocation requests.
+ ///
+ public sealed class SimpleGcMemoryAllocator : MemoryAllocator
+ {
+ ///
+ public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
+
+ return new BasicArrayBuffer(new T[length]);
+ }
+
+ ///
+ public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
+
+ return new BasicByteBuffer(new byte[length]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/Matrix3x2Extensions.cs b/src/SixLabors.Core/Primitives/Matrix3x2Extensions.cs
new file mode 100644
index 0000000000..2d33ea70d6
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/Matrix3x2Extensions.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.Numerics;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Extension methods for the struct.
+ ///
+ public static class Matrix3x2Extensions
+ {
+ ///
+ /// Creates a translation matrix from the given vector.
+ ///
+ /// The translation position.
+ /// A translation matrix.
+ public static Matrix3x2 CreateTranslation(PointF position) => Matrix3x2.CreateTranslation(position);
+
+ ///
+ /// Creates a scale matrix that is offset by a given center point.
+ ///
+ /// Value to scale by on the X-axis.
+ /// Value to scale by on the Y-axis.
+ /// The center point.
+ /// A scaling matrix.
+ public static Matrix3x2 CreateScale(float xScale, float yScale, PointF centerPoint) => Matrix3x2.CreateScale(xScale, yScale, centerPoint);
+
+ ///
+ /// Creates a scale matrix from the given vector scale.
+ ///
+ /// The scale to use.
+ /// A scaling matrix.
+ public static Matrix3x2 CreateScale(SizeF scales) => Matrix3x2.CreateScale(scales);
+
+ ///
+ /// Creates a scale matrix from the given vector scale with an offset from the given center point.
+ ///
+ /// The scale to use.
+ /// The center offset.
+ /// A scaling matrix.
+ public static Matrix3x2 CreateScale(SizeF scales, PointF centerPoint) => Matrix3x2.CreateScale(scales, centerPoint);
+
+ ///
+ /// Creates a scale matrix that scales uniformly with the given scale with an offset from the given center.
+ ///
+ /// The uniform scale to use.
+ /// The center offset.
+ /// A scaling matrix.
+ public static Matrix3x2 CreateScale(float scale, PointF centerPoint) => Matrix3x2.CreateScale(scale, centerPoint);
+
+ ///
+ /// Creates a skew matrix from the given angles in degrees.
+ ///
+ /// The X angle, in degrees.
+ /// The Y angle, in degrees.
+ /// A skew matrix.
+ public static Matrix3x2 CreateSkewDegrees(float degreesX, float degreesY) => Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY));
+
+ ///
+ /// Creates a skew matrix from the given angles in radians and a center point.
+ ///
+ /// The X angle, in radians.
+ /// The Y angle, in radians.
+ /// The center point.
+ /// A skew matrix.
+ public static Matrix3x2 CreateSkew(float radiansX, float radiansY, PointF centerPoint) => Matrix3x2.CreateSkew(radiansX, radiansY, centerPoint);
+
+ ///
+ /// Creates a skew matrix from the given angles in degrees and a center point.
+ ///
+ /// The X angle, in degrees.
+ /// The Y angle, in degrees.
+ /// The center point.
+ /// A skew matrix.
+ public static Matrix3x2 CreateSkewDegrees(float degreesX, float degreesY, PointF centerPoint) => Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(degreesX), GeometryUtilities.DegreeToRadian(degreesY), centerPoint);
+
+ ///
+ /// Creates a rotation matrix using the given rotation in degrees.
+ ///
+ /// The amount of rotation, in degrees.
+ /// A rotation matrix.
+ public static Matrix3x2 CreateRotationDegrees(float degrees) => Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees));
+
+ ///
+ /// Creates a rotation matrix using the given rotation in radians and a center point.
+ ///
+ /// The amount of rotation, in radians.
+ /// The center point.
+ /// A rotation matrix.
+ public static Matrix3x2 CreateRotation(float radians, PointF centerPoint) => Matrix3x2.CreateRotation(radians, centerPoint);
+
+ ///
+ /// Creates a rotation matrix using the given rotation in degrees and a center point.
+ ///
+ /// The amount of rotation, in degrees.
+ /// The center point.
+ /// A rotation matrix.
+ public static Matrix3x2 CreateRotationDegrees(float degrees, PointF centerPoint) => Matrix3x2.CreateRotation(GeometryUtilities.DegreeToRadian(degrees), centerPoint);
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/Point.cs b/src/SixLabors.Core/Primitives/Point.cs
new file mode 100644
index 0000000000..ec57330411
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/Point.cs
@@ -0,0 +1,288 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.ComponentModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Represents an ordered pair of integer x- and y-coordinates that defines a point in
+ /// a two-dimensional plane.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public struct Point : IEquatable
+ {
+ ///
+ /// Represents a that has X and Y values set to zero.
+ ///
+ public static readonly Point Empty = default;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The horizontal and vertical position of the point.
+ public Point(int value)
+ : this()
+ {
+ this.X = LowInt16(value);
+ this.Y = HighInt16(value);
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The horizontal position of the point.
+ /// The vertical position of the point.
+ public Point(int x, int y)
+ : this()
+ {
+ this.X = x;
+ this.Y = y;
+ }
+
+ ///
+ /// Initializes a new instance of the struct from the given .
+ ///
+ /// The size.
+ public Point(Size size)
+ {
+ this.X = size.Width;
+ this.Y = size.Height;
+ }
+
+ ///
+ /// Gets or sets the x-coordinate of this .
+ ///
+ public int X { get; set; }
+
+ ///
+ /// Gets or sets the y-coordinate of this .
+ ///
+ public int Y { get; set; }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => this.Equals(Empty);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator PointF(Point point) => new PointF(point.X, point.Y);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator Vector2(Point point) => new Vector2(point.X, point.Y);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static explicit operator Size(Point point) => new Size(point.X, point.Y);
+
+ ///
+ /// Negates the given point by multiplying all values by -1.
+ ///
+ /// The source point.
+ /// The negated point.
+ public static Point operator -(Point value) => new Point(-value.X, -value.Y);
+
+ ///
+ /// Translates a by a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point operator +(Point point, Size size) => Add(point, size);
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point operator -(Point point, Size size) => Subtract(point, size);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplier of type .
+ /// Multiplicand of type .
+ /// Product of type .
+ public static Point operator *(int left, Point right) => Multiply(right, left);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type .
+ public static Point operator *(Point left, int right) => Multiply(left, right);
+
+ ///
+ /// Divides by a producing .
+ ///
+ /// Dividend of type .
+ /// Divisor of type .
+ /// Result of type .
+ public static Point operator /(Point left, int right)
+ => new Point(left.X / right, left.Y / right);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(Point left, Point right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(Point left, Point right) => !left.Equals(right);
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Add(Point point, Size size) => new Point(unchecked(point.X + size.Width), unchecked(point.Y + size.Height));
+
+ ///
+ /// Translates a by the negative of a given value.
+ ///
+ /// The point on the left hand of the operand.
+ /// The value on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Multiply(Point point, int value) => new Point(unchecked(point.X * value), unchecked(point.Y * value));
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Subtract(Point point, Size size) => new Point(unchecked(point.X - size.Width), unchecked(point.Y - size.Height));
+
+ ///
+ /// Converts a to a by performing a ceiling operation on all the coordinates.
+ ///
+ /// The point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Ceiling(PointF point) => new Point(unchecked((int)MathF.Ceiling(point.X)), unchecked((int)MathF.Ceiling(point.Y)));
+
+ ///
+ /// Converts a to a by performing a round operation on all the coordinates.
+ ///
+ /// The point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Round(PointF point) => new Point(unchecked((int)MathF.Round(point.X)), unchecked((int)MathF.Round(point.Y)));
+
+ ///
+ /// Converts a to a by performing a round operation on all the coordinates.
+ ///
+ /// The vector.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Round(Vector2 vector) => new Point(unchecked((int)MathF.Round(vector.X)), unchecked((int)MathF.Round(vector.Y)));
+
+ ///
+ /// Converts a to a by performing a truncate operation on all the coordinates.
+ ///
+ /// The point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Truncate(PointF point) => new Point(unchecked((int)point.X), unchecked((int)point.Y));
+
+ ///
+ /// Transforms a point by a specified 3x2 matrix.
+ ///
+ /// The point to transform.
+ /// The transformation matrix used.
+ /// The transformed .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix));
+
+ ///
+ /// Deconstructs this point into two integers.
+ ///
+ /// The out value for X.
+ /// The out value for Y.
+ public void Deconstruct(out int x, out int y)
+ {
+ x = this.X;
+ y = this.Y;
+ }
+
+ ///
+ /// Translates this by the specified amount.
+ ///
+ /// The amount to offset the x-coordinate.
+ /// The amount to offset the y-coordinate.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(int dx, int dy)
+ {
+ unchecked
+ {
+ this.X += dx;
+ this.Y += dy;
+ }
+ }
+
+ ///
+ /// Translates this by the specified amount.
+ ///
+ /// The used offset this .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(Point point) => this.Offset(point.X, point.Y);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
+
+ ///
+ public override string ToString() => $"Point [ X={this.X}, Y={this.Y} ]";
+
+ ///
+ public override bool Equals(object obj) => obj is Point other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public 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));
+
+ private static short LowInt16(int n) => unchecked((short)(n & 0xffff));
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/PointF.cs b/src/SixLabors.Core/Primitives/PointF.cs
new file mode 100644
index 0000000000..4a2da5cdcd
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/PointF.cs
@@ -0,0 +1,293 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.ComponentModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Represents an ordered pair of single precision floating point x- and y-coordinates that defines a point in
+ /// a two-dimensional plane.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public struct PointF : IEquatable
+ {
+ ///
+ /// Represents a that has X and Y values set to zero.
+ ///
+ public static readonly PointF Empty = default;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The horizontal position of the point.
+ /// The vertical position of the point.
+ public PointF(float x, float y)
+ : this()
+ {
+ this.X = x;
+ this.Y = y;
+ }
+
+ ///
+ /// Initializes a new instance of the struct from the given .
+ ///
+ /// The size.
+ public PointF(SizeF size)
+ {
+ this.X = size.Width;
+ this.Y = size.Height;
+ }
+
+ ///
+ /// Gets or sets the x-coordinate of this .
+ ///
+ public float X { get; set; }
+
+ ///
+ /// Gets or sets the y-coordinate of this .
+ ///
+ public float Y { get; set; }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => this.Equals(Empty);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The vector.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator PointF(Vector2 vector) => new PointF(vector.X, vector.Y);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The point.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator Vector2(PointF point) => new Vector2(point.X, point.Y);
+
+ ///
+ /// Creates a with the coordinates of the specified by truncating each of the coordinates.
+ ///
+ /// The point.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static explicit operator Point(PointF point) => Point.Truncate(point);
+
+ ///
+ /// Negates the given point by multiplying all values by -1.
+ ///
+ /// The source point.
+ /// The negated point.
+ public static PointF operator -(PointF value) => new PointF(-value.X, -value.Y);
+
+ ///
+ /// Translates a by a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF operator +(PointF point, SizeF size) => Add(point, size);
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF operator -(PointF point, PointF size) => Subtract(point, size);
+
+ ///
+ /// Translates a by a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF operator +(PointF point, PointF size) => Add(point, size);
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF operator -(PointF point, SizeF size) => Subtract(point, size);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplier of type .
+ /// Multiplicand of type .
+ /// Product of type .
+ public static PointF operator *(float left, PointF right) => Multiply(right, left);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type .
+ public static PointF operator *(PointF left, float right) => Multiply(left, right);
+
+ ///
+ /// Divides by a producing .
+ ///
+ /// Dividend of type .
+ /// Divisor of type .
+ /// Result of type .
+ public static PointF operator /(PointF left, float right)
+ => new PointF(left.X / right, left.Y / right);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(PointF left, PointF right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(PointF left, PointF right) => !left.Equals(right);
+
+ ///
+ /// Translates a by the given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Add(PointF point, SizeF size) => new PointF(point.X + size.Width, point.Y + size.Height);
+
+ ///
+ /// Translates a by the given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The point on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Add(PointF point, PointF pointb) => new PointF(point.X + pointb.X, point.Y + pointb.Y);
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Subtract(PointF point, SizeF size) => new PointF(point.X - size.Width, point.Y - size.Height);
+
+ ///
+ /// Translates a by the negative of a given .
+ ///
+ /// The point on the left hand of the operand.
+ /// The point on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Subtract(PointF point, PointF pointb) => new PointF(point.X - pointb.X, point.Y - pointb.Y);
+
+ ///
+ /// Translates a by the multiplying the X and Y by the given value.
+ ///
+ /// The point on the left hand of the operand.
+ /// The value on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Multiply(PointF point, float right) => new PointF(point.X * right, point.Y * right);
+
+ ///
+ /// Transforms a point by a specified 3x2 matrix.
+ ///
+ /// The point to transform.
+ /// The transformation matrix used.
+ /// The transformed .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix);
+
+ ///
+ /// Deconstructs this point into two floats.
+ ///
+ /// The out value for X.
+ /// The out value for Y.
+ public void Deconstruct(out float x, out float y)
+ {
+ x = this.X;
+ y = this.Y;
+ }
+
+ ///
+ /// Translates this by the specified amount.
+ ///
+ /// The amount to offset the x-coordinate.
+ /// The amount to offset the y-coordinate.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(float dx, float dy)
+ {
+ this.X += dx;
+ this.Y += dy;
+ }
+
+ ///
+ /// Translates this by the specified amount.
+ ///
+ /// The used offset this .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(PointF point) => this.Offset(point.X, point.Y);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
+
+ ///
+ public override string ToString() => $"PointF [ X={this.X}, Y={this.Y} ]";
+
+ ///
+ public override bool Equals(object obj) => obj is PointF && this.Equals((PointF)obj);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(PointF other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/Rectangle.cs b/src/SixLabors.Core/Primitives/Rectangle.cs
new file mode 100644
index 0000000000..8600e2e4c8
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/Rectangle.cs
@@ -0,0 +1,463 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.ComponentModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Stores a set of four integers that represent the location and size of a rectangle.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public struct Rectangle : IEquatable
+ {
+ ///
+ /// Represents a that has X, Y, Width, and Height values set to zero.
+ ///
+ public static readonly Rectangle Empty = default;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The horizontal position of the rectangle.
+ /// The vertical position of the rectangle.
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public Rectangle(int x, int y, int width, int height)
+ {
+ this.X = x;
+ this.Y = y;
+ this.Width = width;
+ this.Height = height;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ ///
+ /// The which specifies the rectangles point in a two-dimensional plane.
+ ///
+ ///
+ /// The which specifies the rectangles height and width.
+ ///
+ public Rectangle(Point point, Size size)
+ {
+ this.X = point.X;
+ this.Y = point.Y;
+ this.Width = size.Width;
+ this.Height = size.Height;
+ }
+
+ ///
+ /// Gets or sets the x-coordinate of this .
+ ///
+ public int X { get; set; }
+
+ ///
+ /// Gets or sets the y-coordinate of this .
+ ///
+ public int Y { get; set; }
+
+ ///
+ /// Gets or sets the width of this .
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets the height of this .
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this .
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Point Location
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new Point(this.X, this.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set
+ {
+ this.X = value.X;
+ this.Y = value.Y;
+ }
+ }
+
+ ///
+ /// Gets or sets the size of this .
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public Size Size
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new Size(this.Width, this.Height);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set
+ {
+ this.Width = value.Width;
+ this.Height = value.Height;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => this.Equals(Empty);
+
+ ///
+ /// Gets the y-coordinate of the top edge of this .
+ ///
+ public int Top => this.Y;
+
+ ///
+ /// Gets the x-coordinate of the right edge of this .
+ ///
+ public int Right
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => unchecked(this.X + this.Width);
+ }
+
+ ///
+ /// Gets the y-coordinate of the bottom edge of this .
+ ///
+ public int Bottom
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => unchecked(this.Y + this.Height);
+ }
+
+ ///
+ /// Gets the x-coordinate of the left edge of this .
+ ///
+ public int Left => this.X;
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The rectangle.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator RectangleF(Rectangle rectangle) => new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The rectangle.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator Vector4(Rectangle rectangle) => new Vector4(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(Rectangle left, Rectangle right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(Rectangle left, Rectangle right) => !left.Equals(right);
+
+ ///
+ /// Creates a new with the specified location and size.
+ /// The left coordinate of the rectangle.
+ /// The top coordinate of the rectangle.
+ /// The right coordinate of the rectangle.
+ /// The bottom coordinate of the rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+
+ // ReSharper disable once InconsistentNaming
+ public static Rectangle FromLTRB(int left, int top, int right, int bottom) => new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top));
+
+ ///
+ /// Returns the center point of the given .
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Point Center(Rectangle rectangle) => new Point(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
+
+ ///
+ /// Creates a rectangle that represents the intersection between and
+ /// . If there is no intersection, an empty rectangle is returned.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rectangle Intersect(Rectangle a, Rectangle b)
+ {
+ int x1 = Math.Max(a.X, b.X);
+ int x2 = Math.Min(a.Right, b.Right);
+ int y1 = Math.Max(a.Y, b.Y);
+ int y2 = Math.Min(a.Bottom, b.Bottom);
+
+ if (x2 >= x1 && y2 >= y1)
+ {
+ return new Rectangle(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ return Empty;
+ }
+
+ ///
+ /// Creates a that is inflated by the specified amount.
+ ///
+ /// The rectangle.
+ /// The amount to inflate the width by.
+ /// The amount to inflate the height by.
+ /// A new .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rectangle Inflate(Rectangle rectangle, int x, int y)
+ {
+ Rectangle r = rectangle;
+ r.Inflate(x, y);
+ return r;
+ }
+
+ ///
+ /// Converts a to a by performing a ceiling operation on all the coordinates.
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rectangle Ceiling(RectangleF rectangle)
+ {
+ unchecked
+ {
+ return new Rectangle(
+ (int)MathF.Ceiling(rectangle.X),
+ (int)MathF.Ceiling(rectangle.Y),
+ (int)MathF.Ceiling(rectangle.Width),
+ (int)MathF.Ceiling(rectangle.Height));
+ }
+ }
+
+ ///
+ /// Transforms a rectangle by the given matrix.
+ ///
+ /// The source rectangle.
+ /// The transformation matrix.
+ /// A transformed rectangle.
+ public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix)
+ {
+ PointF bottomRight = Point.Transform(new Point(rectangle.Right, rectangle.Bottom), matrix);
+ PointF topLeft = Point.Transform(rectangle.Location, matrix);
+ return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
+ }
+
+ ///
+ /// Converts a to a by performing a truncate operation on all the coordinates.
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rectangle Truncate(RectangleF rectangle)
+ {
+ unchecked
+ {
+ return new Rectangle(
+ (int)rectangle.X,
+ (int)rectangle.Y,
+ (int)rectangle.Width,
+ (int)rectangle.Height);
+ }
+ }
+
+ ///
+ /// Converts a to a by performing a round operation on all the coordinates.
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rectangle Round(RectangleF rectangle)
+ {
+ unchecked
+ {
+ return new Rectangle(
+ (int)MathF.Round(rectangle.X),
+ (int)MathF.Round(rectangle.Y),
+ (int)MathF.Round(rectangle.Width),
+ (int)MathF.Round(rectangle.Height));
+ }
+ }
+
+ ///
+ /// Creates a rectangle that represents the union between and .
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Rectangle Union(Rectangle a, Rectangle b)
+ {
+ int x1 = Math.Min(a.X, b.X);
+ int x2 = Math.Max(a.Right, b.Right);
+ int y1 = Math.Min(a.Y, b.Y);
+ int y2 = Math.Max(a.Bottom, b.Bottom);
+
+ return new Rectangle(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ ///
+ /// Deconstructs this rectangle into four integers.
+ ///
+ /// The out value for X.
+ /// The out value for Y.
+ /// The out value for the width.
+ /// The out value for the height.
+ public void Deconstruct(out int x, out int y, out int width, out int height)
+ {
+ x = this.X;
+ y = this.Y;
+ width = this.Width;
+ height = this.Height;
+ }
+
+ ///
+ /// Creates a Rectangle that represents the intersection between this Rectangle and the .
+ ///
+ /// The rectangle.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Intersect(Rectangle rectangle)
+ {
+ Rectangle result = Intersect(rectangle, this);
+
+ this.X = result.X;
+ this.Y = result.Y;
+ this.Width = result.Width;
+ this.Height = result.Height;
+ }
+
+ ///
+ /// Inflates this by the specified amount.
+ ///
+ /// The width.
+ /// The height.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Inflate(int width, int height)
+ {
+ unchecked
+ {
+ this.X -= width;
+ this.Y -= height;
+
+ this.Width += 2 * width;
+ this.Height += 2 * height;
+ }
+ }
+
+ ///
+ /// Inflates this by the specified amount.
+ ///
+ /// The size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Inflate(Size size) => this.Inflate(size.Width, size.Height);
+
+ ///
+ /// Determines if the specfied point is contained within the rectangular region defined by
+ /// this .
+ ///
+ /// The x-coordinate of the given point.
+ /// The y-coordinate of the given point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Contains(int x, int y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom;
+
+ ///
+ /// Determines if the specified point is contained within the rectangular region defined by this .
+ ///
+ /// The point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Contains(Point point) => this.Contains(point.X, point.Y);
+
+ ///
+ /// Determines if the rectangular region represented by is entirely contained
+ /// within the rectangular region represented by this .
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Contains(Rectangle rectangle) =>
+ (this.X <= rectangle.X) && (rectangle.Right <= this.Right) &&
+ (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom);
+
+ ///
+ /// Determines if the specfied intersects the rectangular region defined by
+ /// this .
+ ///
+ /// The other Rectange.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IntersectsWith(Rectangle rectangle) =>
+ (rectangle.X < this.Right) && (this.X < rectangle.Right) &&
+ (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom);
+
+ ///
+ /// Adjusts the location of this rectangle by the specified amount.
+ ///
+ /// The point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(Point point) => this.Offset(point.X, point.Y);
+
+ ///
+ /// Adjusts the location of this rectangle by the specified amount.
+ ///
+ /// The amount to offset the x-coordinate.
+ /// The amount to offset the y-coordinate.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(int dx, int dy)
+ {
+ unchecked
+ {
+ this.X += dx;
+ this.Y += dy;
+ }
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(this.X, this.Y, this.Width, this.Height);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"Rectangle [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
+ }
+
+ ///
+ public override bool Equals(object obj) => obj is Rectangle other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(Rectangle other) =>
+ this.X.Equals(other.X) &&
+ this.Y.Equals(other.Y) &&
+ this.Width.Equals(other.Width) &&
+ this.Height.Equals(other.Height);
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/RectangleF.cs b/src/SixLabors.Core/Primitives/RectangleF.cs
new file mode 100644
index 0000000000..c8b58f4ede
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/RectangleF.cs
@@ -0,0 +1,396 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.ComponentModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Stores a set of four single precision floating points that represent the location and size of a rectangle.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public struct RectangleF : IEquatable
+ {
+ ///
+ /// Represents a that has X, Y, Width, and Height values set to zero.
+ ///
+ public static readonly RectangleF Empty = default;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The horizontal position of the rectangle.
+ /// The vertical position of the rectangle.
+ /// The width of the rectangle.
+ /// The height of the rectangle.
+ public RectangleF(float x, float y, float width, float height)
+ {
+ this.X = x;
+ this.Y = y;
+ this.Width = width;
+ this.Height = height;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ ///
+ /// The which specifies the rectangles point in a two-dimensional plane.
+ ///
+ ///
+ /// The which specifies the rectangles height and width.
+ ///
+ public RectangleF(PointF point, SizeF size)
+ {
+ this.X = point.X;
+ this.Y = point.Y;
+ this.Width = size.Width;
+ this.Height = size.Height;
+ }
+
+ ///
+ /// Gets or sets the x-coordinate of this .
+ ///
+ public float X { get; set; }
+
+ ///
+ /// Gets or sets the y-coordinate of this .
+ ///
+ public float Y { get; set; }
+
+ ///
+ /// Gets or sets the width of this .
+ ///
+ public float Width { get; set; }
+
+ ///
+ /// Gets or sets the height of this .
+ ///
+ public float Height { get; set; }
+
+ ///
+ /// Gets or sets the coordinates of the upper-left corner of the rectangular region represented by this .
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public PointF Location
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new PointF(this.X, this.Y);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set
+ {
+ this.X = value.X;
+ this.Y = value.Y;
+ }
+ }
+
+ ///
+ /// Gets or sets the size of this .
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public SizeF Size
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new SizeF(this.Width, this.Height);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ set
+ {
+ this.Width = value.Width;
+ this.Height = value.Height;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => (this.Width <= 0) || (this.Height <= 0);
+
+ ///
+ /// Gets the y-coordinate of the top edge of this .
+ ///
+ public float Top => this.Y;
+
+ ///
+ /// Gets the x-coordinate of the right edge of this .
+ ///
+ public float Right
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this.X + this.Width;
+ }
+
+ ///
+ /// Gets the y-coordinate of the bottom edge of this .
+ ///
+ public float Bottom
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this.Y + this.Height;
+ }
+
+ ///
+ /// Gets the x-coordinate of the left edge of this .
+ ///
+ public float Left => this.X;
+
+ ///
+ /// Creates a with the coordinates of the specified by truncating each coordinate.
+ ///
+ /// The rectangle.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static explicit operator Rectangle(RectangleF rectangle) => Rectangle.Truncate(rectangle);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(RectangleF left, RectangleF right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ /// The on the left side of the operand.
+ /// The on the right side of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(RectangleF left, RectangleF right) => !left.Equals(right);
+
+ ///
+ /// Creates a new with the specified location and size.
+ /// The left coordinate of the rectangle.
+ /// The top coordinate of the rectangle.
+ /// The right coordinate of the rectangle.
+ /// The bottom coordinate of the rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+
+ // ReSharper disable once InconsistentNaming
+ public static RectangleF FromLTRB(float left, float top, float right, float bottom) => new RectangleF(left, top, right - left, bottom - top);
+
+ ///
+ /// Returns the center point of the given .
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static PointF Center(RectangleF rectangle) => new PointF(rectangle.Left + (rectangle.Width / 2), rectangle.Top + (rectangle.Height / 2));
+
+ ///
+ /// Creates a rectangle that represents the intersection between and
+ /// . If there is no intersection, an empty rectangle is returned.
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static RectangleF Intersect(RectangleF a, RectangleF b)
+ {
+ float x1 = MathF.Max(a.X, b.X);
+ float x2 = MathF.Min(a.Right, b.Right);
+ float y1 = MathF.Max(a.Y, b.Y);
+ float y2 = MathF.Min(a.Bottom, b.Bottom);
+
+ if (x2 >= x1 && y2 >= y1)
+ {
+ return new RectangleF(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ return Empty;
+ }
+
+ ///
+ /// Creates a that is inflated by the specified amount.
+ ///
+ /// The rectangle.
+ /// The amount to inflate the width by.
+ /// The amount to inflate the height by.
+ /// A new .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static RectangleF Inflate(RectangleF rectangle, float x, float y)
+ {
+ RectangleF r = rectangle;
+ r.Inflate(x, y);
+ return r;
+ }
+
+ ///
+ /// Transforms a rectangle by the given matrix.
+ ///
+ /// The source rectangle.
+ /// The transformation matrix.
+ /// A transformed .
+ public static RectangleF Transform(RectangleF rectangle, Matrix3x2 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 .
+ ///
+ /// The first rectangle.
+ /// The second rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static RectangleF Union(RectangleF a, RectangleF b)
+ {
+ float x1 = MathF.Min(a.X, b.X);
+ float x2 = MathF.Max(a.Right, b.Right);
+ float y1 = MathF.Min(a.Y, b.Y);
+ float y2 = MathF.Max(a.Bottom, b.Bottom);
+
+ return new RectangleF(x1, y1, x2 - x1, y2 - y1);
+ }
+
+ ///
+ /// Deconstructs this rectangle into four floats.
+ ///
+ /// The out value for X.
+ /// The out value for Y.
+ /// The out value for the width.
+ /// The out value for the height.
+ public void Deconstruct(out float x, out float y, out float width, out float height)
+ {
+ x = this.X;
+ y = this.Y;
+ width = this.Width;
+ height = this.Height;
+ }
+
+ ///
+ /// Creates a RectangleF that represents the intersection between this RectangleF and the .
+ ///
+ /// The rectangle.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Intersect(RectangleF rectangle)
+ {
+ RectangleF result = Intersect(rectangle, this);
+
+ this.X = result.X;
+ this.Y = result.Y;
+ this.Width = result.Width;
+ this.Height = result.Height;
+ }
+
+ ///
+ /// Inflates this by the specified amount.
+ ///
+ /// The width.
+ /// The height.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Inflate(float width, float height)
+ {
+ this.X -= width;
+ this.Y -= height;
+
+ this.Width += 2 * width;
+ this.Height += 2 * height;
+ }
+
+ ///
+ /// Inflates this by the specified amount.
+ ///
+ /// The size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Inflate(SizeF size) => this.Inflate(size.Width, size.Height);
+
+ ///
+ /// Determines if the specfied point is contained within the rectangular region defined by
+ /// this .
+ ///
+ /// The x-coordinate of the given point.
+ /// The y-coordinate of the given point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Contains(float x, float y) => this.X <= x && x < this.Right && this.Y <= y && y < this.Bottom;
+
+ ///
+ /// Determines if the specified point is contained within the rectangular region defined by this .
+ ///
+ /// The point.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Contains(PointF point) => this.Contains(point.X, point.Y);
+
+ ///
+ /// Determines if the rectangular region represented by is entirely contained
+ /// within the rectangular region represented by this .
+ ///
+ /// The rectangle.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Contains(RectangleF rectangle) =>
+ (this.X <= rectangle.X) && (rectangle.Right <= this.Right) &&
+ (this.Y <= rectangle.Y) && (rectangle.Bottom <= this.Bottom);
+
+ ///
+ /// Determines if the specfied intersects the rectangular region defined by
+ /// this .
+ ///
+ /// The other Rectange.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IntersectsWith(RectangleF rectangle) =>
+ (rectangle.X < this.Right) && (this.X < rectangle.Right) &&
+ (rectangle.Y < this.Bottom) && (this.Y < rectangle.Bottom);
+
+ ///
+ /// Adjusts the location of this rectangle by the specified amount.
+ ///
+ /// The point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(PointF point) => this.Offset(point.X, point.Y);
+
+ ///
+ /// Adjusts the location of this rectangle by the specified amount.
+ ///
+ /// The amount to offset the x-coordinate.
+ /// The amount to offset the y-coordinate.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Offset(float dx, float dy)
+ {
+ this.X += dx;
+ this.Y += dy;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(this.X, this.Y, this.Width, this.Height);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"RectangleF [ X={this.X}, Y={this.Y}, Width={this.Width}, Height={this.Height} ]";
+ }
+
+ ///
+ public override bool Equals(object obj) => obj is RectangleF other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(RectangleF other) =>
+ this.X.Equals(other.X) &&
+ this.Y.Equals(other.Y) &&
+ this.Width.Equals(other.Width) &&
+ this.Height.Equals(other.Height);
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/Size.cs b/src/SixLabors.Core/Primitives/Size.cs
new file mode 100644
index 0000000000..e0eb6484ce
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/Size.cs
@@ -0,0 +1,296 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.ComponentModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Stores an ordered pair of integers, which specify a height and width.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public struct Size : IEquatable
+ {
+ ///
+ /// Represents a that has Width and Height values set to zero.
+ ///
+ public static readonly Size Empty = default;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The width and height of the size.
+ public Size(int value)
+ : this()
+ {
+ this.Width = value;
+ this.Height = value;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The width of the size.
+ /// The height of the size.
+ public Size(int width, int height)
+ {
+ this.Width = width;
+ this.Height = height;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The size.
+ public Size(Size size)
+ : this()
+ {
+ this.Width = size.Width;
+ this.Height = size.Height;
+ }
+
+ ///
+ /// Initializes a new instance of the struct from the given .
+ ///
+ /// The point.
+ public Size(Point point)
+ {
+ this.Width = point.X;
+ this.Height = point.Y;
+ }
+
+ ///
+ /// Gets or sets the width of this .
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets the height of this .
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => this.Equals(Empty);
+
+ ///
+ /// Creates a with the dimensions of the specified .
+ ///
+ /// The point.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator SizeF(Size size) => new SizeF(size.Width, size.Height);
+
+ ///
+ /// Converts the given into a .
+ ///
+ /// The size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static explicit operator Point(Size size) => new Point(size.Width, size.Height);
+
+ ///
+ /// Computes the sum of adding two sizes.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size operator +(Size left, Size right) => Add(left, right);
+
+ ///
+ /// Computes the difference left by subtracting one size from another.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size operator -(Size left, Size right) => Subtract(left, right);
+
+ ///
+ /// Multiplies a by an producing .
+ ///
+ /// Multiplier of type .
+ /// Multiplicand of type .
+ /// Product of type .
+ public static Size operator *(int left, Size right) => Multiply(right, left);
+
+ ///
+ /// Multiplies by an producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type .
+ public static Size operator *(Size left, int right) => Multiply(left, right);
+
+ ///
+ /// Divides by an producing .
+ ///
+ /// Dividend of type .
+ /// Divisor of type .
+ /// Result of type .
+ public static Size operator /(Size left, int right) => new Size(unchecked(left.Width / right), unchecked(left.Height / right));
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplier of type .
+ /// Multiplicand of type .
+ /// Product of type .
+ public static SizeF operator *(float left, Size right) => Multiply(right, left);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type .
+ public static SizeF operator *(Size left, float right) => Multiply(left, right);
+
+ ///
+ /// Divides by a producing .
+ ///
+ /// Dividend of type .
+ /// Divisor of type .
+ /// Result of type .
+ public static SizeF operator /(Size left, float right)
+ => new SizeF(left.Width / right, left.Height / right);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(Size left, Size right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ ///
+ /// The on the left side of the operand.
+ ///
+ ///
+ /// The on the right side of the operand.
+ ///
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(Size left, Size right) => !left.Equals(right);
+
+ ///
+ /// Performs vector addition of two objects.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size Add(Size left, Size right) => new Size(unchecked(left.Width + right.Width), unchecked(left.Height + right.Height));
+
+ ///
+ /// Contracts a by another .
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size Subtract(Size left, Size right) => new Size(unchecked(left.Width - right.Width), unchecked(left.Height - right.Height));
+
+ ///
+ /// Converts a to a by performing a ceiling operation on all the dimensions.
+ ///
+ /// The size.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size Ceiling(SizeF size) => new Size(unchecked((int)MathF.Ceiling(size.Width)), unchecked((int)MathF.Ceiling(size.Height)));
+
+ ///
+ /// Converts a to a by performing a round operation on all the dimensions.
+ ///
+ /// The size.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size Round(SizeF size) => new Size(unchecked((int)MathF.Round(size.Width)), unchecked((int)MathF.Round(size.Height)));
+
+ ///
+ /// Transforms a size by the given matrix.
+ ///
+ /// The source size.
+ /// The transformation matrix.
+ /// A transformed size.
+ public static SizeF Transform(Size size, Matrix3x2 matrix)
+ {
+ var v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix);
+
+ return new SizeF(v.X, v.Y);
+ }
+
+ ///
+ /// Converts a to a by performing a round operation on all the dimensions.
+ ///
+ /// The size.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Size Truncate(SizeF size) => new Size(unchecked((int)size.Width), unchecked((int)size.Height));
+
+ ///
+ /// Deconstructs this size into two integers.
+ ///
+ /// The out value for the width.
+ /// The out value for the height.
+ public void Deconstruct(out int width, out int height)
+ {
+ width = this.Width;
+ height = this.Height;
+ }
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.Width, this.Height);
+
+ ///
+ public override string ToString() => $"Size [ Width={this.Width}, Height={this.Height} ]";
+
+ ///
+ public override bool Equals(object obj) => obj is Size other && this.Equals(other);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(Size other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
+
+ ///
+ /// Multiplies by an producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type .
+ private static Size Multiply(Size size, int multiplier) =>
+ new Size(unchecked(size.Width * multiplier), unchecked(size.Height * multiplier));
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type SizeF.
+ private static SizeF Multiply(Size size, float multiplier) =>
+ new SizeF(size.Width * multiplier, size.Height * multiplier);
+ }
+}
\ No newline at end of file
diff --git a/src/SixLabors.Core/Primitives/SizeF.cs b/src/SixLabors.Core/Primitives/SizeF.cs
new file mode 100644
index 0000000000..5d503a705d
--- /dev/null
+++ b/src/SixLabors.Core/Primitives/SizeF.cs
@@ -0,0 +1,233 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.ComponentModel;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace SixLabors.Primitives
+{
+ ///
+ /// Stores an ordered pair of single precision floating points, which specify a height and width.
+ ///
+ ///
+ /// This struct is fully mutable. This is done (against the guidelines) for the sake of performance,
+ /// as it avoids the need to create new values for modification operations.
+ ///
+ public struct SizeF : IEquatable
+ {
+ ///
+ /// Represents a that has Width and Height values set to zero.
+ ///
+ public static readonly SizeF Empty = default;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The width of the size.
+ /// The height of the size.
+ public SizeF(float width, float height)
+ {
+ this.Width = width;
+ this.Height = height;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The size.
+ public SizeF(SizeF size)
+ : this()
+ {
+ this.Width = size.Width;
+ this.Height = size.Height;
+ }
+
+ ///
+ /// Initializes a new instance of the struct from the given .
+ ///
+ /// The point.
+ public SizeF(PointF point)
+ {
+ this.Width = point.X;
+ this.Height = point.Y;
+ }
+
+ ///
+ /// Gets or sets the width of this .
+ ///
+ public float Width { get; set; }
+
+ ///
+ /// Gets or sets the height of this .
+ ///
+ public float Height { get; set; }
+
+ ///
+ /// Gets a value indicating whether this is empty.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public bool IsEmpty => this.Equals(Empty);
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ /// The point.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static implicit operator Vector2(SizeF point) => new Vector2(point.Width, point.Height);
+
+ ///
+ /// Creates a with the dimensions of the specified by truncating each of the dimensions.
+ ///
+ /// The size.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static explicit operator Size(SizeF size) => new Size(unchecked((int)size.Width), unchecked((int)size.Height));
+
+ ///
+ /// Converts the given into a .
+ ///
+ /// The size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static explicit operator PointF(SizeF size) => new PointF(size.Width, size.Height);
+
+ ///
+ /// Computes the sum of adding two sizes.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SizeF operator +(SizeF left, SizeF right) => Add(left, right);
+
+ ///
+ /// Computes the difference left by subtracting one size from another.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SizeF operator -(SizeF left, SizeF right) => Subtract(left, right);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplier of type .
+ /// Multiplicand of type .
+ /// Product of type .
+ public static SizeF operator *(float left, SizeF right) => Multiply(right, left);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type .
+ public static SizeF operator *(SizeF left, float right) => Multiply(left, right);
+
+ ///
+ /// Divides by a producing .
+ ///
+ /// Dividend of type .
+ /// Divisor of type .
+ /// Result of type .
+ public static SizeF operator /(SizeF left, float right)
+ => new SizeF(left.Width / right, left.Height / right);
+
+ ///
+ /// Compares two objects for equality.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// True if the current left is equal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(SizeF left, SizeF right) => left.Equals(right);
+
+ ///
+ /// Compares two objects for inequality.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ ///
+ /// True if the current left is unequal to the parameter; otherwise, false.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(SizeF left, SizeF right) => !left.Equals(right);
+
+ ///
+ /// Performs vector addition of two objects.
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SizeF Add(SizeF left, SizeF right) => new SizeF(left.Width + right.Width, left.Height + right.Height);
+
+ ///
+ /// Contracts a by another .
+ ///
+ /// The size on the left hand of the operand.
+ /// The size on the right hand of the operand.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SizeF Subtract(SizeF left, SizeF right) => new SizeF(left.Width - right.Width, left.Height - right.Height);
+
+ ///
+ /// Transforms a size by the given matrix.
+ ///
+ /// The source size.
+ /// The transformation matrix.
+ /// A transformed size.
+ public static SizeF Transform(SizeF size, Matrix3x2 matrix)
+ {
+ var v = Vector2.Transform(new Vector2(size.Width, size.Height), matrix);
+
+ return new SizeF(v.X, v.Y);
+ }
+
+ ///
+ /// Deconstructs this size into two floats.
+ ///
+ /// The out value for the width.
+ /// The out value for the height.
+ public void Deconstruct(out float width, out float height)
+ {
+ width = this.Width;
+ height = this.Height;
+ }
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(this.Width, this.Height);
+
+ ///
+ public override string ToString() => $"SizeF [ Width={this.Width}, Height={this.Height} ]";
+
+ ///
+ public override bool Equals(object obj) => obj is SizeF && this.Equals((SizeF)obj);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(SizeF other) => this.Width.Equals(other.Width) && this.Height.Equals(other.Height);
+
+ ///
+ /// Multiplies by a producing .
+ ///
+ /// Multiplicand of type .
+ /// Multiplier of type .
+ /// Product of type SizeF.
+ private static SizeF Multiply(SizeF size, float multiplier) =>
+ new SizeF(size.Width * multiplier, size.Height * multiplier);
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Helpers/DebugGuardTests.cs b/tests/SixLabors.Core.Tests/Helpers/DebugGuardTests.cs
new file mode 100644
index 0000000000..0b0d33090f
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Helpers/DebugGuardTests.cs
@@ -0,0 +1,234 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+// tell this file to enable debug conditional method calls, i.e. all the debug guard calls
+#define DEBUG
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace SixLabors.Helpers.Tests
+{
+ public class DebugGuardTests
+ {
+ private class Foo
+ {
+ }
+
+ [Fact]
+ public void AllStaticMethodsOnOnDebugGuardHaveDEBUGConditional()
+ {
+ IEnumerable methods = typeof(DebugGuard).GetTypeInfo().GetMethods()
+ .Where(x => x.IsStatic);
+
+ foreach (MethodInfo m in methods)
+ {
+ IEnumerable attribs = m.GetCustomAttributes();
+ Assert.True(attribs.Select(x => x.ConditionString).Contains("DEBUG"), $"Method '{m.Name}' does not have [Conditional(\"DEBUG\")] set.");
+ }
+ }
+
+ [Fact]
+ public void NotNull_WhenNull_Throws()
+ {
+ Foo foo = null;
+ Assert.Throws(() => Guard.NotNull(foo, nameof(foo)));
+ }
+
+ [Fact]
+ public void NotNull_WhenNotNull()
+ {
+ var foo = new Foo();
+ Guard.NotNull(foo, nameof(foo));
+ }
+
+ [Theory]
+ [InlineData(null, true)]
+ [InlineData("", true)]
+ [InlineData(" ", true)]
+ [InlineData("$", false)]
+ [InlineData("lol", false)]
+ public void NotNullOrWhiteSpace(string str, bool shouldThrow)
+ {
+ if (shouldThrow)
+ {
+ Assert.ThrowsAny(() => Guard.NotNullOrWhiteSpace(str, nameof(str)));
+ }
+ else
+ {
+ Guard.NotNullOrWhiteSpace(str, nameof(str));
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void IsTrue(bool value)
+ {
+ if (!value)
+ {
+ Assert.Throws(() => Guard.IsTrue(value, nameof(value), "Boo!"));
+ }
+ else
+ {
+ Guard.IsTrue(value, nameof(value), "Boo.");
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void IsFalse(bool value)
+ {
+ if (value)
+ {
+ Assert.Throws(() => Guard.IsFalse(value, nameof(value), "Boo!"));
+ }
+ else
+ {
+ Guard.IsFalse(value, nameof(value), "Boo.");
+ }
+ }
+
+ public static readonly TheoryData SizeCheckData = new TheoryData
+ {
+ { 0, 0, false },
+ { 1, 1, false },
+ { 1, 0, false },
+ { 13, 13, false },
+ { 20, 13, false },
+ { 12, 13, true },
+ { 0, 1, true },
+ };
+
+ [Theory]
+ [MemberData(nameof(SizeCheckData))]
+ public void MustBeSizedAtLeast(int length, int minLength, bool shouldThrow)
+ {
+ int[] data = new int[length];
+
+ if (shouldThrow)
+ {
+ Assert.Throws(() => Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data)));
+ Assert.Throws(() => Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data)));
+ }
+ else
+ {
+ Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data));
+ Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data));
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(SizeCheckData))]
+ public void DestinationShouldNotBeTooShort(int destLength, int sourceLength, bool shouldThrow)
+ {
+ int[] dest = new int[destLength];
+ int[] source = new int[sourceLength];
+
+ if (shouldThrow)
+ {
+ Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest)));
+ Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest)));
+ }
+ else
+ {
+ Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest));
+ Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest));
+ }
+ }
+
+ [Fact]
+ public void MustBeLessThan_IsLess_ThrowsNoException()
+ {
+ DebugGuard.MustBeLessThan(0, 1, "myParamName");
+ }
+
+ [Theory]
+ [InlineData(2, 1)]
+ [InlineData(1, 1)]
+ public void MustBeLessThan_IsGreaterOrEqual_ThrowsNoException(int value, int max)
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(
+ () => DebugGuard.MustBeLessThan(value, max, "myParamName"));
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value {value} must be less than {max}.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(0, 1)]
+ [InlineData(1, 1)]
+ public void MustBeLessThanOrEqualTo_IsLessOrEqual_ThrowsNoException(int value, int max)
+ {
+ DebugGuard.MustBeLessThanOrEqualTo(value, max, "myParamName");
+ }
+
+ [Fact]
+ public void MustBeLessThanOrEqualTo_IsGreater_ThrowsNoException()
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(() => DebugGuard.MustBeLessThanOrEqualTo(2, 1, "myParamName"));
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value 2 must be less than or equal to 1.", exception.Message);
+ }
+
+ [Fact]
+ public void MustBeGreaterThan_IsGreater_ThrowsNoException()
+ {
+ DebugGuard.MustBeGreaterThan(2, 1, "myParamName");
+ }
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(1, 1)]
+ public void MustBeGreaterThan_IsLessOrEqual_ThrowsNoException(int value, int min)
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(
+ () => DebugGuard.MustBeGreaterThan(value, min, "myParamName"));
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value {value} must be greater than {min}.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(2, 1)]
+ [InlineData(1, 1)]
+ public void MustBeGreaterThanOrEqualTo_IsGreaterOrEqual_ThrowsNoException(int value, int min)
+ {
+ DebugGuard.MustBeGreaterThanOrEqualTo(value, min, "myParamName");
+ }
+
+ [Fact]
+ public void MustBeGreaterThanOrEqualTo_IsLess_ThrowsNoException()
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(
+ () => DebugGuard.MustBeGreaterThanOrEqualTo(1, 2, "myParamName"));
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value 1 must be greater than or equal to 2.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(new int[] { 1, 2 }, 1)]
+ [InlineData(new int[] { 1, 2 }, 2)]
+ public void MustBeSizedAtLeast_Array_LengthIsGreaterOrEqual_ThrowsNoException(int[] value, int minLength)
+ {
+ DebugGuard.MustBeSizedAtLeast(value, minLength, "myParamName");
+ }
+
+ [Fact]
+ public void MustBeSizedAtLeast_Array_LengthIsLess_ThrowsException()
+ {
+ ArgumentException exception = Assert.Throws(
+ () => DebugGuard.MustBeSizedAtLeast(new int[] { 1, 2 }, 3, "myParamName"));
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"The size must be at least 3.", exception.Message);
+ }
+ }
+}
diff --git a/tests/SixLabors.Core.Tests/Helpers/FloatRoundingComparer.cs b/tests/SixLabors.Core.Tests/Helpers/FloatRoundingComparer.cs
new file mode 100644
index 0000000000..15220d4b14
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Helpers/FloatRoundingComparer.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace SixLabors.Tests.Helpers
+{
+ ///
+ /// Allows the comparison of single-precision floating point values by precision.
+ ///
+ public struct FloatRoundingComparer : IEqualityComparer, IEqualityComparer
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The number of decimal places (valid values: 0-7).
+ public FloatRoundingComparer(int precision)
+ {
+ Guard.MustBeBetweenOrEqualTo(precision, 0, 7, nameof(precision));
+ this.Precision = precision;
+ }
+
+ ///
+ /// Gets the number of decimal places (valid values: 0-7).
+ ///
+ public int Precision { get; }
+
+ ///
+ public bool Equals(float x, float y)
+ {
+ float xp = (float)Math.Round(x, this.Precision, MidpointRounding.AwayFromZero);
+ float yp = (float)Math.Round(y, this.Precision, MidpointRounding.AwayFromZero);
+
+ return Comparer.Default.Compare(xp, yp) == 0;
+ }
+
+ ///
+ public bool Equals(Vector4 x, Vector4 y)
+ {
+ return this.Equals(x.X, y.X) && this.Equals(x.Y, y.Y) && this.Equals(x.Z, y.Z) && this.Equals(x.W, y.W);
+ }
+
+ ///
+ public int GetHashCode(float obj)
+ {
+ unchecked
+ {
+ int hashCode = obj.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.Precision.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ ///
+ public int GetHashCode(Vector4 obj) => HashCode.Combine(obj, this.Precision);
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Helpers/GeometryUtilitiesTests.cs b/tests/SixLabors.Core.Tests/Helpers/GeometryUtilitiesTests.cs
new file mode 100644
index 0000000000..93e4cec60a
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Helpers/GeometryUtilitiesTests.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using Xunit;
+
+namespace SixLabors.Tests.Helpers
+{
+ public class GeometryUtilitiesTests
+ {
+ [Fact]
+ public void Convert_Degree_To_Radian()
+ => Assert.Equal((float)(Math.PI / 2D), GeometryUtilities.DegreeToRadian(90F), new FloatRoundingComparer(6));
+
+ [Fact]
+ public void Convert_Radian_To_Degree()
+ => Assert.Equal(60F, GeometryUtilities.RadianToDegree((float)(Math.PI / 3D)), new FloatRoundingComparer(5));
+ }
+}
diff --git a/tests/SixLabors.Core.Tests/Helpers/GuardTests.cs b/tests/SixLabors.Core.Tests/Helpers/GuardTests.cs
new file mode 100644
index 0000000000..4b5ebbdfde
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Helpers/GuardTests.cs
@@ -0,0 +1,248 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using Xunit;
+
+namespace SixLabors.Helpers.Tests
+{
+ public class GuardTests
+ {
+ private class Foo
+ {
+ }
+
+ [Fact]
+ public void NotNull_WhenNull_Throws()
+ {
+ Foo foo = null;
+ Assert.Throws(() => Guard.NotNull(foo, nameof(foo)));
+ }
+
+ [Fact]
+ public void NotNull_WhenNotNull()
+ {
+ Foo foo = new Foo();
+ Guard.NotNull(foo, nameof(foo));
+ }
+
+ [Theory]
+ [InlineData(null, true)]
+ [InlineData("", true)]
+ [InlineData(" ", true)]
+ [InlineData("$", false)]
+ [InlineData("lol", false)]
+ public void NotNullOrWhiteSpace(string str, bool shouldThrow)
+ {
+ if (shouldThrow)
+ {
+ Assert.ThrowsAny(() => Guard.NotNullOrWhiteSpace(str, nameof(str)));
+ }
+ else
+ {
+ Guard.NotNullOrWhiteSpace(str, nameof(str));
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void IsTrue(bool value)
+ {
+ if (!value)
+ {
+ Assert.Throws(() => Guard.IsTrue(value, nameof(value), "Boo!"));
+ }
+ else
+ {
+ Guard.IsTrue(value, nameof(value), "Boo.");
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void IsFalse(bool value)
+ {
+ if (value)
+ {
+ Assert.Throws(() => Guard.IsFalse(value, nameof(value), "Boo!"));
+ }
+ else
+ {
+ Guard.IsFalse(value, nameof(value), "Boo.");
+ }
+ }
+
+ public static readonly TheoryData SizeCheckData = new TheoryData
+ {
+ { 0, 0, false },
+ { 1, 1, false },
+ { 1, 0, false },
+ { 13, 13, false },
+ { 20, 13, false },
+ { 12, 13, true },
+ { 0, 1, true },
+ };
+
+ [Theory]
+ [MemberData(nameof(SizeCheckData))]
+ public void MustBeSizedAtLeast(int length, int minLength, bool shouldThrow)
+ {
+ int[] data = new int[length];
+
+ if (shouldThrow)
+ {
+ Assert.Throws(() => Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data)));
+ Assert.Throws(() => Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data)));
+ }
+ else
+ {
+ Guard.MustBeSizedAtLeast((Span)data, minLength, nameof(data));
+ Guard.MustBeSizedAtLeast((ReadOnlySpan)data, minLength, nameof(data));
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(SizeCheckData))]
+ public void DestinationShouldNotBeTooShort(int destLength, int sourceLength, bool shouldThrow)
+ {
+ int[] dest = new int[destLength];
+ int[] source = new int[sourceLength];
+
+ if (shouldThrow)
+ {
+ Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest)));
+ Assert.Throws(() => Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest)));
+ }
+ else
+ {
+ Guard.DestinationShouldNotBeTooShort((Span)source, (Span)dest, nameof(dest));
+ Guard.DestinationShouldNotBeTooShort((ReadOnlySpan)source, (Span)dest, nameof(dest));
+ }
+ }
+
+ [Fact]
+ public void MustBeLessThan_IsLess_ThrowsNoException()
+ {
+ Guard.MustBeLessThan(0, 1, "myParamName");
+ }
+
+ [Theory]
+ [InlineData(2, 1)]
+ [InlineData(1, 1)]
+ public void MustBeLessThan_IsGreaterOrEqual_ThrowsNoException(int value, int max)
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(() =>
+ {
+ Guard.MustBeLessThan(value, max, "myParamName");
+ });
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value {value} must be less than {max}.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(0, 1)]
+ [InlineData(1, 1)]
+ public void MustBeLessThanOrEqualTo_IsLessOrEqual_ThrowsNoException(int value, int max)
+ {
+ Guard.MustBeLessThanOrEqualTo(value, max, "myParamName");
+ }
+
+ [Fact]
+ public void MustBeLessThanOrEqualTo_IsGreater_ThrowsNoException()
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(() =>
+ {
+ Guard.MustBeLessThanOrEqualTo(2, 1, "myParamName");
+ });
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value 2 must be less than or equal to 1.", exception.Message);
+ }
+
+ [Fact]
+ public void MustBeGreaterThan_IsGreater_ThrowsNoException()
+ {
+ Guard.MustBeGreaterThan(2, 1, "myParamName");
+ }
+
+ [Theory]
+ [InlineData(1, 2)]
+ [InlineData(1, 1)]
+ public void MustBeGreaterThan_IsLessOrEqual_ThrowsNoException(int value, int min)
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(() =>
+ {
+ Guard.MustBeGreaterThan(value, min, "myParamName");
+ });
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value {value} must be greater than {min}.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(2, 1)]
+ [InlineData(1, 1)]
+ public void MustBeGreaterThanOrEqualTo_IsGreaterOrEqual_ThrowsNoException(int value, int min)
+ {
+ Guard.MustBeGreaterThanOrEqualTo(value, min, "myParamName");
+ }
+
+ [Fact]
+ public void MustBeGreaterThanOrEqualTo_IsLess_ThrowsNoException()
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(() =>
+ {
+ Guard.MustBeGreaterThanOrEqualTo(1, 2, "myParamName");
+ });
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value 1 must be greater than or equal to 2.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(1, 1, 3)]
+ [InlineData(2, 1, 3)]
+ [InlineData(3, 1, 3)]
+ public void MustBeBetweenOrEqualTo_IsBetweenOrEqual_ThrowsNoException(int value, int min, int max)
+ {
+ Guard.MustBeBetweenOrEqualTo(value, min, max, "myParamName");
+ }
+
+ [Theory]
+ [InlineData(0, 1, 3)]
+ [InlineData(4, 1, 3)]
+ public void MustBeBetweenOrEqualTo_IsLessOrGreater_ThrowsNoException(int value, int min, int max)
+ {
+ ArgumentOutOfRangeException exception = Assert.Throws(() =>
+ {
+ Guard.MustBeBetweenOrEqualTo(value, min, max, "myParamName");
+ });
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains($"Value {value} must be greater than or equal to {min} and less than or equal to {max}.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData(2, 1)]
+ [InlineData(2, 2)]
+ public void MustBeSizedAtLeast_Array_LengthIsGreaterOrEqual_ThrowsNoException(int valueLength, int minLength)
+ {
+ Guard.MustBeSizedAtLeast(new int[valueLength], minLength, "myParamName");
+ }
+
+ [Fact]
+ public void MustBeSizedAtLeast_Array_LengthIsLess_ThrowsException()
+ {
+ ArgumentException exception = Assert.Throws(() =>
+ {
+ Guard.MustBeSizedAtLeast(new int[] { 1, 2 }, 3, "myParamName");
+ });
+
+ Assert.Equal("myParamName", exception.ParamName);
+ Assert.Contains("The size must be at least 3", exception.Message);
+ }
+ }
+}
diff --git a/tests/SixLabors.Core.Tests/Helpers/MathFTests.cs b/tests/SixLabors.Core.Tests/Helpers/MathFTests.cs
new file mode 100644
index 0000000000..9ae95f0140
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Helpers/MathFTests.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using Xunit;
+
+namespace SixLabors.Tests.Helpers
+{
+ public class MathFTests
+ {
+ [Fact]
+ public void MathF_PI_Is_Equal()
+ {
+ Assert.Equal(MathF.PI, (float)Math.PI);
+ }
+
+ [Fact]
+ public void MathF_Ceililng_Is_Equal()
+ {
+ Assert.Equal(MathF.Ceiling(0.3333F), (float)Math.Ceiling(0.3333F));
+ }
+
+ [Fact]
+ public void MathF_Cos_Is_Equal()
+ {
+ Assert.Equal(MathF.Cos(0.3333F), (float)Math.Cos(0.3333F));
+ }
+
+ [Fact]
+ public void MathF_Abs_Is_Equal()
+ {
+ Assert.Equal(MathF.Abs(-0.3333F), (float)Math.Abs(-0.3333F));
+ }
+
+ [Fact]
+ public void MathF_Atan2_Is_Equal()
+ {
+ Assert.Equal(MathF.Atan2(1.2345F, 1.2345F), (float)Math.Atan2(1.2345F, 1.2345F));
+ }
+
+ [Fact]
+ public void MathF_Exp_Is_Equal()
+ {
+ Assert.Equal(MathF.Exp(1.2345F), (float)Math.Exp(1.2345F));
+ }
+
+ [Fact]
+ public void MathF_Floor_Is_Equal()
+ {
+ Assert.Equal(MathF.Floor(1.2345F), (float)Math.Floor(1.2345F));
+ }
+
+ [Fact]
+ public void MathF_Min_Is_Equal()
+ {
+ Assert.Equal(MathF.Min(1.2345F, 5.4321F), (float)Math.Min(1.2345F, 5.4321F));
+ }
+
+ [Fact]
+ public void MathF_Max_Is_Equal()
+ {
+ Assert.Equal(MathF.Max(1.2345F, 5.4321F), (float)Math.Max(1.2345F, 5.4321F));
+ }
+
+ [Fact]
+ public void MathF_Pow_Is_Equal()
+ {
+ Assert.Equal(MathF.Pow(1.2345F, 5.4321F), (float)Math.Pow(1.2345F, 5.4321F));
+ }
+
+ [Fact]
+ public void MathF_Round_Is_Equal()
+ {
+ Assert.Equal(MathF.Round(1.2345F), (float)Math.Round(1.2345F));
+ }
+
+ [Fact]
+ public void MathF_Round_With_Midpoint_Is_Equal()
+ {
+ Assert.Equal(MathF.Round(1.2345F, MidpointRounding.AwayFromZero), (float)Math.Round(1.2345F, MidpointRounding.AwayFromZero));
+ }
+
+ [Fact]
+ public void MathF_Sin_Is_Equal()
+ {
+ Assert.Equal(MathF.Sin(1.2345F), (float)Math.Sin(1.2345F));
+ }
+
+ [Fact]
+ public void MathF_Sqrt_Is_Equal()
+ {
+ Assert.Equal(MathF.Sqrt(2F), (float)Math.Sqrt(2F));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryAllocatorTests.cs b/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryAllocatorTests.cs
new file mode 100644
index 0000000000..1c1d721674
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Memory/ArrayPoolMemoryAllocatorTests.cs
@@ -0,0 +1,257 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+// ReSharper disable InconsistentNaming
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.Tests;
+using Xunit;
+
+namespace SixLabors.Memory.Tests
+{
+ public class ArrayPoolMemoryAllocatorTests
+ {
+ private const int MaxPooledBufferSizeInBytes = 2048;
+
+ private const int PoolSelectorThresholdInBytes = MaxPooledBufferSizeInBytes / 2;
+
+ private MemoryAllocator MemoryAllocator { get; set; } =
+ new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes);
+
+ ///
+ /// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location.
+ ///
+ private bool CheckIsRentingPooledBuffer(int length)
+ where T : struct
+ {
+ IMemoryOwner buffer = this.MemoryAllocator.Allocate(length);
+ ref T ptrToPrevPosition0 = ref buffer.GetReference();
+ buffer.Dispose();
+
+ buffer = this.MemoryAllocator.Allocate(length);
+ bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref buffer.GetReference());
+ buffer.Dispose();
+
+ return sameBuffers;
+ }
+
+ public class BufferTests : BufferTestSuite
+ {
+ public BufferTests()
+ : base(new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes))
+ {
+ }
+ }
+
+ public class Constructor
+ {
+ [Fact]
+ public void WhenBothParametersPassedByUser()
+ {
+ var mgr = new ArrayPoolMemoryAllocator(1111, 666);
+ Assert.Equal(1111, mgr.MaxPoolSizeInBytes);
+ Assert.Equal(666, mgr.PoolSelectorThresholdInBytes);
+ }
+
+ [Fact]
+ public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdValueIsAutoCalculated()
+ {
+ var mgr = new ArrayPoolMemoryAllocator(5000);
+ Assert.Equal(5000, mgr.MaxPoolSizeInBytes);
+ Assert.True(mgr.PoolSelectorThresholdInBytes < mgr.MaxPoolSizeInBytes);
+ }
+
+ [Fact]
+ public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown()
+ {
+ Assert.ThrowsAny(() => new ArrayPoolMemoryAllocator(100, 200));
+ }
+ }
+
+ [Theory]
+ [InlineData(32)]
+ [InlineData(512)]
+ [InlineData(MaxPooledBufferSizeInBytes - 1)]
+ public void SmallBuffersArePooled_OfByte(int size)
+ {
+ Assert.True(this.CheckIsRentingPooledBuffer(size));
+ }
+
+ [Theory]
+ [InlineData(128 * 1024 * 1024)]
+ [InlineData(MaxPooledBufferSizeInBytes + 1)]
+ public void LargeBuffersAreNotPooled_OfByte(int size)
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ // can lead to OutOfMemoryException
+ return;
+ }
+
+ Assert.False(this.CheckIsRentingPooledBuffer(size));
+ }
+
+ [Fact]
+ public unsafe void SmallBuffersArePooled_OfBigValueType()
+ {
+ int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) - 1;
+
+ Assert.True(this.CheckIsRentingPooledBuffer(count));
+ }
+
+ [Fact]
+ public unsafe void LaregeBuffersAreNotPooled_OfBigValueType()
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ // can lead to OutOfMemoryException
+ return;
+ }
+
+ int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) + 1;
+
+ Assert.False(this.CheckIsRentingPooledBuffer(count));
+ }
+
+ [Theory]
+ [InlineData(AllocationOptions.None)]
+ [InlineData(AllocationOptions.Clean)]
+ public void CleaningRequests_AreControlledByAllocationParameter_Clean(AllocationOptions options)
+ {
+ using (IMemoryOwner firstAlloc = this.MemoryAllocator.Allocate(42))
+ {
+ firstAlloc.GetSpan().Fill(666);
+ }
+
+ using (IMemoryOwner secondAlloc = this.MemoryAllocator.Allocate(42, options))
+ {
+ int expected = options == AllocationOptions.Clean ? 0 : 666;
+ Assert.Equal(expected, secondAlloc.GetSpan()[0]);
+ }
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ReleaseRetainedResources_ReplacesInnerArrayPool(bool keepBufferAlive)
+ {
+ IMemoryOwner buffer = this.MemoryAllocator.Allocate(32);
+ ref int ptrToPrev0 = ref MemoryMarshal.GetReference(buffer.GetSpan());
+
+ if (!keepBufferAlive)
+ {
+ buffer.Dispose();
+ }
+
+ this.MemoryAllocator.ReleaseRetainedResources();
+
+ buffer = this.MemoryAllocator.Allocate(32);
+
+ Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.GetReference()));
+ }
+
+ [Fact]
+ public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed()
+ {
+ IMemoryOwner buffer = this.MemoryAllocator.Allocate(32);
+ this.MemoryAllocator.ReleaseRetainedResources();
+ buffer.Dispose();
+ }
+
+ [Fact]
+ public void AllocationOverLargeArrayThreshold_UsesDifferentPool()
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ // can lead to OutOfMemoryException
+ return;
+ }
+
+ const int ArrayLengthThreshold = PoolSelectorThresholdInBytes / sizeof(int);
+
+ IMemoryOwner small = this.MemoryAllocator.Allocate(ArrayLengthThreshold - 1);
+ ref int ptr2Small = ref small.GetReference();
+ small.Dispose();
+
+ IMemoryOwner large = this.MemoryAllocator.Allocate(ArrayLengthThreshold + 1);
+
+ Assert.False(Unsafe.AreSame(ref ptr2Small, ref large.GetReference()));
+ }
+
+ [Fact]
+ public void CreateWithAggressivePooling()
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ // can lead to OutOfMemoryException
+ return;
+ }
+
+ this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithAggressivePooling();
+
+ Assert.True(this.CheckIsRentingPooledBuffer(4096 * 4096));
+ }
+
+ [Fact]
+ public void CreateDefault()
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ // can lead to OutOfMemoryException
+ return;
+ }
+
+ this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault();
+
+ Assert.False(this.CheckIsRentingPooledBuffer(2 * 4096 * 4096));
+ Assert.True(this.CheckIsRentingPooledBuffer(2048 * 2048));
+ }
+
+ [Fact]
+ public void CreateWithModeratePooling()
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ // can lead to OutOfMemoryException
+ return;
+ }
+
+ this.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling();
+
+ Assert.False(this.CheckIsRentingPooledBuffer(2048 * 2048));
+ Assert.True(this.CheckIsRentingPooledBuffer(1024 * 16));
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct Rgba32
+ {
+ private readonly uint dummy;
+ }
+
+ private const int SizeOfLargeStruct = MaxPooledBufferSizeInBytes / 5;
+
+ [StructLayout(LayoutKind.Explicit, Size = SizeOfLargeStruct)]
+ private struct LargeStruct
+ {
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ [InlineData((int.MaxValue / SizeOfLargeStruct) + 1)]
+ public void AllocateIncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
+ {
+ ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length));
+ Assert.Equal("length", ex.ParamName);
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
+ {
+ ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.AllocateManagedByteBuffer(length));
+ Assert.Equal("length", ex.ParamName);
+ }
+ }
+}
diff --git a/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs b/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs
new file mode 100644
index 0000000000..fd53db1d8d
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Memory/BufferExtensions.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.Memory.Tests
+{
+ internal static class BufferExtensions
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Span GetSpan(this IMemoryOwner buffer)
+ => buffer.Memory.Span;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int Length(this IMemoryOwner buffer)
+ => buffer.GetSpan().Length;
+
+ public static ref T GetReference(this IMemoryOwner buffer)
+ where T : struct =>
+ ref MemoryMarshal.GetReference(buffer.GetSpan());
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs b/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs
new file mode 100644
index 0000000000..eca1842646
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Memory/BufferTestSuite.cs
@@ -0,0 +1,318 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Xunit;
+
+// ReSharper disable InconsistentNaming
+namespace SixLabors.Memory.Tests
+{
+ ///
+ /// Inherit this class to test an implementation (provided by ).
+ ///
+ public abstract class BufferTestSuite
+ {
+ protected BufferTestSuite(MemoryAllocator memoryAllocator)
+ {
+ this.MemoryAllocator = memoryAllocator;
+ }
+
+ protected MemoryAllocator MemoryAllocator { get; }
+
+ public struct CustomStruct : IEquatable
+ {
+ public long A;
+
+ public byte B;
+
+ public float C;
+
+ public CustomStruct(long a, byte b, float c)
+ {
+ this.A = a;
+ this.B = b;
+ this.C = c;
+ }
+
+ public bool Equals(CustomStruct other)
+ {
+ return this.A == other.A && this.B == other.B && this.C.Equals(other.C);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is CustomStruct other && this.Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hashCode = this.A.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.B.GetHashCode();
+ hashCode = (hashCode * 397) ^ this.C.GetHashCode();
+ return hashCode;
+ }
+ }
+ }
+
+ public static readonly TheoryData LenthValues = new TheoryData { 0, 1, 7, 1023, 1024 };
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void HasCorrectLength_byte(int desiredLength)
+ {
+ this.TestHasCorrectLength(desiredLength);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void HasCorrectLength_float(int desiredLength)
+ {
+ this.TestHasCorrectLength(desiredLength);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void HasCorrectLength_CustomStruct(int desiredLength)
+ {
+ this.TestHasCorrectLength(desiredLength);
+ }
+
+ private void TestHasCorrectLength(int desiredLength)
+ where T : struct
+ {
+ using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength))
+ {
+ Assert.Equal(desiredLength, buffer.GetSpan().Length);
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void CanAllocateCleanBuffer_byte(int desiredLength)
+ {
+ this.TestCanAllocateCleanBuffer(desiredLength, false);
+ this.TestCanAllocateCleanBuffer(desiredLength, true);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void CanAllocateCleanBuffer_double(int desiredLength)
+ {
+ this.TestCanAllocateCleanBuffer(desiredLength);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void CanAllocateCleanBuffer_CustomStruct(int desiredLength)
+ {
+ this.TestCanAllocateCleanBuffer(desiredLength);
+ }
+
+ private IMemoryOwner Allocate(int desiredLength, AllocationOptions options, bool managedByteBuffer)
+ where T : struct
+ {
+ if (managedByteBuffer)
+ {
+ if (!(this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength, options) is IMemoryOwner buffer))
+ {
+ throw new InvalidOperationException("typeof(T) != typeof(byte)");
+ }
+
+ return buffer;
+ }
+
+ return this.MemoryAllocator.Allocate(desiredLength, options);
+ }
+
+ private void TestCanAllocateCleanBuffer(int desiredLength, bool testManagedByteBuffer = false)
+ where T : struct, IEquatable
+ {
+ ReadOnlySpan expected = new T[desiredLength];
+
+ for (int i = 0; i < 10; i++)
+ {
+ using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.Clean, testManagedByteBuffer))
+ {
+ Assert.True(buffer.GetSpan().SequenceEqual(expected));
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void SpanPropertyIsAlwaysTheSame_int(int desiredLength)
+ {
+ this.TestSpanPropertyIsAlwaysTheSame(desiredLength);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void SpanPropertyIsAlwaysTheSame_byte(int desiredLength)
+ {
+ this.TestSpanPropertyIsAlwaysTheSame(desiredLength, false);
+ this.TestSpanPropertyIsAlwaysTheSame(desiredLength, true);
+ }
+
+ private void TestSpanPropertyIsAlwaysTheSame(int desiredLength, bool testManagedByteBuffer = false)
+ where T : struct
+ {
+ using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer))
+ {
+ ref T a = ref MemoryMarshal.GetReference(buffer.GetSpan());
+ ref T b = ref MemoryMarshal.GetReference(buffer.GetSpan());
+ ref T c = ref MemoryMarshal.GetReference(buffer.GetSpan());
+
+ Assert.True(Unsafe.AreSame(ref a, ref b));
+ Assert.True(Unsafe.AreSame(ref b, ref c));
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void WriteAndReadElements_float(int desiredLength)
+ {
+ this.TestWriteAndReadElements(desiredLength, x => x * 1.2f);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void WriteAndReadElements_byte(int desiredLength)
+ {
+ this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), false);
+ this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), true);
+ }
+
+ private void TestWriteAndReadElements(int desiredLength, Func getExpectedValue, bool testManagedByteBuffer = false)
+ where T : struct
+ {
+ using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer))
+ {
+ T[] expectedVals = new T[buffer.Length()];
+
+ for (int i = 0; i < buffer.Length(); i++)
+ {
+ Span span = buffer.GetSpan();
+ expectedVals[i] = getExpectedValue(i);
+ span[i] = expectedVals[i];
+ }
+
+ for (int i = 0; i < buffer.Length(); i++)
+ {
+ Span span = buffer.GetSpan();
+ Assert.Equal(expectedVals[i], span[i]);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void IndexingSpan_WhenOutOfRange_Throws_byte(int desiredLength)
+ {
+ this.TestIndexOutOfRangeShouldThrow(desiredLength, false);
+ this.TestIndexOutOfRangeShouldThrow(desiredLength, true);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void IndexingSpan_WhenOutOfRange_Throws_long(int desiredLength)
+ {
+ this.TestIndexOutOfRangeShouldThrow(desiredLength);
+ }
+
+ [Theory]
+ [MemberData(nameof(LenthValues))]
+ public void IndexingSpan_WhenOutOfRange_Throws_CustomStruct(int desiredLength)
+ {
+ this.TestIndexOutOfRangeShouldThrow(desiredLength);
+ }
+
+ private T TestIndexOutOfRangeShouldThrow(int desiredLength, bool testManagedByteBuffer = false)
+ where T : struct, IEquatable
+ {
+ var dummy = default(T);
+
+ using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer))
+ {
+ Assert.ThrowsAny(
+ () =>
+ {
+ Span span = buffer.GetSpan();
+ dummy = span[desiredLength];
+ });
+
+ Assert.ThrowsAny(
+ () =>
+ {
+ Span span = buffer.GetSpan();
+ dummy = span[desiredLength + 1];
+ });
+
+ Assert.ThrowsAny(
+ () =>
+ {
+ Span span = buffer.GetSpan();
+ dummy = span[desiredLength + 42];
+ });
+ }
+
+ return dummy;
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(7)]
+ [InlineData(1024)]
+ [InlineData(6666)]
+ public void ManagedByteBuffer_ArrayIsCorrect(int desiredLength)
+ {
+ using (IManagedByteBuffer buffer = this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength))
+ {
+ ref byte array0 = ref buffer.Array[0];
+ ref byte span0 = ref buffer.GetReference();
+
+ Assert.True(Unsafe.AreSame(ref span0, ref array0));
+ Assert.True(buffer.Array.Length >= buffer.GetSpan().Length);
+ }
+ }
+
+ [Fact]
+ public void GetMemory_ReturnsValidMemory()
+ {
+ using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(42))
+ {
+ Span span0 = buffer.GetSpan();
+ span0[10].A = 30;
+ Memory memory = buffer.Memory;
+
+ Assert.Equal(42, memory.Length);
+ Span span1 = memory.Span;
+
+ Assert.Equal(42, span1.Length);
+ Assert.Equal(30, span1[10].A);
+ }
+ }
+
+ [Fact]
+ public unsafe void GetMemory_ResultIsPinnable()
+ {
+ using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(42))
+ {
+ Span span0 = buffer.GetSpan();
+ span0[10] = 30;
+
+ Memory memory = buffer.Memory;
+
+ using (MemoryHandle h = memory.Pin())
+ {
+ int* ptr = (int*)h.Pointer;
+ Assert.Equal(30, ptr[10]);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryAllocatorTests.cs b/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryAllocatorTests.cs
new file mode 100644
index 0000000000..a22e9e375d
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Memory/SimpleGcMemoryAllocatorTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace SixLabors.Memory.Tests
+{
+ public class SimpleGcMemoryAllocatorTests
+ {
+ public class BufferTests : BufferTestSuite
+ {
+ public BufferTests()
+ : base(new SimpleGcMemoryAllocator())
+ {
+ }
+ }
+
+ protected SimpleGcMemoryAllocator MemoryAllocator { get; } = new SimpleGcMemoryAllocator();
+
+ [Theory]
+ [InlineData(-1)]
+ public void Allocate_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
+ {
+ ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length));
+ Assert.Equal("length", ex.ParamName);
+ }
+
+ [Theory]
+ [InlineData(-1)]
+ public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
+ {
+ ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.AllocateManagedByteBuffer(length));
+ Assert.Equal("length", ex.ParamName);
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = 512)]
+ private struct BigStruct
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Primitives/PointFTests.cs b/tests/SixLabors.Core.Tests/Primitives/PointFTests.cs
new file mode 100644
index 0000000000..f78a18fc13
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Primitives/PointFTests.cs
@@ -0,0 +1,210 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Globalization;
+using System.Numerics;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Xunit;
+
+namespace SixLabors.Primitives.Tests
+{
+ public class PointFTests
+ {
+ [Fact]
+ public void CanReinterpretCastFromVector2()
+ {
+ var vector = new Vector2(1, 2);
+
+ PointF point = Unsafe.As(ref vector);
+
+ Assert.Equal(vector.X, point.X);
+ Assert.Equal(vector.Y, point.Y);
+ }
+
+ [Fact]
+ public void DefaultConstructorTest()
+ {
+ Assert.Equal(default, PointF.Empty);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MaxValue)]
+ [InlineData(0.0, 0.0)]
+ public void NonDefaultConstructorTest(float x, float y)
+ {
+ var p1 = new PointF(x, y);
+
+ Assert.Equal(x, p1.X);
+ Assert.Equal(y, p1.Y);
+ }
+
+ [Fact]
+ public void IsEmptyDefaultsTest()
+ {
+ Assert.True(PointF.Empty.IsEmpty);
+ Assert.True(default(PointF).IsEmpty);
+ Assert.True(new PointF(0, 0).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ public void IsEmptyRandomTest(float x, float y)
+ {
+ Assert.False(new PointF(x, y).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void CoordinatesTest(float x, float y)
+ {
+ var p = new PointF(x, y);
+ Assert.Equal(x, p.X);
+ Assert.Equal(y, p.Y);
+
+ p.X = 10;
+ Assert.Equal(10, p.X);
+
+ p.Y = -10.123f;
+ Assert.Equal(-10.123, p.Y, 3);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(float.MinValue, float.MaxValue, int.MinValue, int.MaxValue)]
+ [InlineData(0, 0, 0, 0)]
+ public void ArithmeticTestWithSize(float x, float y, int x1, int y1)
+ {
+ var p = new PointF(x, y);
+ var s = new Size(x1, y1);
+
+ var addExpected = new PointF(x + x1, y + y1);
+ var subExpected = new PointF(x - x1, y - y1);
+ Assert.Equal(addExpected, p + s);
+ Assert.Equal(subExpected, p - s);
+ Assert.Equal(addExpected, PointF.Add(p, s));
+ Assert.Equal(subExpected, PointF.Subtract(p, s));
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void ArithmeticTestWithSizeF(float x, float y)
+ {
+ var p = new PointF(x, y);
+ var s = new SizeF(y, x);
+
+ var addExpected = new PointF(x + y, y + x);
+ var subExpected = new PointF(x - y, y - x);
+ Assert.Equal(addExpected, p + s);
+ Assert.Equal(subExpected, p - s);
+ Assert.Equal(addExpected, PointF.Add(p, s));
+ Assert.Equal(subExpected, PointF.Subtract(p, s));
+ }
+
+ [Fact]
+ public void RotateTest()
+ {
+ var p = new PointF(13, 17);
+ Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty);
+
+ var pout = PointF.Transform(p, matrix);
+
+ Assert.Equal(new PointF(-2.82842732F, 21.2132034F), pout);
+ }
+
+ [Fact]
+ public void SkewTest()
+ {
+ var p = new PointF(13, 17);
+ Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, PointF.Empty);
+
+ var pout = PointF.Transform(p, matrix);
+ Assert.Equal(new PointF(30, 30), pout);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void EqualityTest(float x, float y)
+ {
+ var pLeft = new PointF(x, y);
+ var pRight = new PointF(y, x);
+
+ if (x == y)
+ {
+ Assert.True(pLeft == pRight);
+ Assert.False(pLeft != pRight);
+ Assert.True(pLeft.Equals(pRight));
+ Assert.True(pLeft.Equals((object)pRight));
+ Assert.Equal(pLeft.GetHashCode(), pRight.GetHashCode());
+ return;
+ }
+
+ Assert.True(pLeft != pRight);
+ Assert.False(pLeft == pRight);
+ Assert.False(pLeft.Equals(pRight));
+ Assert.False(pLeft.Equals((object)pRight));
+ }
+
+ [Fact]
+ public void EqualityTest_NotPointF()
+ {
+ var point = new PointF(0, 0);
+ Assert.False(point.Equals(null));
+ Assert.False(point.Equals(0));
+
+ // If PointF implements IEquatable (e.g. in .NET Core), then structs that are implicitly
+ // convertible to var can potentially be equal.
+ // See https://github.com/dotnet/corefx/issues/5255.
+ bool expectsImplicitCastToPointF = typeof(IEquatable).IsAssignableFrom(point.GetType());
+ Assert.Equal(expectsImplicitCastToPointF, point.Equals(new Point(0, 0)));
+
+ Assert.False(point.Equals((object)new Point(0, 0))); // No implicit cast
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ var point = new PointF(10, 10);
+ Assert.Equal(point.GetHashCode(), new PointF(10, 10).GetHashCode());
+ Assert.NotEqual(point.GetHashCode(), new PointF(20, 10).GetHashCode());
+ Assert.NotEqual(point.GetHashCode(), new PointF(10, 20).GetHashCode());
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var p = new PointF(5.1F, -5.123F);
+ Assert.Equal(string.Format(CultureInfo.CurrentCulture, "PointF [ X={0}, Y={1} ]", p.X, p.Y), p.ToString());
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void DeconstructTest(float x, float y)
+ {
+ PointF p = new PointF(x, y);
+
+ (float deconstructedX, float deconstructedY) = p;
+
+ Assert.Equal(x, deconstructedX);
+ Assert.Equal(y, deconstructedY);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Primitives/PointTests.cs b/tests/SixLabors.Core.Tests/Primitives/PointTests.cs
new file mode 100644
index 0000000000..7ad7f0b62c
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Primitives/PointTests.cs
@@ -0,0 +1,258 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Globalization;
+using System.Numerics;
+using Xunit;
+
+namespace SixLabors.Primitives.Tests
+{
+ public class PointTests
+ {
+ [Fact]
+ public void DefaultConstructorTest()
+ {
+ Assert.Equal(default, Point.Empty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void NonDefaultConstructorTest(int x, int y)
+ {
+ var p1 = new Point(x, y);
+ var p2 = new Point(new Size(x, y));
+
+ Assert.Equal(p1, p2);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue)]
+ [InlineData(int.MinValue)]
+ [InlineData(0)]
+ public void SingleIntConstructorTest(int x)
+ {
+ var p1 = new Point(x);
+ var p2 = new Point(unchecked((short)(x & 0xFFFF)), unchecked((short)((x >> 16) & 0xFFFF)));
+
+ Assert.Equal(p1, p2);
+ }
+
+ [Fact]
+ public void IsEmptyDefaultsTest()
+ {
+ Assert.True(Point.Empty.IsEmpty);
+ Assert.True(default(Point).IsEmpty);
+ Assert.True(new Point(0, 0).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ public void IsEmptyRandomTest(int x, int y)
+ {
+ Assert.False(new Point(x, y).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void CoordinatesTest(int x, int y)
+ {
+ var p = new Point(x, y);
+ Assert.Equal(x, p.X);
+ Assert.Equal(y, p.Y);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void PointFConversionTest(int x, int y)
+ {
+ PointF p = new Point(x, y);
+ Assert.Equal(new PointF(x, y), p);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void SizeConversionTest(int x, int y)
+ {
+ var sz = (Size)new Point(x, y);
+ Assert.Equal(new Size(x, y), sz);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void ArithmeticTest(int x, int y)
+ {
+ Point addExpected, subExpected, p = new Point(x, y);
+ var s = new Size(y, x);
+
+ unchecked
+ {
+ addExpected = new Point(x + y, y + x);
+ subExpected = new Point(x - y, y - x);
+ }
+
+ Assert.Equal(addExpected, p + s);
+ Assert.Equal(subExpected, p - s);
+ Assert.Equal(addExpected, Point.Add(p, s));
+ Assert.Equal(subExpected, Point.Subtract(p, s));
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void PointFMathematicalTest(float x, float y)
+ {
+ var pf = new PointF(x, y);
+ Point pCeiling, pTruncate, pRound;
+
+ unchecked
+ {
+ pCeiling = new Point((int)MathF.Ceiling(x), (int)MathF.Ceiling(y));
+ pTruncate = new Point((int)x, (int)y);
+ pRound = new Point((int)MathF.Round(x), (int)MathF.Round(y));
+ }
+
+ Assert.Equal(pCeiling, Point.Ceiling(pf));
+ Assert.Equal(pRound, Point.Round(pf));
+ Assert.Equal(pTruncate, (Point)pf);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void OffsetTest(int x, int y)
+ {
+ var p1 = new Point(x, y);
+ var p2 = new Point(y, x);
+
+ p1.Offset(p2);
+
+ Assert.Equal(unchecked(p2.X + p2.Y), p1.X);
+ Assert.Equal(p1.X, p1.Y);
+
+ p2.Offset(x, y);
+ Assert.Equal(p1, p2);
+ }
+
+ [Fact]
+ public void RotateTest()
+ {
+ var p = new Point(13, 17);
+ Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
+
+ var pout = Point.Transform(p, matrix);
+
+ Assert.Equal(new Point(-3, 21), pout);
+ }
+
+ [Fact]
+ public void SkewTest()
+ {
+ var p = new Point(13, 17);
+ Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, Point.Empty);
+
+ var pout = Point.Transform(p, matrix);
+ Assert.Equal(new Point(30, 30), pout);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void EqualityTest(int x, int y)
+ {
+ var p1 = new Point(x, y);
+ var p2 = new Point((x / 2) - 1, (y / 2) - 1);
+ var p3 = new Point(x, y);
+
+ Assert.True(p1 == p3);
+ Assert.True(p1 != p2);
+ Assert.True(p2 != p3);
+
+ Assert.True(p1.Equals(p3));
+ Assert.False(p1.Equals(p2));
+ Assert.False(p2.Equals(p3));
+
+ Assert.True(p1.Equals((object)p3));
+ Assert.False(p1.Equals((object)p2));
+ Assert.False(p2.Equals((object)p3));
+
+ Assert.Equal(p1.GetHashCode(), p3.GetHashCode());
+ }
+
+ [Fact]
+ public void EqualityTest_NotPoint()
+ {
+ var point = new Point(0, 0);
+ Assert.False(point.Equals(null));
+ Assert.False(point.Equals(0));
+ Assert.False(point.Equals(new PointF(0, 0)));
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ var point = new Point(10, 10);
+ Assert.Equal(point.GetHashCode(), new Point(10, 10).GetHashCode());
+ Assert.NotEqual(point.GetHashCode(), new Point(20, 10).GetHashCode());
+ Assert.NotEqual(point.GetHashCode(), new Point(10, 20).GetHashCode());
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(1, -2, 3, -4)]
+ public void ConversionTest(int x, int y, int width, int height)
+ {
+ var rect = new Rectangle(x, y, width, height);
+ RectangleF rectF = rect;
+ Assert.Equal(x, rectF.X);
+ Assert.Equal(y, rectF.Y);
+ Assert.Equal(width, rectF.Width);
+ Assert.Equal(height, rectF.Height);
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var p = new Point(5, -5);
+ Assert.Equal(string.Format(CultureInfo.CurrentCulture, "Point [ X={0}, Y={1} ]", p.X, p.Y), p.ToString());
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void DeconstructTest(int x, int y)
+ {
+ Point p = new Point(x, y);
+
+ (int deconstructedX, int deconstructedY) = p;
+
+ Assert.Equal(x, deconstructedX);
+ Assert.Equal(y, deconstructedY);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Primitives/RectangleFTests.cs b/tests/SixLabors.Core.Tests/Primitives/RectangleFTests.cs
new file mode 100644
index 0000000000..f83f6435a2
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Primitives/RectangleFTests.cs
@@ -0,0 +1,286 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Globalization;
+using System.Reflection;
+using Xunit;
+
+namespace SixLabors.Primitives.Tests
+{
+ ///
+ /// Tests the struct.
+ ///
+ public class RectangleFTests
+ {
+ [Fact]
+ public void DefaultConstructorTest()
+ {
+ Assert.Equal(default, RectangleF.Empty);
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, 0, 0, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void NonDefaultConstructorTest(float x, float y, float width, float height)
+ {
+ var rect1 = new RectangleF(x, y, width, height);
+ var p = new PointF(x, y);
+ var s = new SizeF(width, height);
+ var rect2 = new RectangleF(p, s);
+
+ Assert.Equal(rect1, rect2);
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, 0, 0, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void FromLTRBTest(float left, float top, float right, float bottom)
+ {
+ var expected = new RectangleF(left, top, right - left, bottom - top);
+ var actual = RectangleF.FromLTRB(left, top, right, bottom);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, 0, 0, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void DimensionsTest(float x, float y, float width, float height)
+ {
+ var rect = new RectangleF(x, y, width, height);
+ var p = new PointF(x, y);
+ var s = new SizeF(width, height);
+
+ Assert.Equal(p, rect.Location);
+ Assert.Equal(s, rect.Size);
+ Assert.Equal(x, rect.X);
+ Assert.Equal(y, rect.Y);
+ Assert.Equal(width, rect.Width);
+ Assert.Equal(height, rect.Height);
+ Assert.Equal(x, rect.Left);
+ Assert.Equal(y, rect.Top);
+ Assert.Equal(x + width, rect.Right);
+ Assert.Equal(y + height, rect.Bottom);
+ }
+
+ [Fact]
+ public void IsEmptyTest()
+ {
+ Assert.True(RectangleF.Empty.IsEmpty);
+ Assert.True(default(RectangleF).IsEmpty);
+ Assert.True(new RectangleF(1, -2, -10, 10).IsEmpty);
+ Assert.True(new RectangleF(1, -2, 10, -10).IsEmpty);
+ Assert.True(new RectangleF(1, -2, 0, 0).IsEmpty);
+
+ Assert.False(new RectangleF(0, 0, 10, 10).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(float.MaxValue, float.MinValue)]
+ public void LocationSetTest(float x, float y)
+ {
+ var point = new PointF(x, y);
+ var rect = new RectangleF(10, 10, 10, 10) { Location = point };
+ Assert.Equal(point, rect.Location);
+ Assert.Equal(point.X, rect.X);
+ Assert.Equal(point.Y, rect.Y);
+ }
+
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(float.MaxValue, float.MinValue)]
+ public void SizeSetTest(float x, float y)
+ {
+ var size = new SizeF(x, y);
+ var rect = new RectangleF(10, 10, 10, 10) { Size = size };
+ Assert.Equal(size, rect.Size);
+ Assert.Equal(size.Width, rect.Width);
+ Assert.Equal(size.Height, rect.Height);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, 0, 0, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void EqualityTest(float x, float y, float width, float height)
+ {
+ var rect1 = new RectangleF(x, y, width, height);
+ var rect2 = new RectangleF(width, height, x, y);
+
+ Assert.True(rect1 != rect2);
+ Assert.False(rect1 == rect2);
+ Assert.False(rect1.Equals(rect2));
+ Assert.False(rect1.Equals((object)rect2));
+ }
+
+ [Fact]
+ public void EqualityTestNotRectangleF()
+ {
+ var rectangle = new RectangleF(0, 0, 0, 0);
+ Assert.False(rectangle.Equals(null));
+ Assert.False(rectangle.Equals(0));
+
+ // If RectangleF implements IEquatable (e.g. in .NET Core), then classes that are implicitly
+ // convertible to RectangleF can potentially be equal.
+ // See https://github.com/dotnet/corefx/issues/5255.
+ bool expectsImplicitCastToRectangleF = typeof(IEquatable).IsAssignableFrom(rectangle.GetType());
+ Assert.Equal(expectsImplicitCastToRectangleF, rectangle.Equals(new Rectangle(0, 0, 0, 0)));
+
+ Assert.False(rectangle.Equals((object)new Rectangle(0, 0, 0, 0))); // No implicit cast
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ var rect1 = new RectangleF(10, 10, 10, 10);
+ var rect2 = new RectangleF(10, 10, 10, 10);
+ Assert.Equal(rect1.GetHashCode(), rect2.GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new RectangleF(20, 10, 10, 10).GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new RectangleF(10, 20, 10, 10).GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new RectangleF(10, 10, 20, 10).GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new RectangleF(10, 10, 10, 20).GetHashCode());
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void ContainsTest(float x, float y, float width, float height)
+ {
+ var rect = new RectangleF(x, y, width, height);
+ float x1 = (x + width) / 2;
+ float y1 = (y + height) / 2;
+ var p = new PointF(x1, y1);
+ var r = new RectangleF(x1, y1, width / 2, height / 2);
+
+ Assert.False(rect.Contains(x1, y1));
+ Assert.False(rect.Contains(p));
+ Assert.False(rect.Contains(r));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(float.MaxValue / 2, float.MinValue / 2, float.MinValue / 2, float.MaxValue / 2)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void InflateTest(float x, float y, float width, float height)
+ {
+ var rect = new RectangleF(x, y, width, height);
+ var inflatedRect = new RectangleF(x - width, y - height, width + (2 * width), height + (2 * height));
+
+ rect.Inflate(width, height);
+ Assert.Equal(inflatedRect, rect);
+
+ var s = new SizeF(x, y);
+ inflatedRect = RectangleF.Inflate(rect, x, y);
+
+ rect.Inflate(s);
+ Assert.Equal(inflatedRect, rect);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue, float.MaxValue / 2, float.MinValue / 2)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void IntersectTest(float x, float y, float width, float height)
+ {
+ var rect1 = new RectangleF(x, y, width, height);
+ var rect2 = new RectangleF(y, x, width, height);
+ var expectedRect = RectangleF.Intersect(rect1, rect2);
+ rect1.Intersect(rect2);
+ Assert.Equal(expectedRect, rect1);
+ Assert.False(rect1.IntersectsWith(expectedRect));
+ }
+
+ [Fact]
+ public void IntersectIntersectingRectsTest()
+ {
+ var rect1 = new RectangleF(0, 0, 5, 5);
+ var rect2 = new RectangleF(1, 1, 3, 3);
+ var expected = new RectangleF(1, 1, 3, 3);
+
+ Assert.Equal(expected, RectangleF.Intersect(rect1, rect2));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, 0, 0, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void UnionTest(float x, float y, float width, float height)
+ {
+ var a = new RectangleF(x, y, width, height);
+ var b = new RectangleF(width, height, x, y);
+
+ float x1 = Math.Min(a.X, b.X);
+ float x2 = Math.Max(a.X + a.Width, b.X + b.Width);
+ float y1 = Math.Min(a.Y, b.Y);
+ float y2 = Math.Max(a.Y + a.Height, b.Y + b.Height);
+
+ var expectedRectangle = new RectangleF(x1, y1, x2 - x1, y2 - y1);
+
+ Assert.Equal(expectedRectangle, RectangleF.Union(a, b));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, 0, 0, float.MaxValue)]
+ [InlineData(0, float.MinValue, float.MaxValue, 0)]
+ public void OffsetTest(float x, float y, float width, float height)
+ {
+ var r1 = new RectangleF(x, y, width, height);
+ var expectedRect = new RectangleF(x + width, y + height, width, height);
+ var p = new PointF(width, height);
+
+ r1.Offset(p);
+ Assert.Equal(expectedRect, r1);
+
+ expectedRect.Offset(p);
+ r1.Offset(width, height);
+ Assert.Equal(expectedRect, r1);
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var r = new RectangleF(5, 5.1F, 1.3F, 1);
+ Assert.Equal(string.Format(CultureInfo.CurrentCulture, "RectangleF [ X={0}, Y={1}, Width={2}, Height={3} ]", r.X, r.Y, r.Width, r.Height), r.ToString());
+ }
+
+ [Theory]
+ [InlineData(float.MinValue, float.MaxValue, float.MaxValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MaxValue, float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue, float.MaxValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MinValue, float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MinValue, float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue, float.MaxValue, float.MaxValue)]
+ [InlineData(float.MaxValue, float.MaxValue, float.MaxValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, float.MaxValue, float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MaxValue)]
+ [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MaxValue)]
+ [InlineData(float.MaxValue, float.MinValue, float.MinValue, float.MinValue)]
+ [InlineData(0, 0, 0, 0)]
+ public void DeconstructTest(float x, float y, float width, float height)
+ {
+ RectangleF r = new RectangleF(x, y, width, height);
+
+ (float dx, float dy, float dw, float dh) = r;
+
+ Assert.Equal(x, dx);
+ Assert.Equal(y, dy);
+ Assert.Equal(width, dw);
+ Assert.Equal(height, dh);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Primitives/RectangleTests.cs b/tests/SixLabors.Core.Tests/Primitives/RectangleTests.cs
new file mode 100644
index 0000000000..84b15d36e0
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Primitives/RectangleTests.cs
@@ -0,0 +1,336 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Globalization;
+using Xunit;
+
+namespace SixLabors.Primitives.Tests
+{
+ ///
+ /// Tests the struct.
+ ///
+ public class RectangleTests
+ {
+ [Fact]
+ public void DefaultConstructorTest()
+ {
+ Assert.Equal(default, Rectangle.Empty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, 0, int.MinValue, 0)]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(0, int.MinValue, 0, int.MaxValue)]
+ public void NonDefaultConstructorTest(int x, int y, int width, int height)
+ {
+ var rect1 = new Rectangle(x, y, width, height);
+ var rect2 = new Rectangle(new Point(x, y), new Size(width, height));
+
+ Assert.Equal(rect1, rect2);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, 0, int.MinValue, 0)]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(0, int.MinValue, 0, int.MaxValue)]
+ public void FromLTRBTest(int left, int top, int right, int bottom)
+ {
+ var rect1 = new Rectangle(left, top, unchecked(right - left), unchecked(bottom - top));
+ var rect2 = Rectangle.FromLTRB(left, top, right, bottom);
+
+ Assert.Equal(rect1, rect2);
+ }
+
+ [Fact]
+ public void EmptyTest()
+ {
+ Assert.True(Rectangle.Empty.IsEmpty);
+ Assert.True(default(Rectangle).IsEmpty);
+ Assert.True(new Rectangle(0, 0, 0, 0).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, 0, int.MinValue, 0)]
+ [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)]
+ [InlineData(0, int.MinValue, 0, int.MaxValue)]
+ public void NonEmptyTest(int x, int y, int width, int height)
+ {
+ Assert.False(new Rectangle(x, y, width, height).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, 0, int.MinValue, 0)]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(0, int.MinValue, 0, int.MaxValue)]
+ [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)]
+ public void DimensionsTest(int x, int y, int width, int height)
+ {
+ var rect = new Rectangle(x, y, width, height);
+ Assert.Equal(new Point(x, y), rect.Location);
+ Assert.Equal(new Size(width, height), rect.Size);
+
+ Assert.Equal(x, rect.X);
+ Assert.Equal(y, rect.Y);
+ Assert.Equal(width, rect.Width);
+ Assert.Equal(height, rect.Height);
+ Assert.Equal(x, rect.Left);
+ Assert.Equal(y, rect.Top);
+ Assert.Equal(unchecked(x + width), rect.Right);
+ Assert.Equal(unchecked(y + height), rect.Bottom);
+
+ var p = new Point(width, height);
+ var s = new Size(x, y);
+ rect.Location = p;
+ rect.Size = s;
+
+ Assert.Equal(p, rect.Location);
+ Assert.Equal(s, rect.Size);
+
+ Assert.Equal(width, rect.X);
+ Assert.Equal(height, rect.Y);
+ Assert.Equal(x, rect.Width);
+ Assert.Equal(y, rect.Height);
+ Assert.Equal(width, rect.Left);
+ Assert.Equal(height, rect.Top);
+ Assert.Equal(unchecked(x + width), rect.Right);
+ Assert.Equal(unchecked(y + height), rect.Bottom);
+ }
+
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(int.MaxValue, int.MinValue)]
+ public void LocationSetTest(int x, int y)
+ {
+ var point = new Point(x, y);
+ var rect = new Rectangle(10, 10, 10, 10) { Location = point };
+ Assert.Equal(point, rect.Location);
+ Assert.Equal(point.X, rect.X);
+ Assert.Equal(point.Y, rect.Y);
+ }
+
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(int.MaxValue, int.MinValue)]
+ public void SizeSetTest(int x, int y)
+ {
+ var size = new Size(x, y);
+ var rect = new Rectangle(10, 10, 10, 10) { Size = size };
+ Assert.Equal(size, rect.Size);
+ Assert.Equal(size.Width, rect.Width);
+ Assert.Equal(size.Height, rect.Height);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, 0, int.MinValue, 0)]
+ [InlineData(0, int.MinValue, 0, int.MaxValue)]
+ [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)]
+ public void EqualityTest(int x, int y, int width, int height)
+ {
+ var rect1 = new Rectangle(x, y, width, height);
+ var rect2 = new Rectangle(width / 2, height / 2, x, y);
+
+ Assert.True(rect1 != rect2);
+ Assert.False(rect1 == rect2);
+ Assert.False(rect1.Equals(rect2));
+ Assert.False(rect1.Equals((object)rect2));
+ }
+
+ [Fact]
+ public void EqualityTestNotRectangle()
+ {
+ var rectangle = new Rectangle(0, 0, 0, 0);
+ Assert.False(rectangle.Equals(null));
+ Assert.False(rectangle.Equals(0));
+ Assert.False(rectangle.Equals(new RectangleF(0, 0, 0, 0)));
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ var rect1 = new Rectangle(10, 10, 10, 10);
+ var rect2 = new Rectangle(10, 10, 10, 10);
+ Assert.Equal(rect1.GetHashCode(), rect2.GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new Rectangle(20, 10, 10, 10).GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new Rectangle(10, 20, 10, 10).GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new Rectangle(10, 10, 20, 10).GetHashCode());
+ Assert.NotEqual(rect1.GetHashCode(), new Rectangle(10, 10, 10, 20).GetHashCode());
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue, float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MaxValue, float.MinValue, float.MaxValue)]
+ [InlineData(0, 0, 0, 0)]
+ public void RectangleFConversionTest(float x, float y, float width, float height)
+ {
+ var rect = new RectangleF(x, y, width, height);
+ Rectangle rCeiling, rTruncate, rRound;
+
+ unchecked
+ {
+ rCeiling = new Rectangle(
+ (int)Math.Ceiling(x),
+ (int)Math.Ceiling(y),
+ (int)Math.Ceiling(width),
+ (int)Math.Ceiling(height));
+
+ rTruncate = new Rectangle((int)x, (int)y, (int)width, (int)height);
+
+ rRound = new Rectangle(
+ (int)Math.Round(x),
+ (int)Math.Round(y),
+ (int)Math.Round(width),
+ (int)Math.Round(height));
+ }
+
+ Assert.Equal(rCeiling, Rectangle.Ceiling(rect));
+ Assert.Equal(rTruncate, Rectangle.Truncate(rect));
+ Assert.Equal(rRound, Rectangle.Round(rect));
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(0, int.MinValue, int.MaxValue, 0)]
+ public void ContainsTest(int x, int y, int width, int height)
+ {
+ var rect = new Rectangle(unchecked((2 * x) - width), unchecked((2 * y) - height), width, height);
+ var p = new Point(x, y);
+ var r = new Rectangle(x, y, width / 2, height / 2);
+
+ Assert.False(rect.Contains(x, y));
+ Assert.False(rect.Contains(p));
+ Assert.False(rect.Contains(r));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(0, int.MinValue, int.MaxValue, 0)]
+ public void InflateTest(int x, int y, int width, int height)
+ {
+ Rectangle inflatedRect, rect = new Rectangle(x, y, width, height);
+ unchecked
+ {
+ inflatedRect = new Rectangle(x - width, y - height, width + (2 * width), height + (2 * height));
+ }
+
+ Assert.Equal(inflatedRect, Rectangle.Inflate(rect, width, height));
+
+ rect.Inflate(width, height);
+ Assert.Equal(inflatedRect, rect);
+
+ var s = new Size(x, y);
+ unchecked
+ {
+ inflatedRect = new Rectangle(rect.X - x, rect.Y - y, rect.Width + (2 * x), rect.Height + (2 * y));
+ }
+
+ rect.Inflate(s);
+ Assert.Equal(inflatedRect, rect);
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(0, int.MinValue, int.MaxValue, 0)]
+ public void IntersectTest(int x, int y, int width, int height)
+ {
+ var rect = new Rectangle(x, y, width, height);
+ var expectedRect = Rectangle.Intersect(rect, rect);
+ rect.Intersect(rect);
+ Assert.Equal(expectedRect, rect);
+ Assert.False(rect.IntersectsWith(expectedRect));
+ }
+
+ [Fact]
+ public void IntersectIntersectingRectsTest()
+ {
+ var rect1 = new Rectangle(0, 0, 5, 5);
+ var rect2 = new Rectangle(1, 1, 3, 3);
+ var expected = new Rectangle(1, 1, 3, 3);
+
+ Assert.Equal(expected, Rectangle.Intersect(rect1, rect2));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(int.MaxValue, 0, 0, int.MaxValue)]
+ [InlineData(0, int.MinValue, int.MaxValue, 0)]
+ public void UnionTest(int x, int y, int width, int height)
+ {
+ var a = new Rectangle(x, y, width, height);
+ var b = new Rectangle(width, height, x, y);
+
+ int x1 = Math.Min(a.X, b.X);
+ int x2 = Math.Max(a.X + a.Width, b.X + b.Width);
+ int y1 = Math.Min(a.Y, b.Y);
+ int y2 = Math.Max(a.Y + a.Height, b.Y + b.Height);
+
+ var expectedRectangle = new Rectangle(x1, y1, x2 - x1, y2 - y1);
+
+ Assert.Equal(expectedRectangle, Rectangle.Union(a, b));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(int.MaxValue, 0, 0, int.MaxValue)]
+ [InlineData(0, int.MinValue, int.MaxValue, 0)]
+ public void OffsetTest(int x, int y, int width, int height)
+ {
+ var r1 = new Rectangle(x, y, width, height);
+ var expectedRect = new Rectangle(x + width, y + height, width, height);
+ var p = new Point(width, height);
+
+ r1.Offset(p);
+ Assert.Equal(expectedRect, r1);
+
+ expectedRect.Offset(p);
+ r1.Offset(width, height);
+ Assert.Equal(expectedRect, r1);
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var r = new Rectangle(5, -5, 0, 1);
+ Assert.Equal(string.Format(CultureInfo.CurrentCulture, "Rectangle [ X={0}, Y={1}, Width={2}, Height={3} ]", r.X, r.Y, r.Width, r.Height), r.ToString());
+ }
+
+ [Theory]
+ [InlineData(int.MinValue, int.MaxValue, int.MaxValue, int.MaxValue)]
+ [InlineData(int.MinValue, int.MaxValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MaxValue)]
+ [InlineData(int.MinValue, int.MaxValue, int.MinValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue, int.MaxValue, int.MaxValue)]
+ [InlineData(int.MinValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(int.MinValue, int.MinValue, int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue, int.MaxValue, int.MaxValue)]
+ [InlineData(int.MaxValue, int.MaxValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue, int.MinValue, int.MaxValue)]
+ [InlineData(int.MaxValue, int.MaxValue, int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MaxValue)]
+ [InlineData(int.MaxValue, int.MinValue, int.MaxValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MaxValue)]
+ [InlineData(int.MaxValue, int.MinValue, int.MinValue, int.MinValue)]
+ [InlineData(0, 0, 0, 0)]
+ public void DeconstructTest(int x, int y, int width, int height)
+ {
+ var r = new Rectangle(x, y, width, height);
+
+ (int dx, int dy, int dw, int dh) = r;
+
+ Assert.Equal(x, dx);
+ Assert.Equal(y, dy);
+ Assert.Equal(width, dw);
+ Assert.Equal(height, dh);
+ }
+ }
+}
diff --git a/tests/SixLabors.Core.Tests/Primitives/SizeFTests.cs b/tests/SixLabors.Core.Tests/Primitives/SizeFTests.cs
new file mode 100644
index 0000000000..5bfc8f5ef1
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Primitives/SizeFTests.cs
@@ -0,0 +1,250 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Globalization;
+using System.Reflection;
+using Xunit;
+
+namespace SixLabors.Primitives.Tests
+{
+ public class SizeFTests
+ {
+ [Fact]
+ public void DefaultConstructorTest()
+ {
+ Assert.Equal(default, SizeF.Empty);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void NonDefaultConstructorAndDimensionsTest(float width, float height)
+ {
+ var s1 = new SizeF(width, height);
+ var p1 = new PointF(width, height);
+ var s2 = new SizeF(s1);
+
+ Assert.Equal(s1, s2);
+ Assert.Equal(s1, new SizeF(p1));
+ Assert.Equal(s2, new SizeF(p1));
+
+ Assert.Equal(width, s1.Width);
+ Assert.Equal(height, s1.Height);
+
+ s1.Width = 10;
+ Assert.Equal(10, s1.Width);
+
+ s1.Height = -10.123f;
+ Assert.Equal(-10.123, s1.Height, 3);
+ }
+
+ [Fact]
+ public void IsEmptyDefaultsTest()
+ {
+ Assert.True(SizeF.Empty.IsEmpty);
+ Assert.True(default(SizeF).IsEmpty);
+ Assert.True(new SizeF(0, 0).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ public void IsEmptyRandomTest(float width, float height)
+ {
+ Assert.False(new SizeF(width, height).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void ArithmeticTest(float width, float height)
+ {
+ var s1 = new SizeF(width, height);
+ var s2 = new SizeF(height, width);
+ var addExpected = new SizeF(width + height, width + height);
+ var subExpected = new SizeF(width - height, height - width);
+
+ Assert.Equal(addExpected, s1 + s2);
+ Assert.Equal(addExpected, SizeF.Add(s1, s2));
+
+ Assert.Equal(subExpected, s1 - s2);
+ Assert.Equal(subExpected, SizeF.Subtract(s1, s2));
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void EqualityTest(float width, float height)
+ {
+ var sLeft = new SizeF(width, height);
+ var sRight = new SizeF(height, width);
+
+ if (width == height)
+ {
+ Assert.True(sLeft == sRight);
+ Assert.False(sLeft != sRight);
+ Assert.True(sLeft.Equals(sRight));
+ Assert.True(sLeft.Equals((object)sRight));
+ Assert.Equal(sLeft.GetHashCode(), sRight.GetHashCode());
+ return;
+ }
+
+ Assert.True(sLeft != sRight);
+ Assert.False(sLeft == sRight);
+ Assert.False(sLeft.Equals(sRight));
+ Assert.False(sLeft.Equals((object)sRight));
+ }
+
+ [Fact]
+ public void EqualityTest_NotSizeF()
+ {
+ var size = new SizeF(0, 0);
+ Assert.False(size.Equals(null));
+ Assert.False(size.Equals(0));
+
+ // If SizeF implements IEquatable (e.g in .NET Core), then classes that are implicitly
+ // convertible to SizeF can potentially be equal.
+ // See https://github.com/dotnet/corefx/issues/5255.
+ bool expectsImplicitCastToSizeF = typeof(IEquatable).IsAssignableFrom(size.GetType());
+ Assert.Equal(expectsImplicitCastToSizeF, size.Equals(new Size(0, 0)));
+
+ Assert.False(size.Equals((object)new Size(0, 0))); // No implicit cast
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ var size = new SizeF(10, 10);
+ Assert.Equal(size.GetHashCode(), new SizeF(10, 10).GetHashCode());
+ Assert.NotEqual(size.GetHashCode(), new SizeF(20, 10).GetHashCode());
+ Assert.NotEqual(size.GetHashCode(), new SizeF(10, 20).GetHashCode());
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void ConversionTest(float width, float height)
+ {
+ var s1 = new SizeF(width, height);
+ var p1 = (PointF)s1;
+ var s2 = new Size(unchecked((int)width), unchecked((int)height));
+
+ Assert.Equal(new PointF(width, height), p1);
+ Assert.Equal(p1, (PointF)s1);
+ Assert.Equal(s2, (Size)s1);
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var sz = new SizeF(10, 5);
+ Assert.Equal(string.Format(CultureInfo.CurrentCulture, "SizeF [ Width={0}, Height={1} ]", sz.Width, sz.Height), sz.ToString());
+ }
+
+ [Theory]
+ [InlineData(1000.234f, 0.0f)]
+ [InlineData(1000.234f, 1.0f)]
+ [InlineData(1000.234f, 2400.933f)]
+ [InlineData(1000.234f, float.MaxValue)]
+ [InlineData(1000.234f, -1.0f)]
+ [InlineData(1000.234f, -2400.933f)]
+ [InlineData(1000.234f, float.MinValue)]
+ [InlineData(float.MaxValue, 0.0f)]
+ [InlineData(float.MaxValue, 1.0f)]
+ [InlineData(float.MaxValue, 2400.933f)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(float.MaxValue, -1.0f)]
+ [InlineData(float.MaxValue, -2400.933f)]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, 0.0f)]
+ [InlineData(float.MinValue, 1.0f)]
+ [InlineData(float.MinValue, 2400.933f)]
+ [InlineData(float.MinValue, float.MaxValue)]
+ [InlineData(float.MinValue, -1.0f)]
+ [InlineData(float.MinValue, -2400.933f)]
+ [InlineData(float.MinValue, float.MinValue)]
+ public void MultiplicationTest(float dimension, float multiplier)
+ {
+ SizeF sz1 = new SizeF(dimension, dimension);
+ SizeF mulExpected;
+
+ mulExpected = new SizeF(dimension * multiplier, dimension * multiplier);
+
+ Assert.Equal(mulExpected, sz1 * multiplier);
+ Assert.Equal(mulExpected, multiplier * sz1);
+ }
+
+ [Theory]
+ [InlineData(1111.1111f, 2222.2222f, 3333.3333f)]
+ public void MultiplicationTestWidthHeightMultiplier(float width, float height, float multiplier)
+ {
+ SizeF sz1 = new SizeF(width, height);
+ SizeF mulExpected;
+
+ mulExpected = new SizeF(width * multiplier, height * multiplier);
+
+ Assert.Equal(mulExpected, sz1 * multiplier);
+ Assert.Equal(mulExpected, multiplier * sz1);
+ }
+
+ [Theory]
+ [InlineData(0.0f, 1.0f)]
+ [InlineData(1.0f, 1.0f)]
+ [InlineData(-1.0f, 1.0f)]
+ [InlineData(1.0f, -1.0f)]
+ [InlineData(-1.0f, -1.0f)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MaxValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, 1.0f)]
+ [InlineData(float.MinValue, 1.0f)]
+ [InlineData(float.MaxValue, -1.0f)]
+ [InlineData(float.MinValue, -1.0f)]
+ [InlineData(float.MinValue, 0.0f)]
+ [InlineData(1.0f, float.MinValue)]
+ [InlineData(1.0f, float.MaxValue)]
+ [InlineData(-1.0f, float.MinValue)]
+ [InlineData(-1.0f, float.MaxValue)]
+ public void DivideTestSizeFloat(float dimension, float divisor)
+ {
+ SizeF size = new SizeF(dimension, dimension);
+ SizeF expected = new SizeF(dimension / divisor, dimension / divisor);
+ Assert.Equal(expected, size / divisor);
+ }
+
+ [Theory]
+ [InlineData(-111.111f, 222.222f, 333.333f)]
+ public void DivideTestSizeFloatWidthHeightDivisor(float width, float height, float divisor)
+ {
+ SizeF size = new SizeF(width, height);
+ SizeF expected = new SizeF(width / divisor, height / divisor);
+ Assert.Equal(expected, size / divisor);
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void DeconstructTest(float width, float height)
+ {
+ SizeF s = new SizeF(width, height);
+
+ (float deconstructedWidth, float deconstructedHeight) = s;
+
+ Assert.Equal(width, deconstructedWidth);
+ Assert.Equal(height, deconstructedHeight);
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/SixLabors.Core.Tests/Primitives/SizeTests.cs b/tests/SixLabors.Core.Tests/Primitives/SizeTests.cs
new file mode 100644
index 0000000000..af4b8430d1
--- /dev/null
+++ b/tests/SixLabors.Core.Tests/Primitives/SizeTests.cs
@@ -0,0 +1,379 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Globalization;
+using Xunit;
+
+namespace SixLabors.Primitives.Tests
+{
+ ///
+ /// Tests the struct.
+ ///
+ public class SizeTests
+ {
+ [Fact]
+ public void DefaultConstructorTest()
+ {
+ Assert.Equal(default, Size.Empty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void NonDefaultConstructorTest(int width, int height)
+ {
+ var s1 = new Size(width, height);
+ var s2 = new Size(new Point(width, height));
+
+ Assert.Equal(s1, s2);
+
+ s1.Width = 10;
+ Assert.Equal(10, s1.Width);
+
+ s1.Height = -10;
+ Assert.Equal(-10, s1.Height);
+ }
+
+ [Fact]
+ public void IsEmptyDefaultsTest()
+ {
+ Assert.True(Size.Empty.IsEmpty);
+ Assert.True(default(Size).IsEmpty);
+ Assert.True(new Size(0, 0).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ public void IsEmptyRandomTest(int width, int height)
+ {
+ Assert.False(new Size(width, height).IsEmpty);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void DimensionsTest(int width, int height)
+ {
+ var p = new Size(width, height);
+ Assert.Equal(width, p.Width);
+ Assert.Equal(height, p.Height);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void PointFConversionTest(int width, int height)
+ {
+ SizeF sz = new Size(width, height);
+ Assert.Equal(new SizeF(width, height), sz);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void SizeConversionTest(int width, int height)
+ {
+ var sz = (Point)new Size(width, height);
+ Assert.Equal(new Point(width, height), sz);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void ArithmeticTest(int width, int height)
+ {
+ var sz1 = new Size(width, height);
+ var sz2 = new Size(height, width);
+ Size addExpected, subExpected;
+
+ unchecked
+ {
+ addExpected = new Size(width + height, height + width);
+ subExpected = new Size(width - height, height - width);
+ }
+
+ Assert.Equal(addExpected, sz1 + sz2);
+ Assert.Equal(subExpected, sz1 - sz2);
+ Assert.Equal(addExpected, Size.Add(sz1, sz2));
+ Assert.Equal(subExpected, Size.Subtract(sz1, sz2));
+ }
+
+ [Theory]
+ [InlineData(float.MaxValue, float.MinValue)]
+ [InlineData(float.MinValue, float.MinValue)]
+ [InlineData(float.MaxValue, float.MaxValue)]
+ [InlineData(0, 0)]
+ public void PointFMathematicalTest(float width, float height)
+ {
+ var szF = new SizeF(width, height);
+ Size pCeiling, pTruncate, pRound;
+
+ unchecked
+ {
+ pCeiling = new Size((int)MathF.Ceiling(width), (int)MathF.Ceiling(height));
+ pTruncate = new Size((int)width, (int)height);
+ pRound = new Size((int)MathF.Round(width), (int)MathF.Round(height));
+ }
+
+ Assert.Equal(pCeiling, Size.Ceiling(szF));
+ Assert.Equal(pRound, Size.Round(szF));
+ Assert.Equal(pTruncate, (Size)szF);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void EqualityTest(int width, int height)
+ {
+ var p1 = new Size(width, height);
+ var p2 = new Size(unchecked(width - 1), unchecked(height - 1));
+ var p3 = new Size(width, height);
+
+ Assert.True(p1 == p3);
+ Assert.True(p1 != p2);
+ Assert.True(p2 != p3);
+
+ Assert.True(p1.Equals(p3));
+ Assert.False(p1.Equals(p2));
+ Assert.False(p2.Equals(p3));
+
+ Assert.True(p1.Equals((object)p3));
+ Assert.False(p1.Equals((object)p2));
+ Assert.False(p2.Equals((object)p3));
+
+ Assert.Equal(p1.GetHashCode(), p3.GetHashCode());
+ }
+
+ [Fact]
+ public void EqualityTest_NotSize()
+ {
+ var size = new Size(0, 0);
+ Assert.False(size.Equals(null));
+ Assert.False(size.Equals(0));
+ Assert.False(size.Equals(new SizeF(0, 0)));
+ }
+
+ [Fact]
+ public void GetHashCodeTest()
+ {
+ var size = new Size(10, 10);
+ Assert.Equal(size.GetHashCode(), new Size(10, 10).GetHashCode());
+ Assert.NotEqual(size.GetHashCode(), new Size(20, 10).GetHashCode());
+ Assert.NotEqual(size.GetHashCode(), new Size(10, 20).GetHashCode());
+ }
+
+ [Fact]
+ public void ToStringTest()
+ {
+ var sz = new Size(10, 5);
+ Assert.Equal(string.Format(CultureInfo.CurrentCulture, "Size [ Width={0}, Height={1} ]", sz.Width, sz.Height), sz.ToString());
+ }
+
+ [Theory]
+ [InlineData(1000, 0)]
+ [InlineData(1000, 1)]
+ [InlineData(1000, 2400)]
+ [InlineData(1000, int.MaxValue)]
+ [InlineData(1000, -1)]
+ [InlineData(1000, -2400)]
+ [InlineData(1000, int.MinValue)]
+ [InlineData(int.MaxValue, 0)]
+ [InlineData(int.MaxValue, 1)]
+ [InlineData(int.MaxValue, 2400)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(int.MaxValue, -1)]
+ [InlineData(int.MaxValue, -2400)]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, 0)]
+ [InlineData(int.MinValue, 1)]
+ [InlineData(int.MinValue, 2400)]
+ [InlineData(int.MinValue, int.MaxValue)]
+ [InlineData(int.MinValue, -1)]
+ [InlineData(int.MinValue, -2400)]
+ [InlineData(int.MinValue, int.MinValue)]
+ public void MultiplicationTestSizeInt(int dimension, int multiplier)
+ {
+ Size sz1 = new Size(dimension, dimension);
+ Size mulExpected;
+
+ unchecked
+ {
+ mulExpected = new Size(dimension * multiplier, dimension * multiplier);
+ }
+
+ Assert.Equal(mulExpected, sz1 * multiplier);
+ Assert.Equal(mulExpected, multiplier * sz1);
+ }
+
+ [Theory]
+ [InlineData(1000, 2000, 3000)]
+ public void MultiplicationTestSizeIntWidthHeightMultiplier(int width, int height, int multiplier)
+ {
+ Size sz1 = new Size(width, height);
+ Size mulExpected;
+
+ unchecked
+ {
+ mulExpected = new Size(width * multiplier, height * multiplier);
+ }
+
+ Assert.Equal(mulExpected, sz1 * multiplier);
+ Assert.Equal(mulExpected, multiplier * sz1);
+ }
+
+ [Theory]
+ [InlineData(1000, 0.0f)]
+ [InlineData(1000, 1.0f)]
+ [InlineData(1000, 2400.933f)]
+ [InlineData(1000, float.MaxValue)]
+ [InlineData(1000, -1.0f)]
+ [InlineData(1000, -2400.933f)]
+ [InlineData(1000, float.MinValue)]
+ [InlineData(int.MaxValue, 0.0f)]
+ [InlineData(int.MaxValue, 1.0f)]
+ [InlineData(int.MaxValue, 2400.933f)]
+ [InlineData(int.MaxValue, float.MaxValue)]
+ [InlineData(int.MaxValue, -1.0f)]
+ [InlineData(int.MaxValue, -2400.933f)]
+ [InlineData(int.MaxValue, float.MinValue)]
+ [InlineData(int.MinValue, 0.0f)]
+ [InlineData(int.MinValue, 1.0f)]
+ [InlineData(int.MinValue, 2400.933f)]
+ [InlineData(int.MinValue, float.MaxValue)]
+ [InlineData(int.MinValue, -1.0f)]
+ [InlineData(int.MinValue, -2400.933f)]
+ [InlineData(int.MinValue, float.MinValue)]
+ public void MultiplicationTestSizeFloat(int dimension, float multiplier)
+ {
+ Size sz1 = new Size(dimension, dimension);
+ SizeF mulExpected;
+
+ mulExpected = new SizeF(dimension * multiplier, dimension * multiplier);
+
+ Assert.Equal(mulExpected, sz1 * multiplier);
+ Assert.Equal(mulExpected, multiplier * sz1);
+ }
+
+ [Theory]
+ [InlineData(1000, 2000, 30.33f)]
+ public void MultiplicationTestSizeFloatWidthHeightMultiplier(int width, int height, float multiplier)
+ {
+ Size sz1 = new Size(width, height);
+ SizeF mulExpected;
+
+ mulExpected = new SizeF(width * multiplier, height * multiplier);
+
+ Assert.Equal(mulExpected, sz1 * multiplier);
+ Assert.Equal(mulExpected, multiplier * sz1);
+ }
+
+ [Fact]
+ public void DivideByZeroChecks()
+ {
+ Size size = new Size(100, 100);
+ Assert.Throws(() => size / 0);
+
+ SizeF expectedSizeF = new SizeF(float.PositiveInfinity, float.PositiveInfinity);
+ Assert.Equal(expectedSizeF, size / 0.0f);
+ }
+
+ [Theory]
+ [InlineData(0, 1)]
+ [InlineData(1, 1)]
+ [InlineData(-1, 1)]
+ [InlineData(1, -1)]
+ [InlineData(-1, -1)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MaxValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, 1)]
+ [InlineData(int.MinValue, 1)]
+ [InlineData(int.MaxValue, -1)]
+ public void DivideTestSizeInt(int dimension, int divisor)
+ {
+ Size size = new Size(dimension, dimension);
+ Size expected;
+
+ expected = new Size(dimension / divisor, dimension / divisor);
+
+ Assert.Equal(expected, size / divisor);
+ }
+
+ [Theory]
+ [InlineData(1111, 2222, 3333)]
+ public void DivideTestSizeIntWidthHeightDivisor(int width, int height, int divisor)
+ {
+ Size size = new Size(width, height);
+ Size expected;
+
+ expected = new Size(width / divisor, height / divisor);
+
+ Assert.Equal(expected, size / divisor);
+ }
+
+ [Theory]
+ [InlineData(0, 1.0f)]
+ [InlineData(1, 1.0f)]
+ [InlineData(-1, 1.0f)]
+ [InlineData(1, -1.0f)]
+ [InlineData(-1, -1.0f)]
+ [InlineData(int.MaxValue, float.MaxValue)]
+ [InlineData(int.MaxValue, float.MinValue)]
+ [InlineData(int.MinValue, float.MaxValue)]
+ [InlineData(int.MinValue, float.MinValue)]
+ [InlineData(int.MaxValue, 1.0f)]
+ [InlineData(int.MinValue, 1.0f)]
+ [InlineData(int.MaxValue, -1.0f)]
+ [InlineData(int.MinValue, -1.0f)]
+ public void DivideTestSizeFloat(int dimension, float divisor)
+ {
+ SizeF size = new SizeF(dimension, dimension);
+ SizeF expected;
+
+ expected = new SizeF(dimension / divisor, dimension / divisor);
+ Assert.Equal(expected, size / divisor);
+ }
+
+ [Theory]
+ [InlineData(1111, 2222, -333.33f)]
+ public void DivideTestSizeFloatWidthHeightDivisor(int width, int height, float divisor)
+ {
+ SizeF size = new SizeF(width, height);
+ SizeF expected;
+
+ expected = new SizeF(width / divisor, height / divisor);
+ Assert.Equal(expected, size / divisor);
+ }
+
+ [Theory]
+ [InlineData(int.MaxValue, int.MinValue)]
+ [InlineData(int.MinValue, int.MinValue)]
+ [InlineData(int.MaxValue, int.MaxValue)]
+ [InlineData(0, 0)]
+ public void DeconstructTest(int width, int height)
+ {
+ Size s = new Size(width, height);
+
+ (int deconstructedWidth, int deconstructedHeight) = s;
+
+ Assert.Equal(width, deconstructedWidth);
+ Assert.Equal(height, deconstructedHeight);
+ }
+ }
+}
\ No newline at end of file