Browse Source

Merge branch 'main' into js/accumulative-memory-limit

pull/3056/head
James Jackson-South 2 months ago
committed by GitHub
parent
commit
5909ff369e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      .github/workflows/build-and-test.yml
  2. 4
      .github/workflows/code-coverage.yml
  3. 2
      .gitignore
  4. 8
      README.md
  5. 6
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  6. 5
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  7. 5
      src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs
  8. 31
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  9. 3
      src/ImageSharp/Formats/Png/PngThrowHelper.cs
  10. 46
      src/ImageSharp/GraphicsOptions.cs
  11. 14946
      src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs
  12. 84
      src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt
  13. 98
      src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs
  14. 15
      src/ImageSharp/Primitives/Point.cs
  15. 12
      src/ImageSharp/Primitives/PointF.cs
  16. 16
      src/ImageSharp/Primitives/Rectangle.cs
  17. 32
      src/ImageSharp/Primitives/RectangleF.cs
  18. 10
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  19. 16
      src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
  20. 10
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  21. 29
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  22. 9
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  23. 151
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
  24. 32
      tests/ImageSharp.Tests/GraphicsOptionsTests.cs
  25. 63
      tests/ImageSharp.Tests/Primitives/PointFTests.cs
  26. 57
      tests/ImageSharp.Tests/Primitives/PointTests.cs
  27. 56
      tests/ImageSharp.Tests/Primitives/RectangleFTests.cs
  28. 46
      tests/ImageSharp.Tests/Primitives/RectangleTests.cs
  29. 3
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
  30. 3
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs
  31. 44
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
  32. 12
      tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs

8
.github/workflows/build-and-test.yml

@ -27,7 +27,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
@ -137,7 +137,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
@ -209,7 +209,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
@ -227,7 +227,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive

4
.github/workflows/code-coverage.yml

@ -31,7 +31,7 @@ jobs:
git config --global core.longpaths true
- name: Git Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
@ -86,7 +86,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip

2
.gitignore

@ -227,3 +227,5 @@ artifacts/
#lfs
hooks/**
lfs/**
.dotnet

8
README.md

@ -10,16 +10,14 @@ SixLabors.ImageSharp
[![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp/actions)
[![codecov](https://codecov.io/gh/SixLabors/ImageSharp/graph/badge.svg?token=g2WJwz770q)](https://codecov.io/gh/SixLabors/ImageSharp)
[![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp/blob/main/LICENSE)
[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors)
</div>
### **ImageSharp** is a new, fully featured, fully managed, cross-platform, 2D graphics API.
### **ImageSharp** is a high-performance, fully managed, cross-platform 2D graphics API.
ImageSharp is a new, fully featured, fully managed, cross-platform, 2D graphics library.
Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
ImageSharp is a mature, fully featured, high-performance image processing and graphics library for .NET, built for workloads across device, cloud, and embedded/IoT scenarios.
ImageSharp is designed from the ground up to be flexible and extensible. The library provides API endpoints for common image processing operations and the building blocks to allow for the development of additional operations.
Designed from the ground up to balance performance, portability, and ease of use, ImageSharp provides a powerful yet approachable API for common image processing tasks, along with the low-level building blocks needed to extend the library for specialized workflows.
Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp can be used in device, cloud, and embedded/IoT scenarios.

6
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -1548,6 +1548,12 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
case BmpFileMarkerType.Bitmap:
if (this.fileHeader.HasValue)
{
if (this.fileHeader.Value.Offset > stream.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException(
$"Pixel data offset {this.fileHeader.Value.Offset} exceeds file size {stream.Length}.");
}
colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
}
else

5
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -519,6 +519,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
fileMarker = FindNextFileMarker(stream);
}
if (!metadataOnly && this.Frame is null)
{
JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found.");
}
this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved;
}

5
src/ImageSharp/Formats/Png/Chunks/PngPhysical.cs

@ -46,6 +46,11 @@ internal readonly struct PngPhysical
/// <returns>The parsed PhysicalChunkData.</returns>
public static PngPhysical Parse(ReadOnlySpan<byte> data)
{
if (data.Length < 9)
{
PngThrowHelper.ThrowInvalidImageContentException("pHYs chunk is too short");
}
uint hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]);
uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4));
byte unit = data[8];

31
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -1253,6 +1253,12 @@ internal sealed class PngDecoderCore : ImageDecoderCore
ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(palette);
Color.FromPixel(rgbTable, colorTable);
// The tRNS chunk must not contain more alpha values than there are palette entries.
if (alpha.Length > colorTable.Length)
{
alpha = alpha.Slice(0, colorTable.Length);
}
if (alpha.Length > 0)
{
// The alpha chunk may contain as many transparency entries as there are palette entries
@ -1402,26 +1408,31 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return;
}
int zeroIndex = data.IndexOf((byte)0);
if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength)
int keywordEnd = data.IndexOf((byte)0);
if (keywordEnd is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength)
{
return;
}
byte compressionMethod = data[zeroIndex + 1];
if (keywordEnd < 0 || keywordEnd + 2 > data.Length)
{
return; // Not enough data for keyword + null + compression method.
}
byte compressionMethod = data[keywordEnd + 1];
if (compressionMethod != 0)
{
// Only compression method 0 is supported (zlib datastream with deflate compression).
return;
}
ReadOnlySpan<byte> keywordBytes = data[..zeroIndex];
ReadOnlySpan<byte> keywordBytes = data[..keywordEnd];
if (!TryReadTextKeyword(keywordBytes, out string name))
{
return;
}
ReadOnlySpan<byte> compressedData = data[(zeroIndex + 2)..];
ReadOnlySpan<byte> compressedData = data[(keywordEnd + 2)..];
if (this.TryDecompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed)
&& !TryReadTextChunkMetadata(baseMetadata, name, uncompressed))
@ -1932,6 +1943,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return;
}
if (zeroIndexKeyword < 0 || zeroIndexKeyword + 4 > data.Length)
{
return; // Not enough data for keyword + null + flag + method + language.
}
byte compressionFlag = data[zeroIndexKeyword + 1];
if (compressionFlag is not (0 or 1))
{
@ -1956,6 +1972,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
int translatedKeywordStartIdx = langStartIdx + languageLength + 1;
int translatedKeywordLength = data[translatedKeywordStartIdx..].IndexOf((byte)0);
if (translatedKeywordLength < 0)
{
return;
}
string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength));
ReadOnlySpan<byte> keywordBytes = data[..zeroIndexKeyword];

3
src/ImageSharp/Formats/Png/PngThrowHelper.cs

@ -9,8 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Png;
internal static class PngThrowHelper
{
[DoesNotReturn]
public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException)
=> throw new InvalidImageContentException(errorMessage, innerException);
public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage);
[DoesNotReturn]
public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks.");

46
src/ImageSharp/GraphicsOptions.cs

@ -6,11 +6,12 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp;
/// <summary>
/// Options for influencing the drawing functions.
/// Provides configuration for controlling how graphics operations are rendered,
/// including antialiasing, pixel blending, alpha composition, and coverage thresholding.
/// </summary>
public class GraphicsOptions : IDeepCloneable<GraphicsOptions>
{
private int antialiasSubpixelDepth = 16;
private float antialiasThreshold = .5F;
private float blendPercentage = 1F;
/// <summary>
@ -24,61 +25,62 @@ public class GraphicsOptions : IDeepCloneable<GraphicsOptions>
{
this.AlphaCompositionMode = source.AlphaCompositionMode;
this.Antialias = source.Antialias;
this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth;
this.AntialiasThreshold = source.AntialiasThreshold;
this.BlendPercentage = source.BlendPercentage;
this.ColorBlendingMode = source.ColorBlendingMode;
}
/// <summary>
/// Gets or sets a value indicating whether antialiasing should be applied.
/// Defaults to true.
/// When <see langword="true"/>, edges are rendered with smooth sub-pixel coverage.
/// When <see langword="false"/>, coverage is snapped to binary (fully opaque or fully transparent)
/// using <see cref="AntialiasThreshold"/> as the cutoff.
/// Defaults to <see langword="true"/>.
/// </summary>
public bool Antialias { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
/// Defaults to 16.
/// Gets or sets the coverage threshold used when <see cref="Antialias"/> is <see langword="false"/>.
/// Pixels with antialiased coverage above this value are rendered as fully opaque;
/// pixels below are discarded. Valid range is 0 to 1. Lower values preserve more
/// thin features at small sizes. Defaults to <c>0.5F</c>.
/// </summary>
public int AntialiasSubpixelDepth
public float AntialiasThreshold
{
get
{
return this.antialiasSubpixelDepth;
}
get => this.antialiasThreshold;
set
{
Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth));
this.antialiasSubpixelDepth = value;
Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.AntialiasThreshold));
this.antialiasThreshold = value;
}
}
/// <summary>
/// Gets or sets a value between indicating the blending percentage to apply to the drawing operation.
/// Range 0..1; Defaults to 1.
/// Gets or sets the blending percentage applied to the drawing operation.
/// A value of <c>1.0</c> applies the operation at full strength; <c>0.0</c> makes it invisible.
/// Valid range is 0 to 1. Defaults to <c>1.0F</c>.
/// </summary>
public float BlendPercentage
{
get
{
return this.blendPercentage;
}
get => this.blendPercentage;
set
{
Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage));
Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.BlendPercentage));
this.blendPercentage = value;
}
}
/// <summary>
/// Gets or sets a value indicating the color blending mode to apply to the drawing operation.
/// Gets or sets the color blending mode used to combine source and destination pixel colors.
/// Defaults to <see cref="PixelColorBlendingMode.Normal"/>.
/// </summary>
public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal;
/// <summary>
/// Gets or sets a value indicating the alpha composition mode to apply to the drawing operation
/// Gets or sets the alpha composition mode that determines how source and destination alpha
/// channels are combined using Porter-Duff operators.
/// Defaults to <see cref="PixelAlphaCompositionMode.SrcOver"/>.
/// </summary>
public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver;

14946
src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.cs

File diff suppressed because it is too large

84
src/ImageSharp/PixelFormats/PixelBlenders/DefaultPixelBlenders.Generated.tt

@ -123,6 +123,44 @@ var blenders = new []{
}
}
/// <inheritdoc />
protected override void BlendFunction(Span<Vector4> destination, ReadOnlySpan<Vector4> background, Vector4 source, float amount)
{
amount = Numerics.Clamp(amount, 0, 1);
if (Avx2.IsSupported && destination.Length >= 2)
{
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
ref Vector256<float> destinationBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(destination));
ref Vector256<float> destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u);
ref Vector256<float> backgroundBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(background));
Vector256<float> sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W);
Vector256<float> opacity = Vector256.Create(amount);
while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast))
{
destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity);
destinationBase = ref Unsafe.Add(ref destinationBase, 1);
backgroundBase = ref Unsafe.Add(ref backgroundBase, 1);
}
if (Numerics.Modulo2(destination.Length) != 0)
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
int i = destination.Length - 1;
destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, amount);
}
}
else
{
for (int i = 0; i < destination.Length; i++)
{
destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, amount);
}
}
}
/// <inheritdoc />
protected override void BlendFunction(Span<Vector4> destination, ReadOnlySpan<Vector4> background, ReadOnlySpan<Vector4> source, ReadOnlySpan<float> amount)
{
@ -169,6 +207,52 @@ var blenders = new []{
}
}
}
/// <inheritdoc />
protected override void BlendFunction(Span<Vector4> destination, ReadOnlySpan<Vector4> background, Vector4 source, ReadOnlySpan<float> amount)
{
if (Avx2.IsSupported && destination.Length >= 2)
{
// Divide by 2 as 4 elements per Vector4 and 8 per Vector256<float>
ref Vector256<float> destinationBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(destination));
ref Vector256<float> destinationLast = ref Unsafe.Add(ref destinationBase, (uint)destination.Length / 2u);
ref Vector256<float> backgroundBase = ref Unsafe.As<Vector4, Vector256<float>>(ref MemoryMarshal.GetReference(background));
ref float amountBase = ref MemoryMarshal.GetReference(amount);
Vector256<float> sourceBase = Vector256.Create(source.X, source.Y, source.Z, source.W, source.X, source.Y, source.Z, source.W);
Vector256<float> vOne = Vector256.Create(1F);
while (Unsafe.IsAddressLessThan(ref destinationBase, ref destinationLast))
{
// We need to create a Vector256<float> containing the current and next amount values
// taking up each half of the Vector256<float> and then clamp them.
Vector256<float> opacity = Vector256.Create(
Vector128.Create(amountBase),
Vector128.Create(Unsafe.Add(ref amountBase, 1)));
opacity = Avx.Min(Avx.Max(Vector256<float>.Zero, opacity), vOne);
destinationBase = PorterDuffFunctions.<#=blender_composer#>(backgroundBase, sourceBase, opacity);
destinationBase = ref Unsafe.Add(ref destinationBase, 1);
backgroundBase = ref Unsafe.Add(ref backgroundBase, 1);
amountBase = ref Unsafe.Add(ref amountBase, 2);
}
if (Numerics.Modulo2(destination.Length) != 0)
{
// Vector4 fits neatly in pairs. Any overlap has to be equal to 1.
int i = destination.Length - 1;
destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, Numerics.Clamp(amount[i], 0, 1F));
}
}
else
{
for (int i = 0; i < destination.Length; i++)
{
destination[i] = PorterDuffFunctions.<#=blender_composer#>(background[i], source, Numerics.Clamp(amount[i], 0, 1F));
}
}
}
}
<#

98
src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs

@ -64,6 +64,39 @@ public abstract class PixelBlender<TPixel>
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blends a row against a constant source color.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
float amount)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blends 2 rows together
/// </summary>
@ -121,6 +154,39 @@ public abstract class PixelBlender<TPixel>
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blends a row against a constant source color.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blend 2 rows together.
/// </summary>
@ -137,6 +203,22 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<Vector4> source,
float amount);
/// <summary>
/// Blend a row against a constant source color.
/// </summary>
/// <param name="destination">destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color vector</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
protected abstract void BlendFunction(
Span<Vector4> destination,
ReadOnlySpan<Vector4> background,
Vector4 source,
float amount);
/// <summary>
/// Blend 2 rows together.
/// </summary>
@ -152,4 +234,20 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<Vector4> background,
ReadOnlySpan<Vector4> source,
ReadOnlySpan<float> amount);
/// <summary>
/// Blend a row against a constant source color.
/// </summary>
/// <param name="destination">destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color vector</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
protected abstract void BlendFunction(
Span<Vector4> destination,
ReadOnlySpan<Vector4> background,
Vector4 source,
ReadOnlySpan<float> amount);
}

15
src/ImageSharp/Primitives/Point.cs

@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp;
@ -232,7 +233,19 @@ public struct Point : IEquatable<Point>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="PointF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix));
public static PointF Transform(Point point, Matrix3x2 matrix)
=> Vector2.Transform(new Vector2(point.X, point.Y), matrix);
/// <summary>
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="point">The point to transform.</param>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="PointF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(Point point, Matrix4x4 matrix)
=> TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
/// <summary>
/// Deconstructs this point into two integers.

12
src/ImageSharp/Primitives/PointF.cs

@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp;
@ -246,6 +247,17 @@ public struct PointF : IEquatable<PointF>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix);
/// <summary>
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="point">The point to transform.</param>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="PointF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix4x4 matrix)
=> TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
/// <summary>
/// Deconstructs this point into two floats.
/// </summary>

16
src/ImageSharp/Primitives/Rectangle.cs

@ -260,11 +260,17 @@ public struct Rectangle : IEquatable<Rectangle>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed rectangle.</returns>
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));
}
=> RectangleF.Transform(rectangle, matrix);
/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed rectangle.</returns>
public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix)
=> RectangleF.Transform(rectangle, matrix);
/// <summary>
/// Converts a <see cref="RectangleF"/> to a <see cref="Rectangle"/> by performing a truncate operation on all the coordinates.

32
src/ImageSharp/Primitives/RectangleF.cs

@ -236,9 +236,39 @@ public struct RectangleF : IEquatable<RectangleF>
/// <returns>A transformed <see cref="RectangleF"/>.</returns>
public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix)
{
PointF topLeft = PointF.Transform(rectangle.Location, matrix);
PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix);
PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix);
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X));
float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y));
float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X));
float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y));
return FromLTRB(left, top, right, bottom);
}
/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed <see cref="RectangleF"/>.</returns>
public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix)
{
PointF topLeft = PointF.Transform(rectangle.Location, matrix);
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
PointF topRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Top), matrix);
PointF bottomLeft = PointF.Transform(new PointF(rectangle.Left, rectangle.Bottom), matrix);
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
float left = MathF.Min(MathF.Min(topLeft.X, topRight.X), MathF.Min(bottomLeft.X, bottomRight.X));
float top = MathF.Min(MathF.Min(topLeft.Y, topRight.Y), MathF.Min(bottomLeft.Y, bottomRight.Y));
float right = MathF.Max(MathF.Max(topLeft.X, topRight.X), MathF.Max(bottomLeft.X, bottomRight.X));
float bottom = MathF.Max(MathF.Max(topLeft.Y, topRight.Y), MathF.Max(bottomLeft.Y, bottomRight.Y));
return FromLTRB(left, top, right, bottom);
}
/// <summary>

10
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -349,6 +349,16 @@ public class AffineTransformBuilder
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
/// <summary>
/// Clears all accumulated transform matrices, resetting the builder to its initial state.
/// </summary>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder Clear()
{
this.transformMatrixFactories.Clear();
return this;
}
private static void CheckDegenerate(Matrix3x2 matrix)
{
if (TransformUtilities.IsDegenerate(matrix))

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

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

10
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -397,6 +397,16 @@ public class ProjectiveTransformBuilder
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size);
/// <summary>
/// Clears all accumulated transform matrices, resetting the builder to its initial state.
/// </summary>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder Clear()
{
this.transformMatrixFactories.Clear();
return this;
}
private static void CheckDegenerate(Matrix4x4 matrix)
{
if (TransformUtilities.IsDegenerate(matrix))

29
tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

@ -572,6 +572,7 @@ public class BmpDecoderTests
Assert.IsType<InvalidMemoryOperationException>(ex.InnerException);
}
// https://github.com/SixLabors/ImageSharp/issues/3067
[Fact]
public void BmpDecoder_ThrowsException_Issue3067()
{
@ -594,4 +595,32 @@ public class BmpDecoderTests
using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream);
});
}
// https://github.com/SixLabors/ImageSharp/issues/3074
[Fact]
public void BmpDecoder_ThrowsException_Issue3074()
{
// Crafted BMP: pixel data offset = 0x7FFFFFFF, actual file = 35 bytes
byte[] data =
[
0x42, 0x4D, // "BM" signature
0x3A, 0x00, 0x00, 0x00, // file size: 58
0x00, 0x00, 0x00, 0x00, // reserved
0xFF, 0xFF, 0xFF, 0x7F, // pixel offset: 0x7FFFFFFF (2,147,483,647)
0x28, 0x00, 0x00, 0x00, // DIB header size: 40
0x01, 0x00, 0x00, 0x00, // width: 1
0x01, 0xFF, 0x00, 0x00, // height: 65281
0x01, 0x00, // color planes: 1
0x08, 0x00, // bits per pixel: 8
0x00, 0x00, 0x00, 0x00, // compression: RGB
0x00, 0x00, 0x00 // (truncated)
];
using MemoryStream stream = new(data);
Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
});
}
}

9
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -448,4 +448,13 @@ public partial class JpegDecoderTests
[InlineData(TestImages.Jpeg.Issues.Issue2948)]
public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath)
=> Assert.Throws<InvalidImageContentException>(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes));
[Fact]
public void Issue_3071_Decode_TruncatedJpeg_Throws_InvalidImageContentException()
=> Assert.Throws<InvalidImageContentException>(() =>
{
// SOI marker (FF D8) + garbage bytes — only 11 bytes
byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30];
using Image<Rgba32> image = Image.Load<Rgba32>(data);
});
}

151
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs

@ -20,40 +20,40 @@ public partial class PngDecoderTests
private static readonly byte[] Raw1X1PngIhdrAndpHYs =
[
// PNG Identifier
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
// IHDR
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
0x00, 0x00, 0x00,
// IHDR
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
0x00, 0x00, 0x00,
// IHDR CRC
0x90, 0x77, 0x53, 0xDE,
// IHDR CRC
0x90, 0x77, 0x53, 0xDE,
// pHYS
0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00,
0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01,
// pHYS
0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00,
0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01,
// pHYS CRC
0xC7, 0x6F, 0xA8, 0x64
// pHYS CRC
0xC7, 0x6F, 0xA8, 0x64
];
// Contains the png marker, IDAT and IEND chunks of a 1x1 pixel 32bit png 1 a single black pixel.
private static readonly byte[] Raw1X1PngIdatAndIend =
[
// IDAT
0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18,
0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04,
0x00, 0x01,
0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18,
0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04,
0x00, 0x01,
// IDAT CRC
0x5C, 0xCD, 0xFF, 0x69,
// IDAT CRC
0x5C, 0xCD, 0xFF, 0x69,
// IEND
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
// IEND
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
// IEND CRC
0xAE, 0x42, 0x60, 0x82
// IEND CRC
0xAE, 0x42, 0x60, 0x82
];
[Theory]
@ -70,13 +70,120 @@ public partial class PngDecoderTests
WriteChunk(memStream, chunkName);
WriteDataChunk(memStream);
ImageFormatException exception =
InvalidImageContentException exception =
Assert.Throws<InvalidImageContentException>(() => PngDecoder.Instance.Decode<Rgb24>(DecoderOptions.Default, memStream));
Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message);
}
}
// https://github.com/SixLabors/ImageSharp/issues/3078
[Fact]
public void Decode_TruncatedPhysChunk_ExceptionIsThrown()
{
// 24 bytes — PNG signature + truncated pHYs chunk
byte[] payload = Convert.FromHexString(
"89504e470d0a1a0a3030303070485973" +
"3030303030303030");
using MemoryStream stream = new(payload);
InvalidImageContentException exception = Assert.Throws<InvalidImageContentException>(() => Image.Load<Rgba32>(stream));
Assert.Equal("pHYs chunk is too short", exception.Message);
}
// https://github.com/SixLabors/ImageSharp/issues/3079
[Fact]
public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow()
{
byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature
0, 0, 0, 13, // chunk length 13 bytes
73, 72, 68, 82, // chunk type IHDR
0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, // data
55, 110, 249, 36, // checksum
0, 0, 0, 2, // chunk length
122, 84, 88, 116, // chunk type zTXt
1, 0, // truncated data
100, 138, 166, 20, // crc
0, 0, 0, 10, // chunk length 10 bytes
73, 68, 65, 84, // chunk type IDAT
120, 1, 99, 96, 0, 0, 0, 2, 0, 1, // data
115, 117, 1, 24, // checksum
0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; // end chunk
using MemoryStream stream = new(payload);
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
}
// https://github.com/SixLabors/ImageSharp/issues/3079
[Fact]
public void Decode_InternationalText_WithTruncatedData_DoesNotThrow()
{
byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature
0, 0, 0, 13, // chunk length 13 bytes
73, 72, 68, 82, // chunk type IHDR
0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, // data
55, 110, 249, 36, // checksum
0, 0, 0, 2, // chunk length
105, 84, 88, 116, // chunk type iTXt
1, 0, // truncated data
225, 200, 214, 33, // crc
0, 0, 0, 10, // chunk length 10 bytes
73, 68, 65, 84, // chunk type IDAT
120, 1, 99, 96, 0, 0, 0, 2, 0, 1, // data
115, 117, 1, 24, // checksum
0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; // end chunk
using MemoryStream stream = new(payload);
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
}
// https://github.com/SixLabors/ImageSharp/issues/3079
[Fact]
public void Decode_InternationalText_WithTruncatedDataAfterLanguageTag_DoesNotThrow()
{
byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature
0, 0, 0, 13, // chunk length 13 bytes
73, 72, 68, 82, // chunk type IHDR
0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, // data
55, 110, 249, 36, // checksum
0, 0, 0, 21, // chunk length
105, 84, 88, 116, // chunk type iTXt
73, 110, 116, 101, 114, 110, 97, 116, 105, 111, 110, 97, 108, 50, 0, 0, 0, 114, 117, 115, 0, // truncated data after language tag
225, 200, 214, 33, // crc
0, 0, 0, 10, // chunk length 10 bytes
73, 68, 65, 84, // chunk type IDAT
120, 1, 99, 96, 0, 0, 0, 2, 0, 1, // data
115, 117, 1, 24, // checksum
0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]; // end chunk
using MemoryStream stream = new(payload);
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
}
[Fact]
public void Decode_tRnsChunk_WithAlphaLengthGreaterColorTableLength_ShouldNotThrowException()
{
byte[] payload = [137, 80, 78, 71, 13, 10, 26, 10, // PNG signature
0, 0, 0, 13, // chunk length 13 bytes
73, 72, 68, 82, // chunk type IHDR
0, 0, 0, 1, 0, 0, 0, 1, 8, 3, 0, 0, 0, // data
40, 203, 52, 187, // crc
0, 0, 0, 6, // chunk length 6 bytes
80, 76, 84, 69, // chunk type palettte
255, 0, 0, 0, 255, 0, // data
210, 135, 239, 113, // crc
0, 0, 0, 18, // chunk length
116, 82, 78, 83, // chunk type tRns
48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, // data
0, 0, 0, 10, // chunk length
73, 68, 65, 84, // chunk type data
120, 156, 99, 96, 0, 0, 0, 2, 0, 1, 72, 175, 164, 113]; // alpha.Length > colorTable.Length
using MemoryStream stream = new(payload);
using Image<Rgba32> image = Image.Load<Rgba32>(stream);
}
private static string GetChunkTypeName(uint value)
{
byte[] data = new byte[4];

32
tests/ImageSharp.Tests/GraphicsOptionsTests.cs

@ -13,7 +13,7 @@ public class GraphicsOptionsTests
private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone();
[Fact]
public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null);
public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions);
[Fact]
public void DefaultGraphicsOptionsAntialias()
@ -23,35 +23,35 @@ public class GraphicsOptionsTests
}
[Fact]
public void DefaultGraphicsOptionsAntialiasSuppixelDepth()
public void DefaultGraphicsOptionsAntialiasThreshold()
{
const int Expected = 16;
Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth);
Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth);
const float expected = .5F;
Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold);
Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold);
}
[Fact]
public void DefaultGraphicsOptionsBlendPercentage()
{
const float Expected = 1F;
Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage);
Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage);
const float expected = 1F;
Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage);
Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage);
}
[Fact]
public void DefaultGraphicsOptionsColorBlendingMode()
{
const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal;
Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode);
Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode);
const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal;
Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode);
Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode);
}
[Fact]
public void DefaultGraphicsOptionsAlphaCompositionMode()
{
const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver;
Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode);
Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode);
const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver;
Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode);
Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode);
}
[Fact]
@ -61,7 +61,7 @@ public class GraphicsOptionsTests
{
AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop,
Antialias = false,
AntialiasSubpixelDepth = 23,
AntialiasThreshold = .33F,
BlendPercentage = .25F,
ColorBlendingMode = PixelColorBlendingMode.HardLight,
};
@ -79,7 +79,7 @@ public class GraphicsOptionsTests
actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop;
actual.Antialias = false;
actual.AntialiasSubpixelDepth = 23;
actual.AntialiasThreshold = .67F;
actual.BlendPercentage = .25F;
actual.ColorBlendingMode = PixelColorBlendingMode.HardLight;

63
tests/ImageSharp.Tests/Primitives/PointFTests.cs

@ -133,6 +133,69 @@ public class PointFTests
Assert.Equal(new PointF(30, 30), pout);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
PointF p = new(13, 17);
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty);
Matrix4x4 m4 = new(m3);
PointF r3 = PointF.Transform(p, m3);
PointF r4 = PointF.Transform(p, m4);
Assert.Equal(r3.X, r4.X, ApproximateFloatComparer);
Assert.Equal(r3.Y, r4.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
PointF p = new(42.5F, -17.3F);
PointF result = PointF.Transform(p, Matrix4x4.Identity);
Assert.Equal(p.X, result.X, ApproximateFloatComparer);
Assert.Equal(p.Y, result.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
PointF p = new(10, 20);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
PointF result = PointF.Transform(p, m);
Assert.Equal(15F, result.X, ApproximateFloatComparer);
Assert.Equal(17F, result.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Scale()
{
PointF p = new(10, 20);
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
PointF result = PointF.Transform(p, m);
Assert.Equal(20F, result.X, ApproximateFloatComparer);
Assert.Equal(60F, result.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Projective()
{
// A taper matrix with M14 != 0 produces W != 1, requiring perspective divide.
PointF p = new(100, 50);
Matrix4x4 m = Matrix4x4.Identity;
m.M14 = 0.005F; // perspective component
PointF result = PointF.Transform(p, m);
// W = x*M14 + M44 = 100*0.005 + 1 = 1.5
// X = x*M11 + M41 = 100, Y = y*M22 + M42 = 50
// result = (100/1.5, 50/1.5)
Assert.Equal(100F / 1.5F, result.X, ApproximateFloatComparer);
Assert.Equal(50F / 1.5F, result.Y, ApproximateFloatComparer);
}
[Theory]
[InlineData(float.MaxValue, float.MinValue)]
[InlineData(float.MinValue, float.MaxValue)]

57
tests/ImageSharp.Tests/Primitives/PointTests.cs

@ -159,9 +159,10 @@ public class PointTests
Point p = new(13, 17);
Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
Point pout = Point.Transform(p, matrix);
PointF pout = Point.Transform(p, matrix);
Assert.Equal(new Point(-3, 21), pout);
Assert.Equal(-2.828427F, pout.X, 4);
Assert.Equal(21.213203F, pout.Y, 4);
}
[Fact]
@ -170,8 +171,56 @@ public class PointTests
Point p = new(13, 17);
Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, Point.Empty);
Point pout = Point.Transform(p, matrix);
Assert.Equal(new Point(30, 30), pout);
PointF pout = Point.Transform(p, matrix);
Assert.Equal(30F, pout.X, 4);
Assert.Equal(30F, pout.Y, 4);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
Point p = new(13, 17);
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
Matrix4x4 m4 = new(m3);
PointF r3 = Point.Transform(p, m3);
PointF r4 = Point.Transform(p, m4);
Assert.Equal(r3, r4);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
Point p = new(42, -17);
PointF result = Point.Transform(p, Matrix4x4.Identity);
Assert.Equal((PointF)p, result);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
Point p = new(10, 20);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
PointF result = Point.Transform(p, m);
Assert.Equal(15F, result.X, 4);
Assert.Equal(17F, result.Y, 4);
}
[Fact]
public void TransformMatrix4x4_Projective()
{
Point p = new(100, 50);
Matrix4x4 m = Matrix4x4.Identity;
m.M14 = 0.005F;
PointF result = Point.Transform(p, m);
// W = 100*0.005 + 1 = 1.5 => (100/1.5, 50/1.5)
Assert.Equal(100F / 1.5F, result.X, 4);
Assert.Equal(50F / 1.5F, result.Y, 4);
}
[Theory]

56
tests/ImageSharp.Tests/Primitives/RectangleFTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics;
namespace SixLabors.ImageSharp.Tests;
@ -243,6 +244,61 @@ public class RectangleFTests
Assert.Equal(expectedRect, r1);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
Matrix4x4 m4 = new(m3);
RectangleF r3 = RectangleF.Transform(rect, m3);
RectangleF r4 = RectangleF.Transform(rect, m4);
Assert.Equal(r3, r4);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
RectangleF rect = new(10, 20, 100, 50);
RectangleF result = RectangleF.Transform(rect, Matrix4x4.Identity);
Assert.Equal(rect, result);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
RectangleF result = RectangleF.Transform(rect, m);
Assert.Equal(new RectangleF(15, 17, 100, 50), result);
}
[Fact]
public void TransformMatrix4x4_Scale()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
RectangleF result = RectangleF.Transform(rect, m);
Assert.Equal(new RectangleF(20, 60, 200, 150), result);
}
[Fact]
public void TransformMatrix4x4_RotationReturnsBoundingBox()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateRotationZ(MathF.PI / 2F);
RectangleF result = RectangleF.Transform(rect, m);
Assert.Equal(-70F, result.X, 4);
Assert.Equal(10F, result.Y, 4);
Assert.Equal(50F, result.Width, 4);
Assert.Equal(100F, result.Height, 4);
}
[Fact]
public void ToStringTest()
{

46
tests/ImageSharp.Tests/Primitives/RectangleTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics;
namespace SixLabors.ImageSharp.Tests;
@ -294,6 +295,51 @@ public class RectangleTests
Assert.Equal(expectedRect, r1);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
Rectangle rect = new(10, 20, 100, 50);
Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
Matrix4x4 m4 = new(m3);
RectangleF r3 = Rectangle.Transform(rect, m3);
RectangleF r4 = Rectangle.Transform(rect, m4);
Assert.Equal(r3, r4);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
Rectangle rect = new(10, 20, 100, 50);
RectangleF result = Rectangle.Transform(rect, Matrix4x4.Identity);
Assert.Equal(new RectangleF(10, 20, 100, 50), result);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
Rectangle rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
RectangleF result = Rectangle.Transform(rect, m);
Assert.Equal(new RectangleF(15, 17, 100, 50), result);
}
[Fact]
public void TransformMatrix4x4_RotationReturnsBoundingBox()
{
Rectangle rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateRotationZ(MathF.PI / 2F);
RectangleF result = Rectangle.Transform(rect, m);
Assert.Equal(-70F, result.X, 4);
Assert.Equal(10F, result.Y, 4);
Assert.Equal(50F, result.Width, 4);
Assert.Equal(100F, result.Height, 4);
}
[Fact]
public void ToStringTest()
{

3
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs

@ -58,6 +58,9 @@ public class AffineTransformBuilderTests : TransformBuilderTestBase<AffineTransf
protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate)
=> builder.PrependTranslation(translate);
protected override void ClearBuilder(AffineTransformBuilder builder)
=> builder.Clear();
protected override Vector2 Execute(
AffineTransformBuilder builder,
Rectangle rectangle,

3
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs

@ -55,6 +55,9 @@ public class ProjectiveTransformBuilderTests : TransformBuilderTestBase<Projecti
protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) =>
builder.PrependRotationRadians(radians, origin);
protected override void ClearBuilder(ProjectiveTransformBuilder builder)
=> builder.Clear();
protected override Vector2 Execute(
ProjectiveTransformBuilder builder,
Rectangle rectangle,

44
tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs

@ -248,6 +248,48 @@ public abstract class TransformBuilderTestBase<TBuilder>
});
}
[Fact]
public void Clear_ResetsBuilderToIdentity()
{
Size size = new(100, 100);
Rectangle rectangle = new(Point.Empty, size);
Vector2 source = new(10, 20);
TBuilder builder = this.CreateBuilder();
// Apply a transform that changes the point.
this.AppendScale(builder, new SizeF(2, 3));
this.AppendTranslation(builder, new PointF(50, 50));
Vector2 transformed = this.Execute(builder, rectangle, source);
Assert.NotEqual(source, transformed, Comparer);
// Clear and verify the builder produces identity behavior.
this.ClearBuilder(builder);
Vector2 afterClear = this.Execute(builder, rectangle, source);
Assert.Equal(source, afterClear, Comparer);
}
[Fact]
public void Clear_AllowsReuse()
{
Size size = new(100, 100);
Rectangle rectangle = new(Point.Empty, size);
Vector2 source = new(10, 20);
TBuilder builder = this.CreateBuilder();
// First transform: scale by 2.
this.AppendScale(builder, new SizeF(2, 2));
Vector2 scaled = this.Execute(builder, rectangle, source);
Assert.Equal(new Vector2(20, 40), scaled, Comparer);
// Clear and apply a different transform: translate.
this.ClearBuilder(builder);
this.AppendTranslation(builder, new PointF(5, 10));
Vector2 translated = this.Execute(builder, rectangle, source);
Assert.Equal(new Vector2(15, 30), translated, Comparer);
}
protected abstract TBuilder CreateBuilder();
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees);
@ -282,5 +324,7 @@ public abstract class TransformBuilderTestBase<TBuilder>
protected abstract void PrependTranslation(TBuilder builder, PointF translate);
protected abstract void ClearBuilder(TBuilder builder);
protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint);
}

12
tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs

@ -6,13 +6,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities;
public class GraphicsOptionsComparer : IEqualityComparer<GraphicsOptions>
{
public bool Equals(GraphicsOptions x, GraphicsOptions y)
{
return x.AlphaCompositionMode == y.AlphaCompositionMode
&& x.Antialias == y.Antialias
&& x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth
&& x.BlendPercentage == y.BlendPercentage
&& x.ColorBlendingMode == y.ColorBlendingMode;
}
=> x.AlphaCompositionMode == y.AlphaCompositionMode
&& x.Antialias == y.Antialias
&& x.AntialiasThreshold == y.AntialiasThreshold
&& x.BlendPercentage == y.BlendPercentage
&& x.ColorBlendingMode == y.ColorBlendingMode;
public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode();
}

Loading…
Cancel
Save