Browse Source

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

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

4
.github/workflows/code-coverage.yml

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

2
.gitignore

@ -227,3 +227,5 @@ artifacts/
#lfs #lfs
hooks/** hooks/**
lfs/** 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) [![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) [![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) [![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> </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. 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.
Designed to simplify image processing, ImageSharp brings you an incredibly powerful yet beautifully simple API.
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. 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: case BmpFileMarkerType.Bitmap:
if (this.fileHeader.HasValue) 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; colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
} }
else else

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

@ -519,6 +519,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
fileMarker = FindNextFileMarker(stream); 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; 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> /// <returns>The parsed PhysicalChunkData.</returns>
public static PngPhysical Parse(ReadOnlySpan<byte> data) 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 hResolution = BinaryPrimitives.ReadUInt32BigEndian(data[..4]);
uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4)); uint vResolution = BinaryPrimitives.ReadUInt32BigEndian(data.Slice(4, 4));
byte unit = data[8]; 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); ReadOnlySpan<Rgb24> rgbTable = MemoryMarshal.Cast<byte, Rgb24>(palette);
Color.FromPixel(rgbTable, colorTable); 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) if (alpha.Length > 0)
{ {
// The alpha chunk may contain as many transparency entries as there are palette entries // The alpha chunk may contain as many transparency entries as there are palette entries
@ -1402,26 +1408,31 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return; return;
} }
int zeroIndex = data.IndexOf((byte)0); int keywordEnd = data.IndexOf((byte)0);
if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) if (keywordEnd is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength)
{ {
return; 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) if (compressionMethod != 0)
{ {
// Only compression method 0 is supported (zlib datastream with deflate compression). // Only compression method 0 is supported (zlib datastream with deflate compression).
return; return;
} }
ReadOnlySpan<byte> keywordBytes = data[..zeroIndex]; ReadOnlySpan<byte> keywordBytes = data[..keywordEnd];
if (!TryReadTextKeyword(keywordBytes, out string name)) if (!TryReadTextKeyword(keywordBytes, out string name))
{ {
return; return;
} }
ReadOnlySpan<byte> compressedData = data[(zeroIndex + 2)..]; ReadOnlySpan<byte> compressedData = data[(keywordEnd + 2)..];
if (this.TryDecompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed) if (this.TryDecompressTextData(compressedData, PngConstants.Encoding, out string? uncompressed)
&& !TryReadTextChunkMetadata(baseMetadata, name, uncompressed)) && !TryReadTextChunkMetadata(baseMetadata, name, uncompressed))
@ -1932,6 +1943,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return; return;
} }
if (zeroIndexKeyword < 0 || zeroIndexKeyword + 4 > data.Length)
{
return; // Not enough data for keyword + null + flag + method + language.
}
byte compressionFlag = data[zeroIndexKeyword + 1]; byte compressionFlag = data[zeroIndexKeyword + 1];
if (compressionFlag is not (0 or 1)) if (compressionFlag is not (0 or 1))
{ {
@ -1956,6 +1972,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
int translatedKeywordStartIdx = langStartIdx + languageLength + 1; int translatedKeywordStartIdx = langStartIdx + languageLength + 1;
int translatedKeywordLength = data[translatedKeywordStartIdx..].IndexOf((byte)0); int translatedKeywordLength = data[translatedKeywordStartIdx..].IndexOf((byte)0);
if (translatedKeywordLength < 0)
{
return;
}
string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength)); string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength));
ReadOnlySpan<byte> keywordBytes = data[..zeroIndexKeyword]; 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 internal static class PngThrowHelper
{ {
[DoesNotReturn] [DoesNotReturn]
public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage);
=> throw new InvalidImageContentException(errorMessage, innerException);
[DoesNotReturn] [DoesNotReturn]
public static void ThrowInvalidHeader() => throw new InvalidImageContentException("PNG Image must contain a header chunk and it must be located before any other chunks."); 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; namespace SixLabors.ImageSharp;
/// <summary> /// <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> /// </summary>
public class GraphicsOptions : IDeepCloneable<GraphicsOptions> public class GraphicsOptions : IDeepCloneable<GraphicsOptions>
{ {
private int antialiasSubpixelDepth = 16; private float antialiasThreshold = .5F;
private float blendPercentage = 1F; private float blendPercentage = 1F;
/// <summary> /// <summary>
@ -24,61 +25,62 @@ public class GraphicsOptions : IDeepCloneable<GraphicsOptions>
{ {
this.AlphaCompositionMode = source.AlphaCompositionMode; this.AlphaCompositionMode = source.AlphaCompositionMode;
this.Antialias = source.Antialias; this.Antialias = source.Antialias;
this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; this.AntialiasThreshold = source.AntialiasThreshold;
this.BlendPercentage = source.BlendPercentage; this.BlendPercentage = source.BlendPercentage;
this.ColorBlendingMode = source.ColorBlendingMode; this.ColorBlendingMode = source.ColorBlendingMode;
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether antialiasing should be applied. /// 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> /// </summary>
public bool Antialias { get; set; } = true; public bool Antialias { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. /// Gets or sets the coverage threshold used when <see cref="Antialias"/> is <see langword="false"/>.
/// Defaults to 16. /// 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> /// </summary>
public int AntialiasSubpixelDepth public float AntialiasThreshold
{ {
get get => this.antialiasThreshold;
{
return this.antialiasSubpixelDepth;
}
set set
{ {
Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth)); Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.AntialiasThreshold));
this.antialiasSubpixelDepth = value; this.antialiasThreshold = value;
} }
} }
/// <summary> /// <summary>
/// Gets or sets a value between indicating the blending percentage to apply to the drawing operation. /// Gets or sets the blending percentage applied to the drawing operation.
/// Range 0..1; Defaults to 1. /// 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> /// </summary>
public float BlendPercentage public float BlendPercentage
{ {
get get => this.blendPercentage;
{
return this.blendPercentage;
}
set set
{ {
Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.BlendPercentage));
this.blendPercentage = value; this.blendPercentage = value;
} }
} }
/// <summary> /// <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"/>. /// Defaults to <see cref="PixelColorBlendingMode.Normal"/>.
/// </summary> /// </summary>
public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal;
/// <summary> /// <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"/>. /// Defaults to <see cref="PixelAlphaCompositionMode.SrcOver"/>.
/// </summary> /// </summary>
public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; 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 /> /// <inheritdoc />
protected override void BlendFunction(Span<Vector4> destination, ReadOnlySpan<Vector4> background, ReadOnlySpan<Vector4> source, ReadOnlySpan<float> amount) 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); 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> /// <summary>
/// Blends 2 rows together /// Blends 2 rows together
/// </summary> /// </summary>
@ -121,6 +154,39 @@ public abstract class PixelBlender<TPixel>
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale); 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> /// <summary>
/// Blend 2 rows together. /// Blend 2 rows together.
/// </summary> /// </summary>
@ -137,6 +203,22 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<Vector4> source, ReadOnlySpan<Vector4> source,
float amount); 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> /// <summary>
/// Blend 2 rows together. /// Blend 2 rows together.
/// </summary> /// </summary>
@ -152,4 +234,20 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<Vector4> background, ReadOnlySpan<Vector4> background,
ReadOnlySpan<Vector4> source, ReadOnlySpan<Vector4> source,
ReadOnlySpan<float> amount); 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.ComponentModel;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
@ -232,7 +233,19 @@ public struct Point : IEquatable<Point>
/// <param name="matrix">The transformation matrix used.</param> /// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="PointF"/>.</returns> /// <returns>The transformed <see cref="PointF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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> /// <summary>
/// Deconstructs this point into two integers. /// Deconstructs this point into two integers.

12
src/ImageSharp/Primitives/PointF.cs

@ -4,6 +4,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
@ -246,6 +247,17 @@ public struct PointF : IEquatable<PointF>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix); 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> /// <summary>
/// Deconstructs this point into two floats. /// Deconstructs this point into two floats.
/// </summary> /// </summary>

16
src/ImageSharp/Primitives/Rectangle.cs

@ -260,11 +260,17 @@ public struct Rectangle : IEquatable<Rectangle>
/// <param name="matrix">The transformation matrix.</param> /// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed rectangle.</returns> /// <returns>A transformed rectangle.</returns>
public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix) public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix)
{ => RectangleF.Transform(rectangle, matrix);
PointF bottomRight = Point.Transform(new Point(rectangle.Right, rectangle.Bottom), matrix);
PointF topLeft = Point.Transform(rectangle.Location, matrix); /// <summary>
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft)); /// 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> /// <summary>
/// Converts a <see cref="RectangleF"/> to a <see cref="Rectangle"/> by performing a truncate operation on all the coordinates. /// 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> /// <returns>A transformed <see cref="RectangleF"/>.</returns>
public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix) 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); 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); 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> /// <summary>

10
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -349,6 +349,16 @@ public class AffineTransformBuilder
internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix) internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix3x2 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size); => 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) private static void CheckDegenerate(Matrix3x2 matrix)
{ {
if (TransformUtilities.IsDegenerate(matrix)) if (TransformUtilities.IsDegenerate(matrix))

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

@ -69,11 +69,17 @@ internal static class TransformUtilities
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) 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, // Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and
// such as when the point is transformed behind the camera in a perspective projection. // performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space.
// 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 // For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide
// we don't divide by a very small or negative number, effectively treating any negative w as epsilon. // 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; const float epsilon = 0.0000001F;
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon); 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) internal static SizeF GetTransformedSize(Rectangle sourceRectangle, Matrix4x4 matrix)
=> TransformUtilities.GetRawTransformedSize(matrix, sourceRectangle.Size); => 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) private static void CheckDegenerate(Matrix4x4 matrix)
{ {
if (TransformUtilities.IsDegenerate(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); Assert.IsType<InvalidMemoryOperationException>(ex.InnerException);
} }
// https://github.com/SixLabors/ImageSharp/issues/3067
[Fact] [Fact]
public void BmpDecoder_ThrowsException_Issue3067() public void BmpDecoder_ThrowsException_Issue3067()
{ {
@ -594,4 +595,32 @@ public class BmpDecoderTests
using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream); 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)] [InlineData(TestImages.Jpeg.Issues.Issue2948)]
public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath) public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath)
=> Assert.Throws<InvalidImageContentException>(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes)); => 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 = private static readonly byte[] Raw1X1PngIhdrAndpHYs =
[ [
// PNG Identifier // PNG Identifier
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
// IHDR // IHDR
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// IHDR CRC // IHDR CRC
0x90, 0x77, 0x53, 0xDE, 0x90, 0x77, 0x53, 0xDE,
// pHYS // pHYS
0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00,
0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01, 0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01,
// pHYS CRC // pHYS CRC
0xC7, 0x6F, 0xA8, 0x64 0xC7, 0x6F, 0xA8, 0x64
]; ];
// Contains the png marker, IDAT and IEND chunks of a 1x1 pixel 32bit png 1 a single black pixel. // Contains the png marker, IDAT and IEND chunks of a 1x1 pixel 32bit png 1 a single black pixel.
private static readonly byte[] Raw1X1PngIdatAndIend = private static readonly byte[] Raw1X1PngIdatAndIend =
[ [
// IDAT // IDAT
0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18,
0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04, 0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, 0x04,
0x00, 0x01, 0x00, 0x01,
// IDAT CRC // IDAT CRC
0x5C, 0xCD, 0xFF, 0x69, 0x5C, 0xCD, 0xFF, 0x69,
// IEND // IEND
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44,
// IEND CRC // IEND CRC
0xAE, 0x42, 0x60, 0x82 0xAE, 0x42, 0x60, 0x82
]; ];
[Theory] [Theory]
@ -70,13 +70,120 @@ public partial class PngDecoderTests
WriteChunk(memStream, chunkName); WriteChunk(memStream, chunkName);
WriteDataChunk(memStream); WriteDataChunk(memStream);
ImageFormatException exception = InvalidImageContentException exception =
Assert.Throws<InvalidImageContentException>(() => PngDecoder.Instance.Decode<Rgb24>(DecoderOptions.Default, memStream)); Assert.Throws<InvalidImageContentException>(() => PngDecoder.Instance.Decode<Rgb24>(DecoderOptions.Default, memStream));
Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message); 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) private static string GetChunkTypeName(uint value)
{ {
byte[] data = new byte[4]; 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(); private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone();
[Fact] [Fact]
public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions);
[Fact] [Fact]
public void DefaultGraphicsOptionsAntialias() public void DefaultGraphicsOptionsAntialias()
@ -23,35 +23,35 @@ public class GraphicsOptionsTests
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsAntialiasSuppixelDepth() public void DefaultGraphicsOptionsAntialiasThreshold()
{ {
const int Expected = 16; const float expected = .5F;
Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold);
Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold);
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsBlendPercentage() public void DefaultGraphicsOptionsBlendPercentage()
{ {
const float Expected = 1F; const float expected = 1F;
Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage);
Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage);
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsColorBlendingMode() public void DefaultGraphicsOptionsColorBlendingMode()
{ {
const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal;
Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode);
Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode);
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsAlphaCompositionMode() public void DefaultGraphicsOptionsAlphaCompositionMode()
{ {
const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver;
Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode);
Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode);
} }
[Fact] [Fact]
@ -61,7 +61,7 @@ public class GraphicsOptionsTests
{ {
AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop,
Antialias = false, Antialias = false,
AntialiasSubpixelDepth = 23, AntialiasThreshold = .33F,
BlendPercentage = .25F, BlendPercentage = .25F,
ColorBlendingMode = PixelColorBlendingMode.HardLight, ColorBlendingMode = PixelColorBlendingMode.HardLight,
}; };
@ -79,7 +79,7 @@ public class GraphicsOptionsTests
actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop;
actual.Antialias = false; actual.Antialias = false;
actual.AntialiasSubpixelDepth = 23; actual.AntialiasThreshold = .67F;
actual.BlendPercentage = .25F; actual.BlendPercentage = .25F;
actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; 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); 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] [Theory]
[InlineData(float.MaxValue, float.MinValue)] [InlineData(float.MaxValue, float.MinValue)]
[InlineData(float.MinValue, float.MaxValue)] [InlineData(float.MinValue, float.MaxValue)]

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

@ -159,9 +159,10 @@ public class PointTests
Point p = new(13, 17); Point p = new(13, 17);
Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty); 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] [Fact]
@ -170,8 +171,56 @@ public class PointTests
Point p = new(13, 17); Point p = new(13, 17);
Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, Point.Empty); Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(45, 45, Point.Empty);
Point pout = Point.Transform(p, matrix); PointF pout = Point.Transform(p, matrix);
Assert.Equal(new Point(30, 30), pout); 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] [Theory]

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Globalization; using System.Globalization;
using System.Numerics;
namespace SixLabors.ImageSharp.Tests; namespace SixLabors.ImageSharp.Tests;
@ -243,6 +244,61 @@ public class RectangleFTests
Assert.Equal(expectedRect, r1); 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] [Fact]
public void ToStringTest() public void ToStringTest()
{ {

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Globalization; using System.Globalization;
using System.Numerics;
namespace SixLabors.ImageSharp.Tests; namespace SixLabors.ImageSharp.Tests;
@ -294,6 +295,51 @@ public class RectangleTests
Assert.Equal(expectedRect, r1); 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] [Fact]
public void ToStringTest() 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) protected override void PrependTranslation(AffineTransformBuilder builder, PointF translate)
=> builder.PrependTranslation(translate); => builder.PrependTranslation(translate);
protected override void ClearBuilder(AffineTransformBuilder builder)
=> builder.Clear();
protected override Vector2 Execute( protected override Vector2 Execute(
AffineTransformBuilder builder, AffineTransformBuilder builder,
Rectangle rectangle, 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) => protected override void PrependRotationRadians(ProjectiveTransformBuilder builder, float radians, Vector2 origin) =>
builder.PrependRotationRadians(radians, origin); builder.PrependRotationRadians(radians, origin);
protected override void ClearBuilder(ProjectiveTransformBuilder builder)
=> builder.Clear();
protected override Vector2 Execute( protected override Vector2 Execute(
ProjectiveTransformBuilder builder, ProjectiveTransformBuilder builder,
Rectangle rectangle, 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 TBuilder CreateBuilder();
protected abstract void AppendRotationDegrees(TBuilder builder, float degrees); 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 PrependTranslation(TBuilder builder, PointF translate);
protected abstract void ClearBuilder(TBuilder builder);
protected abstract Vector2 Execute(TBuilder builder, Rectangle rectangle, Vector2 sourcePoint); 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 class GraphicsOptionsComparer : IEqualityComparer<GraphicsOptions>
{ {
public bool Equals(GraphicsOptions x, GraphicsOptions y) public bool Equals(GraphicsOptions x, GraphicsOptions y)
{ => x.AlphaCompositionMode == y.AlphaCompositionMode
return x.AlphaCompositionMode == y.AlphaCompositionMode && x.Antialias == y.Antialias
&& x.Antialias == y.Antialias && x.AntialiasThreshold == y.AntialiasThreshold
&& x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth && x.BlendPercentage == y.BlendPercentage
&& x.BlendPercentage == y.BlendPercentage && x.ColorBlendingMode == y.ColorBlendingMode;
&& x.ColorBlendingMode == y.ColorBlendingMode;
}
public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode(); public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode();
} }

Loading…
Cancel
Save