Browse Source

Merge branch 'main' into js/optimize-allocation-tracking

pull/3120/head
James Jackson-South 2 months ago
committed by GitHub
parent
commit
66bd3ec7d6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      .gitattributes
  2. 5
      ImageSharp.sln
  3. 2
      shared-infrastructure
  4. 1
      src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs
  5. 6
      src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs
  6. 72
      src/ImageSharp/Common/Tuples/Octet{T}.cs
  7. 117
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  8. 20
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  9. 8
      src/ImageSharp/Formats/Exr/ExrUtils.cs
  10. 5
      tests/Directory.Build.targets
  11. 11
      tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs
  12. 18
      tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs
  13. 122
      tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs
  14. 4
      tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs
  15. 4
      tests/ImageSharp.Tests/Common/SimdUtilsTests.cs
  16. 274
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs
  17. 4
      tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs
  18. 4
      tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs
  19. 8
      tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs
  20. 350
      tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs
  21. 2
      tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs
  22. 2
      tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs
  23. 2
      tests/coverlet.runsettings

16
.gitattributes

@ -85,18 +85,12 @@
###############################################################################
*.basis binary
*.dll binary
*.eot binary
*.exe binary
*.otf binary
*.pdf binary
*.ppt binary
*.pptx binary
*.pvr binary
*.snk binary
*.ttc binary
*.ttf binary
*.woff binary
*.woff2 binary
*.xls binary
*.xlsx binary
###############################################################################
@ -126,6 +120,7 @@
*.dds filter=lfs diff=lfs merge=lfs -text
*.ktx filter=lfs diff=lfs merge=lfs -text
*.ktx2 filter=lfs diff=lfs merge=lfs -text
*.astc filter=lfs diff=lfs merge=lfs -text
*.pam filter=lfs diff=lfs merge=lfs -text
*.pbm filter=lfs diff=lfs merge=lfs -text
*.pgm filter=lfs diff=lfs merge=lfs -text
@ -143,3 +138,12 @@
# Handle ICC files by git lfs
###############################################################################
*.icc filter=lfs diff=lfs merge=lfs -text
###############################################################################
# Handle font files by git lfs
###############################################################################
*.eot filter=lfs diff=lfs merge=lfs -text
*.otf filter=lfs diff=lfs merge=lfs -text
*.ttc filter=lfs diff=lfs merge=lfs -text
*.ttf filter=lfs diff=lfs merge=lfs -text
*.woff filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text

5
ImageSharp.sln

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
# Visual Studio Version 18
VisualStudioVersion = 18.5.11723.231 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
@ -45,6 +45,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageShar
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{56801022-D71A-4FBE-BC5B-CBA08E2284EC}"
ProjectSection(SolutionItems) = preProject
tests\coverlet.runsettings = tests\coverlet.runsettings
tests\Directory.Build.props = tests\Directory.Build.props
tests\Directory.Build.targets = tests\Directory.Build.targets
tests\ImageSharp.Tests.ruleset = tests\ImageSharp.Tests.ruleset

2
shared-infrastructure

@ -1 +1 @@
Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d
Subproject commit 7ac5703452348d9295db31fc0912c2bd9e419dc9

1
src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs

@ -62,6 +62,7 @@ public sealed class GammaWorkingSpace : RgbWorkingSpace
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(
typeof(GammaWorkingSpace),
this.WhitePoint,
this.ChromaticityCoordinates,
this.Gamma);

6
src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs

@ -70,8 +70,10 @@ public abstract class RgbWorkingSpace
return true;
}
if (obj is RgbWorkingSpace other)
if (obj.GetType() == this.GetType())
{
RgbWorkingSpace other = (RgbWorkingSpace)obj;
return this.WhitePoint.Equals(other.WhitePoint)
&& this.ChromaticityCoordinates.Equals(other.ChromaticityCoordinates);
}
@ -81,5 +83,5 @@ public abstract class RgbWorkingSpace
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates);
=> HashCode.Combine(this.GetType(), this.WhitePoint, this.ChromaticityCoordinates);
}

72
src/ImageSharp/Common/Tuples/Octet{T}.cs

@ -1,72 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Tuples;
/// <summary>
/// Contains 8 element value tuples of various types.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal struct Octet<T>
where T : unmanaged
{
public T V0;
public T V1;
public T V2;
public T V3;
public T V4;
public T V5;
public T V6;
public T V7;
/// <inheritdoc/>
public override readonly string ToString()
{
return $"Octet<{typeof(T)}>({this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7})";
}
}
/// <summary>
/// Extension methods for the <see cref="Octet{T}"/> type.
/// </summary>
internal static class OctetExtensions
{
/// <summary>
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="uint"/> from one of <see cref="byte"/> type.
/// </summary>
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void LoadFrom(ref this Octet<uint> destination, ref Octet<byte> source)
{
destination.V0 = source.V0;
destination.V1 = source.V1;
destination.V2 = source.V2;
destination.V3 = source.V3;
destination.V4 = source.V4;
destination.V5 = source.V5;
destination.V6 = source.V6;
destination.V7 = source.V7;
}
/// <summary>
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="byte"/> from one of <see cref="uint"/> type.
/// </summary>
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
[MethodImpl(InliningOptions.ShortMethod)]
public static void LoadFrom(ref this Octet<byte> destination, ref Octet<uint> source)
{
destination.V0 = (byte)source.V0;
destination.V1 = (byte)source.V1;
destination.V2 = (byte)source.V2;
destination.V3 = (byte)source.V3;
destination.V4 = (byte)source.V4;
destination.V5 = (byte)source.V5;
destination.V6 = (byte)source.V6;
destination.V7 = (byte)source.V7;
}
}

117
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -91,6 +91,11 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
/// </summary>
private ExrHeaderAttributes HeaderAttributes { get; set; }
/// <summary>
/// Gets or sets the earliest valid stream position for a scanline chunk.
/// </summary>
private long MinimumChunkOffset { get; set; }
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
@ -100,24 +105,33 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
}
Image<TPixel> image = new(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
Image<TPixel> image = null;
try
{
image = new Image<TPixel>(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
switch (this.PixelType)
{
case ExrPixelType.Half:
case ExrPixelType.Float:
this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
break;
case ExrPixelType.UnsignedInt:
this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
break;
default:
ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
break;
}
switch (this.PixelType)
return image;
}
catch
{
case ExrPixelType.Half:
case ExrPixelType.Float:
this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
break;
case ExrPixelType.UnsignedInt:
this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
break;
default:
ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
break;
image?.Dispose();
throw;
}
return image;
}
/// <inheritdoc />
@ -139,9 +153,14 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
where TPixel : unmanaged, IPixel<TPixel>
{
bool hasAlpha = this.HasAlpha();
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerBlock > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size.");
}
int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
@ -158,8 +177,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
(uint)bytesPerBlock,
(uint)bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
@ -170,6 +189,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
ulong rowOffset = this.ReadUnsignedLong(stream);
long nextRowOffsetPosition = stream.Position;
this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);
@ -212,9 +232,14 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
where TPixel : unmanaged, IPixel<TPixel>
{
bool hasAlpha = this.HasAlpha();
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerBlock > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size.");
}
int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
@ -231,8 +256,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
(uint)bytesPerBlock,
(uint)bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
@ -243,6 +268,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
ulong rowOffset = this.ReadUnsignedLong(stream);
long nextRowOffsetPosition = stream.Position;
this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);
@ -597,10 +623,38 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.HeaderAttributes = this.ParseHeaderAttributes(stream);
this.Width = this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1;
this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1;
ExrBox2i dataWindow = this.HeaderAttributes.DataWindow;
if (dataWindow.XMax < dataWindow.XMin || dataWindow.YMax < dataWindow.YMin)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow max values must be greater than or equal to min values.");
}
long width = (long)dataWindow.XMax - dataWindow.XMin + 1;
long height = (long)dataWindow.YMax - dataWindow.YMin + 1;
// Decoding stages each row as four color planes, so the width must be bounded
// before later width * 4 buffer sizing can overflow.
if (width > int.MaxValue / 4 || height > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size.");
}
this.Width = (int)width;
this.Height = (int)height;
this.Channels = this.HeaderAttributes.Channels;
this.Compression = this.HeaderAttributes.Compression;
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock;
long offsetTableByteCount = chunkCount * sizeof(ulong);
// The scanline offset table sits between the header and pixel chunks; proving it
// fits in the stream keeps all later chunk offsets on the pixel-data side.
if (stream.Position > stream.Length || offsetTableByteCount > stream.Length - stream.Position)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset table is outside the bounds of the stream.");
}
this.MinimumChunkOffset = stream.Position + offsetTableByteCount;
this.PixelType = this.ValidateChannels();
this.ImageDataType = this.DetermineImageDataType();
@ -866,6 +920,19 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
_ => false,
};
/// <summary>
/// Validates a scanline chunk offset read from the EXR offset table.
/// </summary>
/// <param name="chunkOffset">The chunk offset to validate.</param>
/// <param name="stream">The stream containing the image data.</param>
private void ValidateChunkOffset(ulong chunkOffset, BufferedReadStream stream)
{
if (chunkOffset < (ulong)this.MinimumChunkOffset || chunkOffset >= (ulong)stream.Length)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset is outside the bounds of the stream.");
}
}
/// <summary>
/// Determines whether this image has alpha channel.
/// </summary>

20
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -170,9 +170,13 @@ internal sealed class ExrEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
{
throw new ImageFormatException("Image is too large to encode in EXR format.");
}
using IMemoryOwner<float> rgbBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
@ -181,7 +185,7 @@ internal sealed class ExrEncoderCore
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);
ulong[] rowOffsets = new ulong[height];
for (uint y = 0; y < height; y += rowsPerBlock)
@ -262,9 +266,13 @@ internal sealed class ExrEncoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
{
throw new ImageFormatException("Image is too large to encode in EXR format.");
}
using IMemoryOwner<uint> rgbBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
@ -273,7 +281,7 @@ internal sealed class ExrEncoderCore
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);
Rgba128 rgb = default;
ulong[] rowOffsets = new ulong[height];

8
src/ImageSharp/Formats/Exr/ExrUtils.cs

@ -13,9 +13,9 @@ internal static class ExrUtils
/// <param name="channels">The image channels array.</param>
/// <param name="width">The width in pixels of a row.</param>
/// <returns>The number of bytes per row.</returns>
public static uint CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
public static ulong CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
{
uint bytesPerRow = 0;
ulong bytesPerRow = 0;
foreach (ExrChannelInfo channelInfo in channels)
{
if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)
@ -26,11 +26,11 @@ internal static class ExrUtils
{
if (channelInfo.PixelType == ExrPixelType.Half)
{
bytesPerRow += 2 * width;
bytesPerRow += 2UL * width;
}
else
{
bytesPerRow += 4 * width;
bytesPerRow += 4UL * width;
}
}
}

5
tests/Directory.Build.targets

@ -19,11 +19,6 @@
<ItemGroup>
<!-- Test Dependencies -->
<PackageReference Update="Colourful" Version="3.2.0" />
<!--
Do not update to 14+ yet. There's differnce in how the BMP decoder handles rounding in 16 bit images.
See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c
-->
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />

11
tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs

@ -7,7 +7,6 @@ using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tuples;
namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion;
@ -225,8 +224,8 @@ public class PixelConversion_Rgba32_To_Bgra32
// [Benchmark]
public void Bitops_Simd()
{
ref Octet<uint> sBase = ref Unsafe.As<Rgba32, Octet<uint>>(ref this.source[0]);
ref Octet<uint> dBase = ref Unsafe.As<Bgra32, Octet<uint>>(ref this.dest[0]);
ref InlineArray8<uint> sBase = ref Unsafe.As<Rgba32, InlineArray8<uint>>(ref this.source[0]);
ref InlineArray8<uint> dBase = ref Unsafe.As<Bgra32, InlineArray8<uint>>(ref this.dest[0]);
for (nuint i = 0; i < (uint)this.Count / 8; i++)
{
@ -249,9 +248,9 @@ public class PixelConversion_Rgba32_To_Bgra32
#pragma warning restore SA1132 // Do not combine fields
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void BitopsSimdImpl(ref Octet<uint> s, ref Octet<uint> d)
private static void BitopsSimdImpl(ref InlineArray8<uint> s, ref InlineArray8<uint> d)
{
Vector<uint> sVec = Unsafe.As<Octet<uint>, Vector<uint>>(ref s);
Vector<uint> sVec = Unsafe.As<InlineArray8<uint>, Vector<uint>>(ref s);
Vector<uint> aMask = new(0xFF00FF00);
Vector<uint> bMask = new(0x00FF00FF);
@ -274,7 +273,7 @@ public class PixelConversion_Rgba32_To_Bgra32
Vector<uint> cc = Unsafe.As<C, Vector<uint>>(ref c);
Vector<uint> dd = aa + cc;
d = Unsafe.As<Vector<uint>, Octet<uint>>(ref dd);
d = Unsafe.As<Vector<uint>, InlineArray8<uint>>(ref dd);
}
// [Benchmark]

18
tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs

@ -5,8 +5,6 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Tuples;
namespace SixLabors.ImageSharp.Benchmarks.General.Vectorization;
[Config(typeof(Config.Short))]
@ -30,12 +28,22 @@ public class WidenBytesToUInt32
{
const int N = Count / 8;
ref Octet<byte> sBase = ref Unsafe.As<byte, Octet<byte>>(ref this.source[0]);
ref Octet<uint> dBase = ref Unsafe.As<uint, Octet<uint>>(ref this.dest[0]);
ref InlineArray8<byte> sBase = ref Unsafe.As<byte, InlineArray8<byte>>(ref this.source[0]);
ref InlineArray8<uint> dBase = ref Unsafe.As<uint, InlineArray8<uint>>(ref this.dest[0]);
for (nuint i = 0; i < N; i++)
{
Unsafe.Add(ref dBase, i).LoadFrom(ref Unsafe.Add(ref sBase, i));
ref InlineArray8<byte> source = ref Unsafe.Add(ref sBase, i);
ref InlineArray8<uint> destination = ref Unsafe.Add(ref dBase, i);
destination[0] = source[0];
destination[1] = source[1];
destination[2] = source[2];
destination[3] = source[3];
destination[4] = source[4];
destination[5] = source[5];
destination[6] = source[6];
destination[7] = source[7];
}
}

122
tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs

@ -0,0 +1,122 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.Tests.ColorProfiles;
/// <summary>
/// Tests the <see cref="RgbWorkingSpace"/> class.
/// </summary>
[Trait("Color", "Conversion")]
public class RgbWorkingSpaceTests
{
private static readonly ApproximateFloatComparer TolerantComparer = new(1e-5F);
public static readonly TheoryData<RgbWorkingSpace> WorkingSpaces = new()
{
KnownRgbWorkingSpaces.SRgb,
KnownRgbWorkingSpaces.SRgbSimplified,
KnownRgbWorkingSpaces.Rec709,
KnownRgbWorkingSpaces.Rec2020,
KnownRgbWorkingSpaces.ECIRgbv2,
KnownRgbWorkingSpaces.AdobeRgb1998,
KnownRgbWorkingSpaces.ApplesRgb,
KnownRgbWorkingSpaces.BestRgb,
KnownRgbWorkingSpaces.BetaRgb,
KnownRgbWorkingSpaces.BruceRgb,
KnownRgbWorkingSpaces.CIERgb,
KnownRgbWorkingSpaces.ColorMatchRgb,
KnownRgbWorkingSpaces.DonRgb4,
KnownRgbWorkingSpaces.EktaSpacePS5,
KnownRgbWorkingSpaces.NTSCRgb,
KnownRgbWorkingSpaces.PALSECAMRgb,
KnownRgbWorkingSpaces.ProPhotoRgb,
KnownRgbWorkingSpaces.SMPTECRgb,
KnownRgbWorkingSpaces.WideGamutRgb
};
[Fact]
public void RgbWorkingSpaceEqualityRequiresSameConcreteType()
{
RgbWorkingSpace sRgb = KnownRgbWorkingSpaces.SRgb;
RgbWorkingSpace sRgbSimplified = KnownRgbWorkingSpaces.SRgbSimplified;
RgbWorkingSpace rec709 = KnownRgbWorkingSpaces.Rec709;
Assert.False(sRgb.Equals(sRgbSimplified));
Assert.False(sRgbSimplified.Equals(sRgb));
Assert.False(sRgb.Equals(rec709));
Assert.False(rec709.Equals(sRgb));
Assert.NotEqual(sRgb.GetHashCode(), sRgbSimplified.GetHashCode());
Assert.NotEqual(sRgb.GetHashCode(), rec709.GetHashCode());
}
[Fact]
public void RgbWorkingSpaceEqualityMatchesSameConcreteTypeValues()
{
RgbWorkingSpace x = new SRgbWorkingSpace(
KnownRgbWorkingSpaces.SRgb.WhitePoint,
KnownRgbWorkingSpaces.SRgb.ChromaticityCoordinates);
RgbWorkingSpace y = new SRgbWorkingSpace(
KnownRgbWorkingSpaces.SRgb.WhitePoint,
KnownRgbWorkingSpaces.SRgb.ChromaticityCoordinates);
Assert.Equal(x, y);
Assert.Equal(x.GetHashCode(), y.GetHashCode());
}
[Fact]
public void GammaWorkingSpaceEqualityIncludesGamma()
{
GammaWorkingSpace x = new(
2.2F,
KnownRgbWorkingSpaces.SRgbSimplified.WhitePoint,
KnownRgbWorkingSpaces.SRgbSimplified.ChromaticityCoordinates);
GammaWorkingSpace y = new(
1.8F,
KnownRgbWorkingSpaces.SRgbSimplified.WhitePoint,
KnownRgbWorkingSpaces.SRgbSimplified.ChromaticityCoordinates);
Assert.NotEqual(x, y);
Assert.NotEqual(x.GetHashCode(), y.GetHashCode());
}
[Theory]
[MemberData(nameof(WorkingSpaces))]
public void CompressAndExpand_RoundTripsWithTolerance(RgbWorkingSpace workingSpace)
{
Vector4[] linear =
[
new(0F, .001F, .18F, 1F), // Endpoint, below the sRGB breakpoint, common middle gray, and opaque alpha.
new(.0031308F, .25F, .5F, .75F), // sRGB linear/gamma breakpoint with interior values and partial alpha.
new(.75F, .5F, .25F, .5F), // Reversed interior values ensure channel order does not hide errors.
new(1F, .8F, .2F, .25F) // Upper endpoint, high/low interior values, and low alpha.
];
Vector4[] expectedCompressed = new Vector4[linear.Length];
for (int i = 0; i < linear.Length; i++)
{
Vector4 compressed = workingSpace.Compress(linear[i]);
Vector4 expanded = workingSpace.Expand(compressed);
expectedCompressed[i] = compressed;
Assert.Equal(linear[i], expanded, TolerantComparer);
}
Vector4[] actualCompressed = linear.ToArray();
workingSpace.Compress(actualCompressed);
Assert.Equal(expectedCompressed, actualCompressed, TolerantComparer);
workingSpace.Expand(actualCompressed);
Assert.Equal(linear, actualCompressed, TolerantComparer);
}
}

4
tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs

@ -292,7 +292,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
[Theory]
@ -478,7 +478,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
private static void TestShuffleFloat4Channel(

4
tests/ImageSharp.Tests/Common/SimdUtilsTests.cs

@ -133,7 +133,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2);
}
[Theory]
@ -171,7 +171,7 @@ public partial class SimdUtilsTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
count,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2);
}
[Theory]

274
tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs

@ -0,0 +1,274 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Exr;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Exr;
/// <summary>
/// Security regression tests for the EXR decoder (Findings EXR-1, EXR-2, EXR-3).
/// The EXR decoder was merged to main but not yet included in a tagged NuGet release.
/// Each test demonstrates a crafted-input crash present in the unfixed code.
/// </summary>
[Trait("Format", "Exr")]
[ValidateDisposedMemoryAllocations]
public class ExrDecoderSecurityTests
{
/// <summary>
/// EXR-1 — EXR DataWindow Integer Overflow Produces Negative Image Dimensions (DoS)
///
/// Width and Height are computed from attacker-controlled DataWindow attributes
/// using unchecked int subtraction:
/// this.Width = XMax - XMin + 1 // overflows to -2147483647
///
/// The negative Width is then passed to the Image&lt;TPixel&gt; constructor, which calls
/// Guard.MustBeGreaterThan(width, 0) → ArgumentOutOfRangeException.
///
/// After a fix this should throw InvalidImageContentException instead.
///
/// Affected file:
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 600–601
/// </summary>
[Fact]
public void Decode_DataWindowOverflow_NegativeWidth_Throws()
{
// XMin = -1073741825, XMax = 1073741823
// Width = 1073741823 - (-1073741825) + 1 = 2^31 + 1 → wraps to -2147483647
byte[] data = BuildMinimalExr(xMin: -1073741825, yMin: 0, xMax: 1073741823, yMax: 0);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
/// <summary>
/// EXR-2 — EXR Row Offset Table Unvalidated Seek (DoS)
///
/// Row offsets are read from the file and used unconditionally to seek the stream:
/// ulong rowOffset = this.ReadUnsignedLong(stream);
/// stream.Position = (long)rowOffset; // no bounds check
///
/// A crafted offset of 0xFFFFFFFFFFFFFFFF casts to −1 as long, causing
/// an ArgumentOutOfRangeException when setting stream.Position.
///
/// After a fix this should throw InvalidImageContentException instead.
///
/// Affected file:
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 170–175 (scanline)
/// lines 243–248 (tile)
/// </summary>
[Fact]
public void Decode_CraftedRowOffsets_OutOfBounds_Throws()
{
// Valid 2×2 image (XMin=0,YMin=0,XMax=1,YMax=1 → Width=2,Height=2).
// Row offset table immediately follows the header null byte:
// 2 rows × 8 bytes each, all set to 0xFFFFFFFFFFFFFFFF.
byte[] invalidOffsets = new byte[16];
BinaryPrimitives.WriteUInt64LittleEndian(invalidOffsets, 0xFFFFFFFFFFFFFFFF);
BinaryPrimitives.WriteUInt64LittleEndian(invalidOffsets.AsSpan(8), 0xFFFFFFFFFFFFFFFF);
byte[] data = BuildMinimalExr(
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
rowOffsetTableAppend: invalidOffsets);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
[Fact]
public void Decode_CraftedRowOffsets_IntoHeader_Throws()
{
// Offset 0 points back into the EXR file header and must be rejected
// before the decoder seeks to attacker-controlled non-pixel data.
byte[] headerOffsets = new byte[16];
byte[] data = BuildMinimalExr(
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
rowOffsetTableAppend: headerOffsets);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
[Fact]
public void Decode_CraftedRowOffsets_IntoOffsetTable_Throws()
{
byte[] data = BuildMinimalExr(
xMin: 0, yMin: 0, xMax: 1, yMax: 1,
rowOffsetTableAppend: new byte[16]);
// Point the first row offset at the second row offset entry.
BinaryPrimitives.WriteUInt64LittleEndian(data.AsSpan(data.Length - 16), (ulong)(data.Length - 8));
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
/// <summary>
/// EXR-3 — Oversized EXR RGBA row sizing is rejected as invalid image content.
///
/// With 4 RGBA HALF channels and Width = 2^29, the decoded row staging and
/// bytes-per-row arithmetic both exceed the supported buffer sizing limits.
/// The decoder must reject this as InvalidImageContentException before any allocation.
///
/// Affected file:
/// src/ImageSharp/Formats/Exr/ExrDecoderCore.cs lines 142–150, 215–223
/// src/ImageSharp/Formats/Exr/ExrUtils.cs CalculateBytesPerRow
/// </summary>
[Fact]
public void Decode_RgbaRowSizingExceedsBufferLimits_Throws()
{
// 4 RGBA HALF channels at this width cannot be represented by the decoder's
// int-sized row staging or block buffers.
byte[] data = BuildMinimalRgbaExr(xMin: 0, yMin: 0, xMax: 536870911, yMax: 0);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
[Fact]
public void Decode_DataWindowWidthExceedsRowBufferLimit_Throws()
{
// A single HALF channel keeps bytesPerBlock below int.MaxValue, but the decoder
// still stages four color planes and must reject widths that overflow width × 4.
byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: int.MaxValue / 4, yMax: 0);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, stream));
}
[Fact]
public void Identify_RowOffsetTableExceedsStream_Throws()
{
// Identify parses the header only, so this verifies the offset table bound is
// validated before scanline decoding reads from the table.
byte[] data = BuildMinimalExr(xMin: 0, yMin: 0, xMax: 1, yMax: 1);
using var stream = new MemoryStream(data);
Assert.Throws<InvalidImageContentException>(
() => ExrDecoder.Instance.Identify(DecoderOptions.Default, stream));
}
// -------------------------------------------------------------------------
// Helpers: construct minimal valid-enough EXR scanline files.
//
// Required attributes per ParseHeaderAttributes validation:
// channels, compression, dataWindow, displayWindow,
// lineOrder, pixelAspectRatio, screenWindowCenter, screenWindowWidth
// -------------------------------------------------------------------------
private static byte[] BuildMinimalExr(
int xMin, int yMin, int xMax, int yMax,
byte[] rowOffsetTableAppend = null)
{
// channels: single "R" HALF channel with xSampling=1, ySampling=1
// Layout per ReadChannelInfo: name\0 (2) + pixelType (4) + pLinear+reserved (4)
// + xSampling (4) + ySampling (4) = 18 bytes/channel
// + list-null (1) = 19 total
byte[] channelData =
[
0x52, 0x00, // "R\0"
0x01, 0x00, 0x00, 0x00, // pixelType = Half (1)
0x00, 0x00, 0x00, 0x00, // pLinear + 3 reserved bytes
0x01, 0x00, 0x00, 0x00, // xSampling = 1
0x01, 0x00, 0x00, 0x00, // ySampling = 1
0x00, // channel-list null terminator
];
return BuildExrWithChannels(xMin, yMin, xMax, yMax, channelData, rowOffsetTableAppend);
}
private static byte[] BuildMinimalRgbaExr(int xMin, int yMin, int xMax, int yMax)
{
// 4 HALF channels in alphabetical order (A, B, G, R) per EXR spec.
// 18 bytes per channel × 4 channels + 1 list-null = 73 bytes total.
byte[] channelData =
[
0x41, 0x00, // "A\0"
0x01, 0x00, 0x00, 0x00, // pixelType = Half (1)
0x00, 0x00, 0x00, 0x00, // pLinear + 3 reserved bytes
0x01, 0x00, 0x00, 0x00, // xSampling = 1
0x01, 0x00, 0x00, 0x00, // ySampling = 1
0x42, 0x00, // "B\0"
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x47, 0x00, // "G\0"
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x52, 0x00, // "R\0"
0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00,
0x00, // channel-list null terminator
];
return BuildExrWithChannels(xMin, yMin, xMax, yMax, channelData);
}
private static byte[] BuildExrWithChannels(
int xMin, int yMin, int xMax, int yMax,
byte[] channelData,
byte[] rowOffsetTableAppend = null)
{
using var ms = new MemoryStream();
using var bw = new BinaryWriter(ms, System.Text.Encoding.ASCII, leaveOpen: true);
// Magic (0x01312F76 LE) + version 2, scanline (flags = 0x00)
bw.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01, 0x02, 0x00, 0x00, 0x00 });
void WriteAttr(string name, string type, byte[] payload)
{
foreach (char c in name) bw.Write((byte)c);
bw.Write((byte)0);
foreach (char c in type) bw.Write((byte)c);
bw.Write((byte)0);
bw.Write(payload.Length);
bw.Write(payload);
}
WriteAttr("channels", "chlist", channelData);
WriteAttr("compression", "compression", [0x00]); // None
byte[] dw = new byte[16];
BinaryPrimitives.WriteInt32LittleEndian(dw, xMin);
BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(4), yMin);
BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(8), xMax);
BinaryPrimitives.WriteInt32LittleEndian(dw.AsSpan(12), yMax);
WriteAttr("dataWindow", "box2i", dw);
WriteAttr("displayWindow", "box2i", new byte[16]); // all zeros (0,0,0,0)
WriteAttr("lineOrder", "lineOrder", [0x00]); // IncreasingY
byte[] aspect = new byte[4];
BinaryPrimitives.WriteSingleLittleEndian(aspect, 1.0f);
WriteAttr("pixelAspectRatio", "float", aspect);
WriteAttr("screenWindowCenter", "v2f", new byte[8]); // (0f, 0f)
byte[] sww = new byte[4];
BinaryPrimitives.WriteSingleLittleEndian(sww, 1.0f);
WriteAttr("screenWindowWidth", "float", sww);
bw.Write((byte)0x00); // end-of-header sentinel
if (rowOffsetTableAppend is not null)
bw.Write(rowOffsetTableAppend);
return ms.ToArray();
}
}

4
tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs

@ -219,7 +219,7 @@ public partial class Block8x8FTests : JpegFixture
RunTest,
srcSeed,
qtSeed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
[Fact]
@ -391,7 +391,7 @@ public partial class Block8x8FTests : JpegFixture
// 3. DisableAvx2 - call fallback code of float implementation
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
[Theory]

4
tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs

@ -152,7 +152,7 @@ public static class DCTTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
[Theory]
@ -359,7 +359,7 @@ public static class DCTTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
}
}
}

8
tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs

@ -69,7 +69,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -102,7 +102,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -168,7 +168,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{
@ -201,7 +201,7 @@ public class JpegColorConverterTests
{
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic);
static void RunTest(string arg)
{

350
tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.PixelFormats.PixelBlenders;
using SixLabors.ImageSharp.Tests.TestUtilities;
@ -32,6 +33,172 @@ public class PixelBlenderTests
{ new TestPixel<RgbaVector>(), typeof(DefaultPixelBlenders<RgbaVector>.MultiplySrcOver), PixelColorBlendingMode.Multiply },
};
public static TheoryData<PixelColorBlendingMode, PixelAlphaCompositionMode, Type> BlenderModeMappings
{
get
{
TheoryData<PixelColorBlendingMode, PixelAlphaCompositionMode, Type> data = new();
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.Clear,
typeof(DefaultPixelBlenders<Rgba32>.NormalClear),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyClear),
typeof(DefaultPixelBlenders<Rgba32>.AddClear),
typeof(DefaultPixelBlenders<Rgba32>.SubtractClear),
typeof(DefaultPixelBlenders<Rgba32>.ScreenClear),
typeof(DefaultPixelBlenders<Rgba32>.DarkenClear),
typeof(DefaultPixelBlenders<Rgba32>.LightenClear),
typeof(DefaultPixelBlenders<Rgba32>.OverlayClear),
typeof(DefaultPixelBlenders<Rgba32>.HardLightClear));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.Xor,
typeof(DefaultPixelBlenders<Rgba32>.NormalXor),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyXor),
typeof(DefaultPixelBlenders<Rgba32>.AddXor),
typeof(DefaultPixelBlenders<Rgba32>.SubtractXor),
typeof(DefaultPixelBlenders<Rgba32>.ScreenXor),
typeof(DefaultPixelBlenders<Rgba32>.DarkenXor),
typeof(DefaultPixelBlenders<Rgba32>.LightenXor),
typeof(DefaultPixelBlenders<Rgba32>.OverlayXor),
typeof(DefaultPixelBlenders<Rgba32>.HardLightXor));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.Src,
typeof(DefaultPixelBlenders<Rgba32>.NormalSrc),
typeof(DefaultPixelBlenders<Rgba32>.MultiplySrc),
typeof(DefaultPixelBlenders<Rgba32>.AddSrc),
typeof(DefaultPixelBlenders<Rgba32>.SubtractSrc),
typeof(DefaultPixelBlenders<Rgba32>.ScreenSrc),
typeof(DefaultPixelBlenders<Rgba32>.DarkenSrc),
typeof(DefaultPixelBlenders<Rgba32>.LightenSrc),
typeof(DefaultPixelBlenders<Rgba32>.OverlaySrc),
typeof(DefaultPixelBlenders<Rgba32>.HardLightSrc));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.SrcAtop,
typeof(DefaultPixelBlenders<Rgba32>.NormalSrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.MultiplySrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.AddSrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.SubtractSrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.ScreenSrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.DarkenSrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.LightenSrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.OverlaySrcAtop),
typeof(DefaultPixelBlenders<Rgba32>.HardLightSrcAtop));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.SrcIn,
typeof(DefaultPixelBlenders<Rgba32>.NormalSrcIn),
typeof(DefaultPixelBlenders<Rgba32>.MultiplySrcIn),
typeof(DefaultPixelBlenders<Rgba32>.AddSrcIn),
typeof(DefaultPixelBlenders<Rgba32>.SubtractSrcIn),
typeof(DefaultPixelBlenders<Rgba32>.ScreenSrcIn),
typeof(DefaultPixelBlenders<Rgba32>.DarkenSrcIn),
typeof(DefaultPixelBlenders<Rgba32>.LightenSrcIn),
typeof(DefaultPixelBlenders<Rgba32>.OverlaySrcIn),
typeof(DefaultPixelBlenders<Rgba32>.HardLightSrcIn));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.SrcOut,
typeof(DefaultPixelBlenders<Rgba32>.NormalSrcOut),
typeof(DefaultPixelBlenders<Rgba32>.MultiplySrcOut),
typeof(DefaultPixelBlenders<Rgba32>.AddSrcOut),
typeof(DefaultPixelBlenders<Rgba32>.SubtractSrcOut),
typeof(DefaultPixelBlenders<Rgba32>.ScreenSrcOut),
typeof(DefaultPixelBlenders<Rgba32>.DarkenSrcOut),
typeof(DefaultPixelBlenders<Rgba32>.LightenSrcOut),
typeof(DefaultPixelBlenders<Rgba32>.OverlaySrcOut),
typeof(DefaultPixelBlenders<Rgba32>.HardLightSrcOut));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.Dest,
typeof(DefaultPixelBlenders<Rgba32>.NormalDest),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyDest),
typeof(DefaultPixelBlenders<Rgba32>.AddDest),
typeof(DefaultPixelBlenders<Rgba32>.SubtractDest),
typeof(DefaultPixelBlenders<Rgba32>.ScreenDest),
typeof(DefaultPixelBlenders<Rgba32>.DarkenDest),
typeof(DefaultPixelBlenders<Rgba32>.LightenDest),
typeof(DefaultPixelBlenders<Rgba32>.OverlayDest),
typeof(DefaultPixelBlenders<Rgba32>.HardLightDest));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.DestAtop,
typeof(DefaultPixelBlenders<Rgba32>.NormalDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.AddDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.SubtractDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.ScreenDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.DarkenDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.LightenDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.OverlayDestAtop),
typeof(DefaultPixelBlenders<Rgba32>.HardLightDestAtop));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.DestIn,
typeof(DefaultPixelBlenders<Rgba32>.NormalDestIn),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyDestIn),
typeof(DefaultPixelBlenders<Rgba32>.AddDestIn),
typeof(DefaultPixelBlenders<Rgba32>.SubtractDestIn),
typeof(DefaultPixelBlenders<Rgba32>.ScreenDestIn),
typeof(DefaultPixelBlenders<Rgba32>.DarkenDestIn),
typeof(DefaultPixelBlenders<Rgba32>.LightenDestIn),
typeof(DefaultPixelBlenders<Rgba32>.OverlayDestIn),
typeof(DefaultPixelBlenders<Rgba32>.HardLightDestIn));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.DestOut,
typeof(DefaultPixelBlenders<Rgba32>.NormalDestOut),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyDestOut),
typeof(DefaultPixelBlenders<Rgba32>.AddDestOut),
typeof(DefaultPixelBlenders<Rgba32>.SubtractDestOut),
typeof(DefaultPixelBlenders<Rgba32>.ScreenDestOut),
typeof(DefaultPixelBlenders<Rgba32>.DarkenDestOut),
typeof(DefaultPixelBlenders<Rgba32>.LightenDestOut),
typeof(DefaultPixelBlenders<Rgba32>.OverlayDestOut),
typeof(DefaultPixelBlenders<Rgba32>.HardLightDestOut));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.DestOver,
typeof(DefaultPixelBlenders<Rgba32>.NormalDestOver),
typeof(DefaultPixelBlenders<Rgba32>.MultiplyDestOver),
typeof(DefaultPixelBlenders<Rgba32>.AddDestOver),
typeof(DefaultPixelBlenders<Rgba32>.SubtractDestOver),
typeof(DefaultPixelBlenders<Rgba32>.ScreenDestOver),
typeof(DefaultPixelBlenders<Rgba32>.DarkenDestOver),
typeof(DefaultPixelBlenders<Rgba32>.LightenDestOver),
typeof(DefaultPixelBlenders<Rgba32>.OverlayDestOver),
typeof(DefaultPixelBlenders<Rgba32>.HardLightDestOver));
AddBlenderModeMappings(
data,
PixelAlphaCompositionMode.SrcOver,
typeof(DefaultPixelBlenders<Rgba32>.NormalSrcOver),
typeof(DefaultPixelBlenders<Rgba32>.MultiplySrcOver),
typeof(DefaultPixelBlenders<Rgba32>.AddSrcOver),
typeof(DefaultPixelBlenders<Rgba32>.SubtractSrcOver),
typeof(DefaultPixelBlenders<Rgba32>.ScreenSrcOver),
typeof(DefaultPixelBlenders<Rgba32>.DarkenSrcOver),
typeof(DefaultPixelBlenders<Rgba32>.LightenSrcOver),
typeof(DefaultPixelBlenders<Rgba32>.OverlaySrcOver),
typeof(DefaultPixelBlenders<Rgba32>.HardLightSrcOver));
return data;
}
}
[Theory]
[MemberData(nameof(BlenderMappings))]
public void ReturnsCorrectBlender<TPixel>(TestPixel<TPixel> pixel, Type type, PixelColorBlendingMode mode)
@ -41,6 +208,126 @@ public class PixelBlenderTests
Assert.IsType(type, blender);
}
[Theory]
[MemberData(nameof(BlenderModeMappings))]
public void ReturnsCorrectBlenderForAllModeCombinations(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode, Type type)
{
PixelBlender<Rgba32> blender = PixelOperations<Rgba32>.Instance.GetPixelBlender(colorMode, alphaMode);
Assert.IsType(type, blender);
}
[Fact]
public void BlendFunctionsAreCalledForAllModeCombinations() =>
FeatureTestRunner.RunWithHwIntrinsicsFeature(
ExerciseAllBlenderModeCombinations,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
[Fact]
public void Blend_WithConstantSourceAndSingleAmount()
{
PixelBlender<Rgba32> blender = new DefaultPixelBlenders<Rgba32>.NormalSrcOver();
Rgba32[] destination = new Rgba32[2];
Rgba32[] background =
[
Color.Red.ToPixel<Rgba32>(),
Color.Green.ToPixel<Rgba32>()
];
Rgba32 source = Color.Blue.ToPixel<Rgba32>();
blender.Blend(Configuration.Default, destination, background, source, 1F);
Assert.Equal(source, destination[0]);
Assert.Equal(source, destination[1]);
}
[Fact]
public void Blend_WithConstantSourceSingleAmountAndWorkingBuffer()
{
PixelBlender<Rgba32> blender = new DefaultPixelBlenders<Rgba32>.NormalSrcOver();
Rgba32[] destination = new Rgba32[2];
Rgba32[] background =
[
Color.Red.ToPixel<Rgba32>(),
Color.Green.ToPixel<Rgba32>()
];
Rgba32 source = Color.Blue.ToPixel<Rgba32>();
Vector4[] workingBuffer = new Vector4[destination.Length * 2];
blender.Blend(Configuration.Default, destination, background, source, 1F, workingBuffer);
Assert.Equal(source, destination[0]);
Assert.Equal(source, destination[1]);
}
[Fact]
public void Blend_WithConstantSourceAndAmountSpan()
{
PixelBlender<Rgba32> blender = new DefaultPixelBlenders<Rgba32>.NormalSrcOver();
Rgba32[] destination = new Rgba32[2];
Rgba32[] background =
[
Color.Red.ToPixel<Rgba32>(),
Color.Green.ToPixel<Rgba32>()
];
Rgba32 source = Color.Blue.ToPixel<Rgba32>();
float[] amount = [1F, 1F];
blender.Blend(Configuration.Default, destination, background, source, amount);
Assert.Equal(source, destination[0]);
Assert.Equal(source, destination[1]);
}
[Fact]
public void Blend_WithConstantSourceAmountSpanAndWorkingBuffer()
{
PixelBlender<Rgba32> blender = new DefaultPixelBlenders<Rgba32>.NormalSrcOver();
Rgba32[] destination = new Rgba32[2];
Rgba32[] background =
[
Color.Red.ToPixel<Rgba32>(),
Color.Green.ToPixel<Rgba32>()
];
Rgba32 source = Color.Blue.ToPixel<Rgba32>();
float[] amount = [1F, 1F];
Vector4[] workingBuffer = new Vector4[destination.Length * 2];
blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer);
Assert.Equal(source, destination[0]);
Assert.Equal(source, destination[1]);
}
[Fact]
public void Blend_WithSourceSpanAmountSpanAndWorkingBuffer()
{
PixelBlender<Rgba32> blender = new DefaultPixelBlenders<Rgba32>.NormalSrcOver();
Rgba32[] destination = new Rgba32[2];
Rgba32[] background =
[
Color.Red.ToPixel<Rgba32>(),
Color.Green.ToPixel<Rgba32>()
];
Rgba32[] source =
[
Color.Blue.ToPixel<Rgba32>(),
Color.Yellow.ToPixel<Rgba32>()
];
float[] amount = [1F, 1F];
Vector4[] workingBuffer = new Vector4[destination.Length * 3];
blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer);
Assert.Equal(source[0], destination[0]);
Assert.Equal(source[1], destination[1]);
}
public static TheoryData<Rgba32, Rgba32, float, PixelColorBlendingMode, Rgba32> ColorBlendingExpectedResults = new()
{
{ Color.MistyRose.ToPixel<Rgba32>(), Color.MidnightBlue.ToPixel<Rgba32>(), 1, PixelColorBlendingMode.Normal, Color.MidnightBlue.ToPixel<Rgba32>() },
@ -92,4 +379,67 @@ public class PixelBlenderTests
// var str = actualResult.Rgba.ToString("X8"); // used to extract expectedResults
Assert.Equal(actualResult.ToVector4(), expectedResult.ToVector4());
}
private static void AddBlenderModeMappings(
TheoryData<PixelColorBlendingMode, PixelAlphaCompositionMode, Type> data,
PixelAlphaCompositionMode alphaMode,
Type normal,
Type multiply,
Type add,
Type subtract,
Type screen,
Type darken,
Type lighten,
Type overlay,
Type hardLight)
{
data.Add(PixelColorBlendingMode.Normal, alphaMode, normal);
data.Add(PixelColorBlendingMode.Multiply, alphaMode, multiply);
data.Add(PixelColorBlendingMode.Add, alphaMode, add);
data.Add(PixelColorBlendingMode.Subtract, alphaMode, subtract);
data.Add(PixelColorBlendingMode.Screen, alphaMode, screen);
data.Add(PixelColorBlendingMode.Darken, alphaMode, darken);
data.Add(PixelColorBlendingMode.Lighten, alphaMode, lighten);
data.Add(PixelColorBlendingMode.Overlay, alphaMode, overlay);
data.Add(PixelColorBlendingMode.HardLight, alphaMode, hardLight);
}
private static void ExerciseAllBlenderModeCombinations()
{
foreach (PixelAlphaCompositionMode alphaMode in Enum.GetValues<PixelAlphaCompositionMode>())
{
foreach (PixelColorBlendingMode colorMode in Enum.GetValues<PixelColorBlendingMode>())
{
PixelBlender<Rgba32> blender = PixelOperations<Rgba32>.Instance.GetPixelBlender(colorMode, alphaMode);
ExerciseBlender(blender);
}
}
}
private static void ExerciseBlender(PixelBlender<Rgba32> blender)
{
Rgba32 background = Color.MistyRose.ToPixel<Rgba32>();
Rgba32 source = Color.MidnightBlue.ToPixel<Rgba32>();
float[] amount = [1F, 1F, 1F, 1F];
Rgba32 expected = blender.Blend(background, source, 1F);
Rgba32[] destination = new Rgba32[4];
Rgba32[] backgroundSpan = [background, background, background, background];
Rgba32[] sourceSpan = [source, source, source, source];
Vector4[] sourceSpanBuffer = new Vector4[destination.Length * 3];
Vector4[] constantSourceBuffer = new Vector4[destination.Length * 2];
blender.Blend<Rgba32>(Configuration.Default, destination, backgroundSpan, sourceSpan, 1F, sourceSpanBuffer);
Assert.All(destination, x => Assert.Equal(expected, x));
blender.Blend(Configuration.Default, destination, backgroundSpan, source, 1F, constantSourceBuffer);
Assert.All(destination, x => Assert.Equal(expected, x));
blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, sourceSpanBuffer);
Assert.All(destination, x => Assert.Equal(expected, x));
blender.Blend(Configuration.Default, destination, backgroundSpan, source, amount, constantSourceBuffer);
Assert.All(destination, x => Assert.Equal(expected, x));
}
}

2
tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs

@ -59,7 +59,7 @@ public class PorterDuffCompositorTests
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512 | HwIntrinsics.DisableAVX,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX,
provider,
mode.ToString());
}

2
tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs

@ -439,7 +439,7 @@ public enum HwIntrinsics : long
DisableSSE42 = 1L << 1,
DisableAVX = 1L << 2,
DisableAVX2 = 1L << 3,
DisableAVX512 = 1L << 4,
DisableAVX512F = 1L << 4,
DisableAVX512v2 = 1L << 5,
DisableAVX512v3 = 1L << 6,
DisableAVX10v1 = 1L << 7,

2
tests/coverlet.runsettings

@ -9,7 +9,7 @@
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>lcov</Format>
<Include>[SixLabors.*]*</Include>
<Include>[SixLabors.ImageSharp*]*</Include>
<UseSourceLink>true</UseSourceLink>
</Configuration>
</DataCollector>

Loading…
Cancel
Save