diff --git a/.gitattributes b/.gitattributes index f7bd4d061e..7de2c15977 100644 --- a/.gitattributes +++ b/.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 diff --git a/ImageSharp.sln b/ImageSharp.sln index 13dd2fba7e..7a52040374 100644 --- a/ImageSharp.sln +++ b/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 diff --git a/shared-infrastructure b/shared-infrastructure index a1d3ac2049..7ac5703452 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit a1d3ac20494631e3cc13132897573796b0e4ee6d +Subproject commit 7ac5703452348d9295db31fc0912c2bd9e419dc9 diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs index 91fa426241..a3959f12e4 100644 --- a/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs +++ b/src/ImageSharp/ColorProfiles/WorkingSpaces/GammaWorkingSpace.cs @@ -62,6 +62,7 @@ public sealed class GammaWorkingSpace : RgbWorkingSpace /// public override int GetHashCode() => HashCode.Combine( + typeof(GammaWorkingSpace), this.WhitePoint, this.ChromaticityCoordinates, this.Gamma); diff --git a/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs b/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs index 97069b856b..b278dd3177 100644 --- a/src/ImageSharp/ColorProfiles/WorkingSpaces/RgbWorkingSpace.cs +++ b/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 /// public override int GetHashCode() - => HashCode.Combine(this.WhitePoint, this.ChromaticityCoordinates); + => HashCode.Combine(this.GetType(), this.WhitePoint, this.ChromaticityCoordinates); } diff --git a/src/ImageSharp/Common/Tuples/Octet{T}.cs b/src/ImageSharp/Common/Tuples/Octet{T}.cs deleted file mode 100644 index f73bdd3390..0000000000 --- a/src/ImageSharp/Common/Tuples/Octet{T}.cs +++ /dev/null @@ -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; - -/// -/// Contains 8 element value tuples of various types. -/// -[StructLayout(LayoutKind.Sequential)] -internal struct Octet - 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; - - /// - 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})"; - } -} - -/// -/// Extension methods for the type. -/// -internal static class OctetExtensions -{ - /// - /// Loads the fields in a target of from one of type. - /// - /// The target of instance. - /// The source of instance. - [MethodImpl(InliningOptions.ShortMethod)] - public static void LoadFrom(ref this Octet destination, ref Octet 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; - } - - /// - /// Loads the fields in a target of from one of type. - /// - /// The target of instance. - /// The source of instance. - [MethodImpl(InliningOptions.ShortMethod)] - public static void LoadFrom(ref this Octet destination, ref Octet 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; - } -} diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 2e60f3a5b9..c24352d576 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -91,6 +91,11 @@ internal sealed class ExrDecoderCore : ImageDecoderCore /// private ExrHeaderAttributes HeaderAttributes { get; set; } + /// + /// Gets or sets the earliest valid stream position for a scanline chunk. + /// + private long MinimumChunkOffset { get; set; } + /// protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { @@ -100,24 +105,33 @@ internal sealed class ExrDecoderCore : ImageDecoderCore ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } - Image image = new(this.configuration, this.Width, this.Height, this.metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); + Image image = null; + try + { + image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Buffer2D 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; } /// @@ -139,9 +153,14 @@ internal sealed class ExrDecoderCore : ImageDecoderCore where TPixel : unmanaged, IPixel { 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 { 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, }; + /// + /// Validates a scanline chunk offset read from the EXR offset table. + /// + /// The chunk offset to validate. + /// The stream containing the image data. + 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."); + } + } + /// /// Determines whether this image has alpha channel. /// diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index e2750d8972..81b7c4da11 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -170,9 +170,13 @@ internal sealed class ExrEncoderCore CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - 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 rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); @@ -181,7 +185,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span 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 { - 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 rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); @@ -273,7 +281,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span 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]; diff --git a/src/ImageSharp/Formats/Exr/ExrUtils.cs b/src/ImageSharp/Formats/Exr/ExrUtils.cs index 386210b81d..532950daa9 100644 --- a/src/ImageSharp/Formats/Exr/ExrUtils.cs +++ b/src/ImageSharp/Formats/Exr/ExrUtils.cs @@ -13,9 +13,9 @@ internal static class ExrUtils /// The image channels array. /// The width in pixels of a row. /// The number of bytes per row. - public static uint CalculateBytesPerRow(IList channels, uint width) + public static ulong CalculateBytesPerRow(IList 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; } } } diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 6b25509ed8..71f2bc333c 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,11 +19,6 @@ - - diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs index 232d8b3e27..c69510bc4d 100644 --- a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_Rgba32_To_Bgra32.cs +++ b/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 sBase = ref Unsafe.As>(ref this.source[0]); - ref Octet dBase = ref Unsafe.As>(ref this.dest[0]); + ref InlineArray8 sBase = ref Unsafe.As>(ref this.source[0]); + ref InlineArray8 dBase = ref Unsafe.As>(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 s, ref Octet d) + private static void BitopsSimdImpl(ref InlineArray8 s, ref InlineArray8 d) { - Vector sVec = Unsafe.As, Vector>(ref s); + Vector sVec = Unsafe.As, Vector>(ref s); Vector aMask = new(0xFF00FF00); Vector bMask = new(0x00FF00FF); @@ -274,7 +273,7 @@ public class PixelConversion_Rgba32_To_Bgra32 Vector cc = Unsafe.As>(ref c); Vector dd = aa + cc; - d = Unsafe.As, Octet>(ref dd); + d = Unsafe.As, InlineArray8>(ref dd); } // [Benchmark] diff --git a/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs b/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs index da7ddae41e..fd17262dec 100644 --- a/tests/ImageSharp.Benchmarks/General/Vectorization/WidenBytesToUInt32.cs +++ b/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 sBase = ref Unsafe.As>(ref this.source[0]); - ref Octet dBase = ref Unsafe.As>(ref this.dest[0]); + ref InlineArray8 sBase = ref Unsafe.As>(ref this.source[0]); + ref InlineArray8 dBase = ref Unsafe.As>(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 source = ref Unsafe.Add(ref sBase, i); + ref InlineArray8 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]; } } diff --git a/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs b/tests/ImageSharp.Tests/ColorProfiles/RgbWorkingSpaceTests.cs new file mode 100644 index 0000000000..8a38ea11fc --- /dev/null +++ b/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; + +/// +/// Tests the class. +/// +[Trait("Color", "Conversion")] +public class RgbWorkingSpaceTests +{ + private static readonly ApproximateFloatComparer TolerantComparer = new(1e-5F); + + public static readonly TheoryData 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); + } +} diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs index 81daac3e0b..12b4c34262 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.Shuffle.cs +++ b/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( diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index e39f9456f0..dfd0a6b307 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/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] diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderSecurityTests.cs new file mode 100644 index 0000000000..2d59c02355 --- /dev/null +++ b/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; + +/// +/// 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. +/// +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrDecoderSecurityTests +{ + /// + /// 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<TPixel> 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 + /// + [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( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + /// + /// 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) + /// + [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( + () => ExrDecoder.Instance.Decode(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( + () => ExrDecoder.Instance.Decode(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( + () => ExrDecoder.Instance.Decode(DecoderOptions.Default, stream)); + } + + /// + /// 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 + /// + [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( + () => ExrDecoder.Instance.Decode(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( + () => ExrDecoder.Instance.Decode(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( + () => 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(); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index 544ac85a51..610693c7f4 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/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] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 83c3a344da..b17a39f388 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/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); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 8d94fb5cf4..ba0dce4c54 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/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) { diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs index 924e94d929..ddd79bb756 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs +++ b/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(), typeof(DefaultPixelBlenders.MultiplySrcOver), PixelColorBlendingMode.Multiply }, }; + public static TheoryData BlenderModeMappings + { + get + { + TheoryData data = new(); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Clear, + typeof(DefaultPixelBlenders.NormalClear), + typeof(DefaultPixelBlenders.MultiplyClear), + typeof(DefaultPixelBlenders.AddClear), + typeof(DefaultPixelBlenders.SubtractClear), + typeof(DefaultPixelBlenders.ScreenClear), + typeof(DefaultPixelBlenders.DarkenClear), + typeof(DefaultPixelBlenders.LightenClear), + typeof(DefaultPixelBlenders.OverlayClear), + typeof(DefaultPixelBlenders.HardLightClear)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Xor, + typeof(DefaultPixelBlenders.NormalXor), + typeof(DefaultPixelBlenders.MultiplyXor), + typeof(DefaultPixelBlenders.AddXor), + typeof(DefaultPixelBlenders.SubtractXor), + typeof(DefaultPixelBlenders.ScreenXor), + typeof(DefaultPixelBlenders.DarkenXor), + typeof(DefaultPixelBlenders.LightenXor), + typeof(DefaultPixelBlenders.OverlayXor), + typeof(DefaultPixelBlenders.HardLightXor)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Src, + typeof(DefaultPixelBlenders.NormalSrc), + typeof(DefaultPixelBlenders.MultiplySrc), + typeof(DefaultPixelBlenders.AddSrc), + typeof(DefaultPixelBlenders.SubtractSrc), + typeof(DefaultPixelBlenders.ScreenSrc), + typeof(DefaultPixelBlenders.DarkenSrc), + typeof(DefaultPixelBlenders.LightenSrc), + typeof(DefaultPixelBlenders.OverlaySrc), + typeof(DefaultPixelBlenders.HardLightSrc)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcAtop, + typeof(DefaultPixelBlenders.NormalSrcAtop), + typeof(DefaultPixelBlenders.MultiplySrcAtop), + typeof(DefaultPixelBlenders.AddSrcAtop), + typeof(DefaultPixelBlenders.SubtractSrcAtop), + typeof(DefaultPixelBlenders.ScreenSrcAtop), + typeof(DefaultPixelBlenders.DarkenSrcAtop), + typeof(DefaultPixelBlenders.LightenSrcAtop), + typeof(DefaultPixelBlenders.OverlaySrcAtop), + typeof(DefaultPixelBlenders.HardLightSrcAtop)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcIn, + typeof(DefaultPixelBlenders.NormalSrcIn), + typeof(DefaultPixelBlenders.MultiplySrcIn), + typeof(DefaultPixelBlenders.AddSrcIn), + typeof(DefaultPixelBlenders.SubtractSrcIn), + typeof(DefaultPixelBlenders.ScreenSrcIn), + typeof(DefaultPixelBlenders.DarkenSrcIn), + typeof(DefaultPixelBlenders.LightenSrcIn), + typeof(DefaultPixelBlenders.OverlaySrcIn), + typeof(DefaultPixelBlenders.HardLightSrcIn)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcOut, + typeof(DefaultPixelBlenders.NormalSrcOut), + typeof(DefaultPixelBlenders.MultiplySrcOut), + typeof(DefaultPixelBlenders.AddSrcOut), + typeof(DefaultPixelBlenders.SubtractSrcOut), + typeof(DefaultPixelBlenders.ScreenSrcOut), + typeof(DefaultPixelBlenders.DarkenSrcOut), + typeof(DefaultPixelBlenders.LightenSrcOut), + typeof(DefaultPixelBlenders.OverlaySrcOut), + typeof(DefaultPixelBlenders.HardLightSrcOut)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.Dest, + typeof(DefaultPixelBlenders.NormalDest), + typeof(DefaultPixelBlenders.MultiplyDest), + typeof(DefaultPixelBlenders.AddDest), + typeof(DefaultPixelBlenders.SubtractDest), + typeof(DefaultPixelBlenders.ScreenDest), + typeof(DefaultPixelBlenders.DarkenDest), + typeof(DefaultPixelBlenders.LightenDest), + typeof(DefaultPixelBlenders.OverlayDest), + typeof(DefaultPixelBlenders.HardLightDest)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestAtop, + typeof(DefaultPixelBlenders.NormalDestAtop), + typeof(DefaultPixelBlenders.MultiplyDestAtop), + typeof(DefaultPixelBlenders.AddDestAtop), + typeof(DefaultPixelBlenders.SubtractDestAtop), + typeof(DefaultPixelBlenders.ScreenDestAtop), + typeof(DefaultPixelBlenders.DarkenDestAtop), + typeof(DefaultPixelBlenders.LightenDestAtop), + typeof(DefaultPixelBlenders.OverlayDestAtop), + typeof(DefaultPixelBlenders.HardLightDestAtop)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestIn, + typeof(DefaultPixelBlenders.NormalDestIn), + typeof(DefaultPixelBlenders.MultiplyDestIn), + typeof(DefaultPixelBlenders.AddDestIn), + typeof(DefaultPixelBlenders.SubtractDestIn), + typeof(DefaultPixelBlenders.ScreenDestIn), + typeof(DefaultPixelBlenders.DarkenDestIn), + typeof(DefaultPixelBlenders.LightenDestIn), + typeof(DefaultPixelBlenders.OverlayDestIn), + typeof(DefaultPixelBlenders.HardLightDestIn)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestOut, + typeof(DefaultPixelBlenders.NormalDestOut), + typeof(DefaultPixelBlenders.MultiplyDestOut), + typeof(DefaultPixelBlenders.AddDestOut), + typeof(DefaultPixelBlenders.SubtractDestOut), + typeof(DefaultPixelBlenders.ScreenDestOut), + typeof(DefaultPixelBlenders.DarkenDestOut), + typeof(DefaultPixelBlenders.LightenDestOut), + typeof(DefaultPixelBlenders.OverlayDestOut), + typeof(DefaultPixelBlenders.HardLightDestOut)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.DestOver, + typeof(DefaultPixelBlenders.NormalDestOver), + typeof(DefaultPixelBlenders.MultiplyDestOver), + typeof(DefaultPixelBlenders.AddDestOver), + typeof(DefaultPixelBlenders.SubtractDestOver), + typeof(DefaultPixelBlenders.ScreenDestOver), + typeof(DefaultPixelBlenders.DarkenDestOver), + typeof(DefaultPixelBlenders.LightenDestOver), + typeof(DefaultPixelBlenders.OverlayDestOver), + typeof(DefaultPixelBlenders.HardLightDestOver)); + + AddBlenderModeMappings( + data, + PixelAlphaCompositionMode.SrcOver, + typeof(DefaultPixelBlenders.NormalSrcOver), + typeof(DefaultPixelBlenders.MultiplySrcOver), + typeof(DefaultPixelBlenders.AddSrcOver), + typeof(DefaultPixelBlenders.SubtractSrcOver), + typeof(DefaultPixelBlenders.ScreenSrcOver), + typeof(DefaultPixelBlenders.DarkenSrcOver), + typeof(DefaultPixelBlenders.LightenSrcOver), + typeof(DefaultPixelBlenders.OverlaySrcOver), + typeof(DefaultPixelBlenders.HardLightSrcOver)); + + return data; + } + } + [Theory] [MemberData(nameof(BlenderMappings))] public void ReturnsCorrectBlender(TestPixel 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 blender = PixelOperations.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 blender = new DefaultPixelBlenders.NormalSrcOver(); + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + + blender.Blend(Configuration.Default, destination, background, source, 1F); + + Assert.Equal(source, destination[0]); + Assert.Equal(source, destination[1]); + } + + [Fact] + public void Blend_WithConstantSourceSingleAmountAndWorkingBuffer() + { + PixelBlender blender = new DefaultPixelBlenders.NormalSrcOver(); + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + 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 blender = new DefaultPixelBlenders.NormalSrcOver(); + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + 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 blender = new DefaultPixelBlenders.NormalSrcOver(); + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32 source = Color.Blue.ToPixel(); + 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 blender = new DefaultPixelBlenders.NormalSrcOver(); + Rgba32[] destination = new Rgba32[2]; + Rgba32[] background = + [ + Color.Red.ToPixel(), + Color.Green.ToPixel() + ]; + + Rgba32[] source = + [ + Color.Blue.ToPixel(), + Color.Yellow.ToPixel() + ]; + + 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 ColorBlendingExpectedResults = new() { { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Normal, Color.MidnightBlue.ToPixel() }, @@ -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 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()) + { + foreach (PixelColorBlendingMode colorMode in Enum.GetValues()) + { + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); + ExerciseBlender(blender); + } + } + } + + private static void ExerciseBlender(PixelBlender blender) + { + Rgba32 background = Color.MistyRose.ToPixel(); + Rgba32 source = Color.MidnightBlue.ToPixel(); + 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(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)); + } } diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs index 994b7d02ee..f8c296846b 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelBlenders/PorterDuffCompositorTests.cs +++ b/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()); } diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index be3e9ccd5d..4cd30fc571 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/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, diff --git a/tests/coverlet.runsettings b/tests/coverlet.runsettings index cffce3540b..f327b47a2e 100644 --- a/tests/coverlet.runsettings +++ b/tests/coverlet.runsettings @@ -9,7 +9,7 @@ lcov - [SixLabors.*]* + [SixLabors.ImageSharp*]* true