diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs index 82d475e578..b33d7dd449 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs @@ -42,6 +42,10 @@ public ClutCalculator(IccClut clut) Guard.NotNull(clut, nameof(clut)); Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount)); Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount)); + if (clut.InputChannelCount > 4 || clut.OutputChannelCount > 4) + { + throw new InvalidIccProfileException("ICC conversion supports at most four input and output channels."); + } this.inputCount = clut.InputChannelCount; this.outputCount = clut.OutputChannelCount; diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs index c97578ee3f..08f957bbe5 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs @@ -59,6 +59,11 @@ private static Vector4 CalculateLut(LutCalculator[] lut, Vector4 value) private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix) { + if (inputCurve.Length > 4 || outputCurve.Length > 4) + { + throw new InvalidIccProfileException("ICC conversion supports at most four input and output channels."); + } + this.inputCurve = InitLut(inputCurve); this.outputCurve = InitLut(outputCurve); this.clutCalculator = new ClutCalculator(clut); diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs index d2fc5d9b55..029be68c51 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs @@ -15,6 +15,10 @@ internal class TrcCalculator : IVector4Calculator public TrcCalculator(IccTagDataEntry[] entries, bool inverted) { Guard.NotNull(entries, nameof(entries)); + if (entries.Length > 4) + { + throw new InvalidIccProfileException("ICC conversion supports at most four tone response curves."); + } this.calculators = new ISingleCalculator[entries.Length]; for (int i = 0; i < entries.Length; i++) diff --git a/src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs b/src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs new file mode 100644 index 0000000000..e90aba7138 --- /dev/null +++ b/src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the type. +/// +internal static class BufferedReadStreamExtensions +{ + /// + /// Determines whether the complete read range is contained in the stream. + /// + /// The stream containing the data. + /// The absolute start of the range. + /// The number of bytes in the range. + /// Whether the range is contained in the stream. + public static bool IsReadRangeValid(this BufferedReadStream stream, long offset, ulong length) + { + // Compare the offset first so subtraction cannot underflow, and avoid + // adding an untrusted length to the offset where it could wrap around. + ulong streamLength = (ulong)stream.Length; + return (ulong)offset <= streamLength && length <= streamLength - (ulong)offset; + } + + /// + /// Gets a buffer length when the complete read fits in both the stream and an integer-sized buffer. + /// + /// The stream containing the data. + /// The declared length in bytes. + /// The validated length, or zero when the range is invalid. + /// Whether the complete read is valid. + public static bool TryGetReadLength(this BufferedReadStream stream, ulong length, out int bufferLength) + { + if (length > int.MaxValue || !stream.IsReadRangeValid(stream.Position, length)) + { + bufferLength = 0; + return false; + } + + bufferLength = (int)length; + return true; + } + + /// + /// Reads data from the stream into a slice of the provided buffer. + /// + /// The stream. + /// The buffer. + /// The offset within the buffer where bytes are read into. + /// The number of bytes, if available, to read. + /// The actual number of bytes read. + public static int Read(this BufferedReadStream stream, Span buffer, int offset, int count) + => stream.Read(buffer.Slice(offset, count)); + + /// + /// Advances the stream by the specified number of bytes. Nonpositive counts are ignored. + /// + /// The stream. + /// The number of bytes to skip. + public static void Skip(this BufferedReadStream stream, int count) + { + if (count > 0) + { + // BufferedReadStream is always seekable; its position setter preserves + // buffered data when the destination is inside the current buffer. + stream.Position += count; + } + } +} diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 7ed3348240..13761d4d2a 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; - namespace SixLabors.ImageSharp; /// @@ -19,53 +17,4 @@ internal static class StreamExtensions /// The number of bytes to write to the stream. public static void Write(this Stream stream, Span buffer, int offset, int count) => stream.Write(buffer.Slice(offset, count)); - - /// - /// Reads data from a stream into the provided buffer. - /// - /// The stream. - /// The buffer. - /// The offset within the buffer where the bytes are read into. - /// The number of bytes, if available, to read. - /// The actual number of bytes read. - public static int Read(this Stream stream, Span buffer, int offset, int count) - => stream.Read(buffer.Slice(offset, count)); - - /// - /// Skips the number of bytes in the given stream. - /// - /// The stream. - /// A byte offset relative to the origin parameter. - public static void Skip(this Stream stream, int count) - { - if (count < 1) - { - return; - } - - if (stream.CanSeek) - { - stream.Seek(count, SeekOrigin.Current); - return; - } - - byte[] buffer = ArrayPool.Shared.Rent(count); - try - { - while (count > 0) - { - int bytesRead = stream.Read(buffer, 0, count); - if (bytesRead == 0) - { - break; - } - - count -= bytesRead; - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } } diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index 0b88aa5b07..8e99728314 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -26,8 +26,7 @@ internal static class ColorNumerics /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) - => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); + public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1)); /// /// Gets the luminance from the rgb components using the formula diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs index 8980d2b53e..4154cbf1f7 100644 --- a/src/ImageSharp/Common/Helpers/Numerics.cs +++ b/src/ImageSharp/Common/Helpers/Numerics.cs @@ -263,63 +263,95 @@ public static int Clamp(int value, int min, int max) } /// - /// Returns the value clamped to the inclusive range of min and max. + /// Returns the value clamped to the inclusive range of min and max, mapping NaN to min. /// /// The value to clamp. /// The minimum inclusive value. /// The maximum inclusive value. /// The clamped . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Clamp(float value, float min, float max) - { - if (value > max) - { - return max; - } - - if (value < min) - { - return min; - } - - return value; - } + public static float Clamp(float value, float min, float max) => Clamp(value, min, max); /// - /// Returns the value clamped to the inclusive range of min and max. + /// Returns the value clamped to the inclusive range of min and max, mapping NaN to min. /// /// The value to clamp. /// The minimum inclusive value. /// The maximum inclusive value. /// The clamped . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Clamp(double value, double min, double max) + public static double Clamp(double value, double min, double max) => Clamp(value, min, max); + + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Clamp(Vector2 value, Vector2 min, Vector2 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector2(); + + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Clamp(Vector128 value, Vector128 min, Vector128 max) + where T : struct, INumber { - if (value > max) - { - return max; - } + // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime. + Vector128 lowerClamped = Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(value, min), value, min); + return Vector128.ConditionalSelect(Vector128.GreaterThan(value, max), max, lowerClamped); + } - if (value < min) - { - return min; - } + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Clamp(Vector256 value, Vector256 min, Vector256 max) + where T : struct, INumber + { + // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime. + Vector256 lowerClamped = Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(value, min), value, min); + return Vector256.ConditionalSelect(Vector256.GreaterThan(value, max), max, lowerClamped); + } - return value; + /// + /// Clamps components to the inclusive range of min and max, mapping NaN to min. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bounds. + /// The inclusive upper bounds. + /// The clamped components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Clamp(Vector512 value, Vector512 min, Vector512 max) + where T : struct, INumber + { + // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime. + Vector512 lowerClamped = Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(value, min), value, min); + return Vector512.ConditionalSelect(Vector512.GreaterThan(value, max), max, lowerClamped); } /// - /// Returns the value clamped to the inclusive range of min and max. - /// 5x Faster than - /// on platforms < NET 5. + /// Clamps components to the inclusive range of min and max, mapping NaN to min. /// /// The value to clamp. /// The minimum inclusive value. /// The maximum inclusive value. /// The clamped . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) - => Vector4.Min(Vector4.Max(value, min), max); + public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector4(); /// /// Clamps the span values to the inclusive range of min and max. @@ -352,24 +384,24 @@ public static void Clamp(Span span, int min, int max) => TensorPrimitives_.Clamp(span, min, max, span); /// - /// Clamps the span values to the inclusive range of min and max. + /// Clamps the span values to the inclusive range of min and max, mapping NaN to min. /// /// The span containing the values to clamp. /// The minimum inclusive value. /// The maximum inclusive value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Clamp(Span span, float min, float max) - => TensorPrimitives_.Clamp(span, min, max, span); + => Clamp(span, min, max); /// - /// Clamps the span values to the inclusive range of min and max. + /// Clamps the span values to the inclusive range of min and max, mapping NaN to min. /// /// The span containing the values to clamp. /// The minimum inclusive value. /// The maximum inclusive value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Clamp(Span span, double min, double max) - => TensorPrimitives_.Clamp(span, min, max, span); + => Clamp(span, min, max); /// /// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. @@ -392,7 +424,7 @@ public static void Premultiply(ref Vector4 source) public static void ClampRgbToAlpha(ref Vector4 source) { Vector4 alpha = PermuteW(source); - source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha); + source = WithW(Clamp(source, Vector4.Zero, alpha), alpha); } /// @@ -1071,4 +1103,78 @@ public static nuint Vector512Count(int length) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Normalize(Span span, float sum) => TensorPrimitives_.Divide(span, sum, span); + + /// + /// Clamps a floating-point component while mapping NaN to the lower bound. + /// + /// The component type. + /// The component to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The clamped component. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static T Clamp(T value, T min, T max) + where T : struct, INumber + { + // Ordered comparisons map NaN to min; in-range values retain their original bits, including signed zero. + return value > max ? max : value >= min ? value : min; + } + + /// + /// Applies the scalar clamp contract to floating-point spans in place. + /// + /// The component type. + /// The components to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + private static void Clamp(Span span, T min, T max) + where T : struct, INumber + { + ref T start = ref MemoryMarshal.GetReference(span); + int i = 0; + + // Each register uses the same Clamp overload as individual vector callers. Descending widths consume + // the remainder without overlapping stores, and the final components use the scalar overload. + if (Vector512.IsHardwareAccelerated) + { + Vector512 lower = Vector512.Create(min); + Vector512 upper = Vector512.Create(max); + + for (; i <= span.Length - Vector512.Count; i += Vector512.Count) + { + Vector512 value = Vector512.LoadUnsafe(ref start, (nuint)i); + Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 lower = Vector256.Create(min); + Vector256 upper = Vector256.Create(max); + + for (; i <= span.Length - Vector256.Count; i += Vector256.Count) + { + Vector256 value = Vector256.LoadUnsafe(ref start, (nuint)i); + Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 lower = Vector128.Create(min); + Vector128 upper = Vector128.Create(max); + + for (; i <= span.Length - Vector128.Count; i += Vector128.Count) + { + Vector128 value = Vector128.LoadUnsafe(ref start, (nuint)i); + Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i); + } + } + + for (; i < span.Length; i++) + { + ref T value = ref Unsafe.Add(ref start, (uint)i); + value = Clamp(value, min, max); + } + } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index aba1243d77..95f02503e9 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -1429,7 +1429,7 @@ private void ReadInfoHeader(BufferedReadStream stream) // > 108 bytes infoHeaderType = BmpInfoHeaderType.WinVersion5; this.infoHeader = BmpInfoHeader.ParseV5(buffer); - if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) + if (!this.Options.SkipMetadata && this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) { long streamPosition = stream.Position; this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart)); @@ -1474,8 +1474,16 @@ private void ReadInfoHeader(BufferedReadStream stream) /// The stream position where the info header begins. private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart) { + long profileStart = infoHeaderStart + this.infoHeader.ProfileData; + if (this.infoHeader.ProfileData < 0 || + this.infoHeader.ProfileSize <= 0 || + !stream.IsReadRangeValid(profileStart, (uint)this.infoHeader.ProfileSize)) + { + BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile."); + } + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; - stream.Position = infoHeaderStart + this.infoHeader.ProfileData; + stream.Position = profileStart; if (stream.Read(iccProfileData) != iccProfileData.Length) { @@ -1560,6 +1568,11 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b this.infoHeader.Height = -this.infoHeader.Height; } + if (this.infoHeader.Width <= 0 || this.infoHeader.Height <= 0) + { + BmpThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); + } + int bytesPerColorMapEntry = 4; int colorMapSizeBytes = -1; if (this.infoHeader.ClrUsed == 0) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index 2b5b740569..e58eca4e98 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -39,7 +39,7 @@ public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint byt } /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { Span outputBuffer = MemoryMarshal.Cast(buffer); Span decompressed = this.tmpBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index 19edb31afe..b1a2094dab 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -25,10 +25,10 @@ public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint by } /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { - int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); - if (bytesRead != (int)this.BytesPerBlock) + int bytesRead = stream.Read(buffer[..(int)uncompressedBytes]); + if (bytesRead != uncompressedBytes) { ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!"); } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index f45b660e7d..d621c893e5 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -39,19 +39,20 @@ public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint byte } /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { - Span uncompressed = this.tmpBuffer.GetSpan(); + uint rowCount = uncompressedBytes / this.BytesPerRow; + uint packedBytes = this.pixelType == ExrPixelType.Float ? (uncompressedBytes / 4) * 3 : uncompressedBytes; + Span uncompressed = this.tmpBuffer.GetSpan()[..(int)packedBytes]; Span outputBufferHalf = MemoryMarshal.Cast(buffer); Span outputBufferFloat = MemoryMarshal.Cast(buffer); Span outputBufferUint = MemoryMarshal.Cast(buffer); - uint uncompressedBytes = this.BytesPerBlock; - UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); + UndoZipCompression(stream, compressedBytes, uncompressed, packedBytes); int lastIn = 0; int outputOffset = 0; - for (int y = 0; y < this.RowsPerBlock; y++) + for (uint y = 0; y < rowCount; y++) { for (int c = 0; c < this.channelCount; c++) { diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs index f548a81810..5ffe83af3c 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -26,7 +26,7 @@ public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, ui : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { Span uncompressed = this.tmpBuffer.GetSpan(); int maxLength = (int)this.BytesPerBlock; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 8bab76f402..41fda54366 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -26,15 +26,14 @@ public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint byt : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { - Span uncompressed = this.tmpBuffer.GetSpan(); + Span uncompressed = this.tmpBuffer.GetSpan()[..(int)uncompressedBytes]; - uint uncompressedBytes = (uint)buffer.Length; int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); - Reconstruct(uncompressed, (uint)totalRead); - Interleave(uncompressed, (uint)totalRead, buffer); + Reconstruct(uncompressed, uncompressedBytes); + Interleave(uncompressed, uncompressedBytes, buffer); } /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index f598955f43..b85c37546e 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -31,8 +31,9 @@ protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uin /// /// The buffered stream to decompress. /// The compressed bytes. + /// The expected byte count for the current block. /// The buffer to write the decompressed data to. - public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer); /// /// Decompresses zip compressed data. @@ -52,13 +53,19 @@ protected static int UndoZipCompression(BufferedReadStream stream, uint compress int left = (int)(compressedBytes - (stream.Position - pos)); return left > 0 ? left : 0; }); - inflateStream.AllocateNewBytes((int)compressedBytes, true); - using DeflateStream dataStream = inflateStream.CompressedStream!; + + // Incomplete headers return false even for critical chunks, leaving no stream to read. + if (!inflateStream.AllocateNewBytes((int)compressedBytes, true)) + { + ExrThrowHelper.ThrowInvalidImageContentException("ZIP compressed EXR block has an incomplete zlib header."); + } + + using DeflateStream dataStream = inflateStream.CompressedStream; int totalRead = 0; while (totalRead < uncompressedBytes) { - int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead); + int bytesRead = dataStream.Read(uncompressed.Slice(totalRead, (int)uncompressedBytes - totalRead)); if (bytesRead <= 0) { break; @@ -67,9 +74,9 @@ protected static int UndoZipCompression(BufferedReadStream stream, uint compress totalRead += bytesRead; } - if (totalRead == 0) + if (totalRead != uncompressedBytes || dataStream.ReadByte() != -1) { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!"); + ExrThrowHelper.ThrowInvalidImageContentException("ZIP compressed EXR block has an invalid decompressed length."); } return totalRead; diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 680e8d333d..4c9924ec3b 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -166,7 +166,8 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf int height = this.Height; int channelCount = this.Channels.Count; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row. + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span redPixelData = rowBuffer.GetSpan()[..width]; @@ -192,10 +193,19 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf this.ValidateChunkOffset(rowOffset, stream); stream.Position = (long)rowOffset; - uint rowStartIndex = this.ReadUnsignedInteger(stream); + + // Chunk coordinates are signed and absolute; pixel rows are relative to the data window. + uint rowStartIndex = (uint)((long)this.ReadSignedInteger(stream) - this.HeaderAttributes.DataWindow.YMin); + if (rowStartIndex >= height) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk row index is outside the data window."); + } uint compressedBytesCount = this.ReadUnsignedInteger(stream); - decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex); + uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock); + + this.DecompressBlock(decompressor, stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); int offset = 0; for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) @@ -247,7 +257,8 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe int height = this.Height; int channelCount = this.Channels.Count; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row. + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span redPixelData = rowBuffer.GetSpan()[..width]; @@ -273,10 +284,19 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe this.ValidateChunkOffset(rowOffset, stream); stream.Position = (long)rowOffset; - uint rowStartIndex = this.ReadUnsignedInteger(stream); + + // Chunk coordinates are signed and absolute; pixel rows are relative to the data window. + uint rowStartIndex = (uint)((long)this.ReadSignedInteger(stream) - this.HeaderAttributes.DataWindow.YMin); + if (rowStartIndex >= height) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk row index is outside the data window."); + } uint compressedBytesCount = this.ReadUnsignedInteger(stream); - decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex); + uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock); + + this.DecompressBlock(decompressor, stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); int offset = 0; for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) @@ -305,6 +325,28 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe } } + /// + /// Decompresses a block according to the configured image-data integrity policy. + /// + /// The decompressor for the stored compression type. + /// The encoded block stream. + /// The declared compressed byte count. + /// The expected byte count for the rows in this block. + /// The reusable decompressed pixel buffer. + private void DecompressBlock(ExrBaseDecompressor decompressor, BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) + { + try + { + decompressor.Decompress(stream, compressedBytes, uncompressedBytes, buffer); + } + catch (Exception ex) when (this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.IgnoreImageData && ex is InvalidImageContentException or InvalidDataException) + { + // The offset table locates the next block independently of this damaged payload. + // Discard the entire failed block so partial output or pooled bytes cannot become pixels. + buffer[..(int)uncompressedBytes].Clear(); + } + } + /// /// Reads float image channel data. /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e4ffd42823..0a06f4915b 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -261,11 +261,6 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok this.currentLocalColorTable?.Dispose(); } - if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) - { - GifThrowHelper.ThrowNoHeader(); - } - // Ignoring a malformed ancillary extension must not let identify succeed for a file // that never contained any readable image frame data. if (previousFrame is null) @@ -328,6 +323,10 @@ private void ReadLogicalScreenDescriptor(BufferedReadStream stream) } this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); + if (this.logicalScreenDescriptor.Width == 0 || this.logicalScreenDescriptor.Height == 0) + { + GifThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); + } } /// diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index 85c4959a98..aa3df4a4fd 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Gif; /// The stream to read from. /// The memory allocator. /// The XMP metadata - public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) + public static GifXmpApplicationExtension Read(BufferedReadStream stream, MemoryAllocator allocator) { byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated); if (!terminated) @@ -75,7 +75,7 @@ public int WriteTo(Span buffer) return this.ContentLength; } - private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated) + private static byte[] ReadXmpData(BufferedReadStream stream, MemoryAllocator allocator, out bool terminated) { using ChunkedMemoryStream bytes = new(allocator); diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index ae26914e8e..05cdd9cc77 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -274,10 +274,9 @@ public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder) // Get the marker length. int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; - // Check whether the stream actually has enough bytes to read - // markerContentByteSize is always positive so we cast - // to uint to avoid sign extension - if (stream.RemainingBytes < (uint)markerContentByteSize) + // Validate the entire segment before parsing it. Casting directly + // to ulong also rejects lengths smaller than the two-byte length field. + if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize)) { JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); } @@ -351,10 +350,9 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC // Get the marker length. int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; - // Check whether stream actually has enough bytes to read - // markerContentByteSize is always positive so we cast - // to uint to avoid sign extension. - if (stream.RemainingBytes < (uint)markerContentByteSize) + // Validate the entire segment before parsing it. Casting directly + // to ulong also rejects lengths smaller than the two-byte length field. + if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize)) { if (metadataOnly && this.Metadata != null && this.Frame != null) { @@ -841,7 +839,7 @@ private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remai // TODO: thumbnail if (remaining > 0) { - if (stream.Position + remaining >= stream.Length) + if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1)) { this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); stream.Skip(remaining); @@ -877,7 +875,7 @@ private void ProcessApp1Marker(BufferedReadStream stream, int remaining) return; } - if (stream.Position + remaining >= stream.Length) + if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1)) { this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); stream.Skip(remaining); diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 28f9a989b9..5e5225cdd0 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -195,11 +195,6 @@ protected override Image Decode(BufferedReadStream stream, Cance switch (chunk.Type) { case PngChunkType.Header: - if (!Equals(this.header, default(PngHeader))) - { - PngThrowHelper.ThrowInvalidHeader(); - } - this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.AnimationControl: @@ -656,7 +651,7 @@ private void InitializeImage(ImageMetadata metadata, FrameControl frameC frameMetadata.FromChunk(in frameControl); this.bytesPerPixel = this.CalculateBytesPerPixel(); - this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1; + this.bytesPerScanline = CalculateScanlineLength(this.header.Width, this.header.BitDepth, this.bytesPerPixel) + 1; this.bytesPerSample = 1; if (this.header.BitDepth >= 8) { @@ -741,21 +736,29 @@ private int CalculateBytesPerPixel() /// Calculates the scanline length. /// /// The width of the row. + /// The number of bits per sample. + /// The number of bytes per pixel. /// /// The representing the length. /// - private int CalculateScanlineLength(int width) + internal static int CalculateScanlineLength(int width, int bitDepth, int bytesPerPixel) { - int mod = this.header.BitDepth == 16 ? 16 : 8; - int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; + int mod = bitDepth == 16 ? 16 : 8; + long scanlineLength = (long)width * bitDepth * bytesPerPixel; - int amount = scanlineLength % mod; + long amount = scanlineLength % mod; if (amount != 0) { scanlineLength += mod - amount; } - return scanlineLength / mod; + scanlineLength /= mod; + if (scanlineLength >= int.MaxValue) + { + PngThrowHelper.ThrowInvalidImageContentException("PNG scanline length exceeds the supported maximum."); + } + + return (int)scanlineLength; } /// @@ -875,13 +878,13 @@ private void DecodePixelDataCore( while (currentRow < height) { cancellationToken.ThrowIfCancellationRequested(); - int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1; + int bytesPerFrameScanline = CalculateScanlineLength((int)frameControl.Width, this.header.BitDepth, this.bytesPerPixel) + 1; Span scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline]; Span prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline]; while (currentRowBytesRead < bytesPerFrameScanline) { - int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); + int bytesRead = compressedStream.Read(scanSpan.Slice(currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead)); if (bytesRead <= 0) { goto EXIT; @@ -1006,14 +1009,14 @@ private void DecodeInterlacedPixelDataCore( continue; } - int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; + int bytesPerInterlaceScanline = CalculateScanlineLength(numColumns, this.header.BitDepth, this.bytesPerPixel) + 1; while (currentRow < endRow) { cancellationToken.ThrowIfCancellationRequested(); while (currentRowBytesRead < bytesPerInterlaceScanline) { - int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead); + int bytesRead = compressedStream.Read(this.scanline.GetSpan().Slice(currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead)); if (bytesRead <= 0) { goto EXIT; @@ -1439,6 +1442,11 @@ private FrameControl ReadFrameControlChunk(ReadOnlySpan data) /// The containing data. private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) { + if (!Equals(this.header, default(PngHeader))) + { + PngThrowHelper.ThrowInvalidHeader(); + } + this.header = PngHeader.Parse(data); this.header.Validate(); @@ -1976,21 +1984,36 @@ private unsafe bool TryDecompressZlibData(ReadOnlySpan compressedData, int return false; } - int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); - while (bytesRead != 0) + try { - if (memoryStreamOutput.Length > maxLength) + int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); + while (bytesRead != 0) { - uncompressedBytesArray = []; - return false; + if (memoryStreamOutput.Length > maxLength) + { + uncompressedBytesArray = []; + return false; + } + + memoryStreamOutput.Write(destUncompressedData[..bytesRead]); + bytesRead = inflateStream.CompressedStream.Read(destUncompressedData); } - memoryStreamOutput.Write(destUncompressedData[..bytesRead]); - bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); + uncompressedBytesArray = memoryStreamOutput.ToArray(); + return true; } + catch (InvalidDataException ex) + { + // ICC and text chunks are already bounded in memory, so rejecting their compressed contents + // does not lose the next chunk boundary. Apply the ancillary policy without keeping partial output. + if (this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.Strict) + { + throw new InvalidImageContentException("Invalid compressed PNG metadata.", ex); + } - uncompressedBytesArray = memoryStreamOutput.ToArray(); - return true; + uncompressedBytesArray = []; + return false; + } } } diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index ead157986a..86f80530fb 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -72,9 +72,9 @@ protected override Image Decode(BufferedReadStream stream, Cance TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); } - if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0) + if (this.fileHeader.Width <= 0 || this.fileHeader.Height <= 0) { - throw new UnknownImageFormatException("Width or height cannot be 0"); + TgaThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0."); } Image image = new(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index dc9e1e7296..74914e036f 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -126,6 +126,14 @@ protected override void CompressStrip(Span pixelsAsGray, int height, Span< } } + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // A pixel can require a 13-bit terminating code. Each row can also require + // an 8-bit zero-length white run and a 12-bit EOL, plus the initial EOL. + return 12L + ((((long)this.Width * 13) + 20) * rowsPerStrip); + } + private void WriteEndOfLine(Span compressedData) { if (this.useModifiedHuffman) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs index cffc96fcdf..6dba8b7d25 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -131,6 +131,14 @@ protected override void CompressStrip(Span pixelsAsGray, int height, Span< this.WriteCode(12, 1, compressedData); } + /// + protected override long GetMaximumEncodedBits(int rowsPerStrip) + { + // Alternating pixels use at most 29 bits per two-pixel horizontal mode. + // Allow 16 bits per pixel, row transition overhead, and the final 24-bit EOFB. + return ((((long)this.Width * 16) + 24) * rowsPerStrip) + 24; + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs index 8e2227cba5..06cf84437a 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -465,6 +465,12 @@ protected void PadByte() /// The destination buffer to write the code to. protected void WriteCode(uint codeLength, uint code, Span compressedData) { + long availableBits = (((long)compressedData.Length - this.bytePosition) * 8) - this.bitPosition; + if (codeLength > availableBits) + { + throw new InvalidMemoryOperationException("The CCITT output buffer is too small for the encoded data."); + } + while (codeLength > 0) { int bitNumber = (int)codeLength; @@ -526,8 +532,20 @@ public override void CompressStrip(Span rows, int height) /// public override void Initialize(int rowsPerStrip) { - // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. - int maxNeededBytes = this.Width * rowsPerStrip; - this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + long maxNeededBits = this.GetMaximumEncodedBits(rowsPerStrip); + ulong maxNeededBytes = (ulong)((maxNeededBits + 7) / 8); + if (maxNeededBytes > int.MaxValue) + { + InvalidMemoryOperationException.ThrowAllocationOverLimitException(maxNeededBytes, int.MaxValue); + } + + this.compressedDataBuffer = this.Allocator.Allocate((int)maxNeededBytes); } + + /// + /// Gets an upper bound for the encoded strip length in bits. + /// + /// The number of rows in the strip. + /// The maximum encoded length. + protected abstract long GetMaximumEncodedBits(int rowsPerStrip); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs index d3b65c537c..a09a39b52b 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs @@ -69,7 +69,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int int totalRead = 0; while (totalRead < buffer.Length) { - int bytesRead = dataStream.Read(buffer, totalRead, buffer.Length - totalRead); + int bytesRead = dataStream.Read(buffer[totalRead..]); if (bytesRead <= 0) { break; diff --git a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs index 2b843cc8f6..4276199ee0 100644 --- a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs +++ b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs @@ -34,7 +34,7 @@ protected static IMemoryOwner ReadImageDataFromStream(Stream input, int by { IMemoryOwner data = memoryAllocator.Allocate(bytesToRead, AllocationOptions.Clean); Span dataSpan = data.Memory.Span; - input.Read(dataSpan[..bytesToRead], 0, bytesToRead); + input.Read(dataSpan[..bytesToRead]); return data; } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 39c4beb618..cdf1470ddb 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -143,6 +143,7 @@ public static void WriteTrunksAfterData( { if (exifProfile != null) { + // Serialization applies Parts even when the current profile has not been initialized. RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); } diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs index 7d22f7f2b3..e7b135dc96 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.IO; + namespace SixLabors.ImageSharp.Formats.Webp.Chunks; internal readonly struct WebpFrameData @@ -120,7 +122,7 @@ public long WriteHeaderTo(Stream stream) /// /// The stream to read from. /// Animation frame data. - public static WebpFrameData Parse(Stream stream) + public static WebpFrameData Parse(BufferedReadStream stream) { Span buffer = stackalloc byte[4]; diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 8a8ad823dc..323ee30b90 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -384,7 +384,9 @@ private void ReadOptionalChunk( // While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must // precede the frame data, and we cannot safely skip it without successfully reading its size. - WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata); + // ReadIccProfile therefore validates the complete chunk extent before invoking the ancillary + // handler. Only errors in the contents of a complete chunk follow that recovery policy. + WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata, this.executeAncillarySegmentAction); break; case WebpChunkType.Exif: this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata)); diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs index 119d53fe96..958f4df80b 100644 --- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs +++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs @@ -262,7 +262,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span /// /// Thrown if the input stream is not valid. /// - public static uint ReadUInt24LittleEndian(Stream stream, Span buffer) + public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer) { if (stream.Read(buffer, 0, 3) == 3) { @@ -306,12 +306,33 @@ public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data) /// If true, the chunk size is required to be read, otherwise it can be skipped. /// The chunk size in bytes. /// Thrown if the input stream is not valid. - public static uint ReadChunkSize(Stream stream, Span buffer, bool required = true) + public static uint ReadChunkSize(BufferedReadStream stream, Span buffer, bool required = true) + { + ulong chunkSize = ReadPaddedChunkSize(stream, buffer, required); + + // Structural chunk sizes must remain representable by their uint-sized consumers. + // Metadata readers retain the wider extent so their recovery can skip it safely. + if (chunkSize > uint.MaxValue) + { + WebpThrowHelper.ThrowInvalidImageContentException("WebP chunk size exceeds the supported maximum."); + } + + return (uint)chunkSize; + } + + /// + /// Reads a chunk's complete padded extent without wrapping a uint-sized payload length. + /// + /// The input stream. + /// The four-byte size buffer. + /// Whether an incomplete size field is an error. + /// The padded extent, or remaining bytes when an optional size field is incomplete. + private static ulong ReadPaddedChunkSize(BufferedReadStream stream, Span buffer, bool required) { if (stream.Read(buffer) is 4) { uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); - return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; + return (ulong)chunkSize + (chunkSize & 1); } if (required) @@ -320,7 +341,7 @@ public static uint ReadChunkSize(Stream stream, Span buffer, bool required } // Return the size of the remaining data in the stream. - return (uint)(stream.Length - stream.Position); + return (ulong)stream.RemainingBytes; } /// @@ -349,34 +370,36 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span /// The stream to decode from. /// The image metadata. /// If true, metadata will be ignored. + /// Executes profile parsing under the decoder's integrity policy. public static void ReadIccProfile( BufferedReadStream stream, ImageMetadata metadata, - bool ignoreMetadata) + bool ignoreMetadata, + Action executeAncillarySegmentAction) { - Span buffer = stackalloc byte[4]; - uint iccpChunkSize = ReadChunkSize(stream, buffer); - if (ignoreMetadata || metadata.IccProfile != null) + ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], true); + + // ICCP precedes image/frame data. Its framing must be readable even when + // metadata is skipped; otherwise there is no safe location to resume decoding. + if (!stream.IsReadRangeValid(stream.Position, chunkSize)) { - stream.Skip((int)iccpChunkSize); + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the ICCP chunk."); } - else + + executeAncillarySegmentAction(() => { - byte[] iccpData = new byte[iccpChunkSize]; - int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize); - if (bytesRead != iccpChunkSize) + byte[]? iccpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.IccProfile != null); + if (iccpData is not null) { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); - } + IccProfile profile = new(iccpData); + if (!profile.CheckIsValid()) + { + throw new InvalidIccProfileException("Invalid ICC profile."); + } - IccProfile profile = new(iccpData); - if (!profile.CheckIsValid()) - { - throw new InvalidIccProfileException("Invalid ICC profile."); + metadata.IccProfile = profile; } - - metadata.IccProfile = profile; - } + }); } /// @@ -390,21 +413,10 @@ public static void ReadExifProfile( ImageMetadata metadata, bool ignoreMetadata) { - Span buffer = stackalloc byte[4]; - uint exifChunkSize = ReadChunkSize(stream, buffer); - if (ignoreMetadata || metadata.ExifProfile != null) - { - stream.Skip((int)exifChunkSize); - } - else + ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata); + byte[]? exifData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.ExifProfile != null); + if (exifData is not null) { - byte[] exifData = new byte[exifChunkSize]; - int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize); - if (bytesRead != exifChunkSize) - { - WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile"); - } - ExifProfile exifProfile = new(exifData); // Set the resolution from the metadata. @@ -433,23 +445,46 @@ public static void ReadXmpProfile( ImageMetadata metadata, bool ignoreMetadata) { - Span buffer = stackalloc byte[4]; - uint xmpChunkSize = ReadChunkSize(stream, buffer); - if (ignoreMetadata || metadata.XmpProfile != null) + ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata); + byte[]? xmpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.XmpProfile != null); + if (xmpData is not null) + { + metadata.XmpProfile = new XmpProfile(xmpData); + } + } + + /// + /// Reads a metadata payload, leaving the stream at the next chunk or EOF on a recoverable error. + /// Callers execute metadata parsing under the decoder's ancillary integrity policy. + /// + /// The input stream positioned at the chunk payload. + /// The declared extent including its padding byte. + /// Whether to skip the payload without parsing it. + /// The payload, or null when metadata is skipped. + private static byte[]? ReadMetadataChunk(BufferedReadStream stream, ulong paddedLength, bool ignoreMetadata) + { + long chunkEnd = stream.Position + (long)Math.Min(paddedLength, (ulong)stream.RemainingBytes); + if (ignoreMetadata) { - stream.Skip((int)xmpChunkSize); + stream.Position = chunkEnd; + return null; } - else + + if (!stream.TryGetReadLength(paddedLength, out int bufferLength)) { - byte[] xmpData = new byte[xmpChunkSize]; - int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize); - if (bytesRead != xmpChunkSize) - { - WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile"); - } + // Ignoring an ancillary error must not make the next parser interpret + // this payload as another chunk header. A truncated chunk consumes EOF. + stream.Position = chunkEnd; + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk."); + } - metadata.XmpProfile = new XmpProfile(xmpData); + byte[] data = new byte[bufferLength]; + if (stream.Read(data) != bufferLength) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk."); } + + return data; } private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 55bdca69cb..c2a4e44653 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -92,6 +92,13 @@ protected override Image Decode(BufferedReadStream stream, Cance return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } + // A VP8X header alone describes a canvas, not decodable image data. + // Ignoring a truncated optional chunk must not bypass this requirement. + if (this.webImageInfo.Vp8BitReader is null && this.webImageInfo.Vp8LBitReader is null) + { + WebpThrowHelper.ThrowInvalidImageContentException("Missing WebP image data."); + } + image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); if (this.webImageInfo.IsLossless) @@ -281,7 +288,9 @@ private bool ParseOptionalExtendedChunks( // While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must // precede the image data, and we cannot safely skip it without successfully reading its size. - WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata); + // ReadIccProfile therefore validates the complete chunk extent before invoking the ancillary + // handler. Only errors in the contents of a complete chunk follow that recovery policy. + WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, this.ExecuteAncillarySegmentAction); break; case WebpChunkType.Exif: @@ -330,17 +339,17 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada { // Read chunk header. WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); - if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) + if (chunkType == WebpChunkType.Exif) { this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); } - else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null) + else if (chunkType == WebpChunkType.Xmp) { this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata)); } else { - // Skip duplicate XMP or EXIF chunk. + // Skip unknown chunks. uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); stream.Skip((int)chunkLength); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index aa2eb29e79..6cd16aa9c2 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -221,14 +221,21 @@ public void SetValue(ExifTag tag, TValueType value) => this.SetValueInternal(tag, value); /// - /// Converts this instance to a byte array. + /// Converts the sections selected by to a byte array. /// /// The public byte[]? ToByteArray() { if (this.values is null) { - return this.data; + // The original bytes include every section. They can only be reused when no filtering + // is requested; otherwise lazy profiles must go through the same writer as initialized ones. + if (this.Parts == ExifParts.All) + { + return this.data; + } + + this.InitializeValues(); } if (this.values.Count == 0) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 4cd4b4aac9..4e2315cd88 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -224,6 +224,13 @@ protected void ReadValues64(List values, ulong offset) this.Seek(offset); ulong count = this.ReadUInt64(); + // Each entry occupies 20 bytes and the directory ends with an 8-byte next-IFD offset. + long remainingDirectoryBytes = this.data.Length - this.data.Position; + if (remainingDirectoryBytes < 8 || count > (ulong)((remainingDirectoryBytes - 8) / 20)) + { + throw new InvalidImageContentException("The BigTIFF directory entry count exceeds the available data."); + } + Span offsetBuffer = stackalloc byte[8]; for (ulong i = 0; i < count; i++) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs index 700e43f972..da2f37efd3 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs @@ -72,13 +72,7 @@ public IccClut ReadClut(int inChannelCount, int outChannelCount, bool isFloat) /// The read CLUT8. public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount) { - int length = 0; - for (int i = 0; i < inChannelCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChannelCount); - } - - length /= inChannelCount; + int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 1); const float Max = byte.MaxValue; @@ -105,13 +99,7 @@ public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPoi public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChannelCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChannelCount); - } - - length /= inChannelCount; + int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 2); const float Max = ushort.MaxValue; @@ -139,13 +127,7 @@ public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPo public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) { int start = this.currentIndex; - int length = 0; - for (int i = 0; i < inChCount; i++) - { - length += (int)Math.Pow(gridPointCount[i], inChCount); - } - - length /= inChCount; + int length = this.GetClutLength(inChCount, outChCount, gridPointCount, 4); float[] values = new float[length * outChCount]; int offset = 0; @@ -160,4 +142,28 @@ public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) this.currentIndex = start + (length * outChCount * 4); return new IccClut(values, gridPointCount, IccClutDataType.Float, outChCount); } + + private int GetClutLength(int inputChannelCount, int outputChannelCount, byte[] gridPointCount, int bytesPerValue) + { + int length = 1; + for (int i = 0; i < inputChannelCount; i++) + { + int gridPoints = gridPointCount[i]; + if (gridPoints == 0 || length > int.MaxValue / gridPoints) + { + throw new InvalidIccProfileException("Invalid CLUT dimensions."); + } + + length *= gridPoints; + } + + long valueCount = (long)length * outputChannelCount; + long byteCount = valueCount * bytesPerValue; + if (valueCount > int.MaxValue || byteCount > this.data.Length - this.currentIndex) + { + throw new InvalidIccProfileException("The CLUT data is shorter than its declared dimensions."); + } + + return length; + } } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 084ec388d6..d4ee3bded1 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -119,6 +119,7 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader) } List table = new((int)tagCount); + uint dataLength = (uint)reader.DataLength; for (int i = 0; i < tagCount; i++) { uint tagSignature = reader.ReadUInt32(); @@ -126,7 +127,7 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader) uint tagSize = reader.ReadUInt32(); // Exclude entries that have nonsense values and could cause exceptions further on - if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128) + if (tagSize >= 8 && tagOffset <= dataLength && tagSize <= dataLength - tagOffset) { table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize)); } diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs index 53f22eb09a..6de4b30b9e 100644 --- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs +++ b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.PixelFormats; @@ -50,52 +51,228 @@ internal static class HalfTypeHelper internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); /// - /// Normalizes a finite binary16 value to the scaled pixel range. + /// Normalizes a binary16 value to [0, 1], saturating infinities and mapping NaN to zero. /// /// The native binary16 value represented as a . /// The normalized value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint; + public static float ToScaled(float value) + { + // Clamp after mapping so native infinities reach the scaled endpoints and NaN becomes zero. + return Numerics.Clamp((value * InverseFiniteRange) + ScaledMidpoint, 0F, 1F); + } /// - /// Normalizes finite binary16 values to the scaled pixel range. + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. /// /// The native binary16 values. /// The normalized values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint); + public static Vector2 ToScaled(Vector2 value) => ToScaled(value.AsVector128()).AsVector2(); /// - /// Normalizes finite binary16 values to the scaled pixel range. + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. /// /// The native binary16 values. /// The normalized values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint); + public static Vector4 ToScaled(Vector4 value) => ToScaled(value.AsVector128()).AsVector4(); + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 ToScaled(Vector128 value) + { + Vector128 scaled = (value * Vector128.Create(InverseFiniteRange)) + Vector128.Create(ScaledMidpoint); + + return Numerics.Clamp(scaled, Vector128.Zero, Vector128.One); + } + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 ToScaled(Vector256 value) + { + Vector256 scaled = (value * Vector256.Create(InverseFiniteRange)) + Vector256.Create(ScaledMidpoint); + + return Numerics.Clamp(scaled, Vector256.Zero, Vector256.One); + } + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 ToScaled(Vector512 value) + { + Vector512 scaled = (value * Vector512.Create(InverseFiniteRange)) + Vector512.Create(ScaledMidpoint); + + return Numerics.Clamp(scaled, Vector512.Zero, Vector512.One); + } + + /// + /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero. + /// + /// The component values to convert in place. + public static void ToScaled(Span values) + { + ref Vector4 source = ref MemoryMarshal.GetReference(values); + int i = 0; + + // Each register contains whole RGBA pixels. Convert wide groups first, then narrower + // remainders without revisiting any pixel: mapping the same pixel twice would change its value. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector512.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = ToScaled(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = ToScaled(vector); + } + } + + // One Vector4 uses the same 128-bit conversion as an individual pixel, including the + // runtime's software fallback when SIMD is unavailable. No separate scalar mapping is needed. + for (; i < values.Length; i++) + { + ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i); + + vector = ToScaled(vector); + } + } /// - /// Expands a normalized value to the finite binary16 range. + /// Normalizes a scaled value, mapping NaN to zero, and expands it to the finite binary16 range. /// /// The normalized value. /// The native binary16 value represented as a . [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum; + public static float FromScaled(float value) + { + // Clamp before expanding so nonfinite scaled input cannot become nonfinite half storage. + return (Numerics.Clamp(value, 0F, 1F) * FiniteRange) + FiniteMinimum; + } /// - /// Expands normalized values to the finite binary16 range. + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. /// /// The normalized values. /// The native binary16 values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum); + public static Vector2 FromScaled(Vector2 value) => FromScaled(value.AsVector128()).AsVector2(); /// - /// Expands normalized values to the finite binary16 range. + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. /// /// The normalized values. /// The native binary16 values. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum); + public static Vector4 FromScaled(Vector4 value) => FromScaled(value.AsVector128()).AsVector4(); + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 FromScaled(Vector128 value) + { + Vector128 scaled = Numerics.Clamp(value, Vector128.Zero, Vector128.One); + + return (scaled * Vector128.Create(FiniteRange)) + Vector128.Create(FiniteMinimum); + } + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 FromScaled(Vector256 value) + { + Vector256 scaled = Numerics.Clamp(value, Vector256.Zero, Vector256.One); + + return (scaled * Vector256.Create(FiniteRange)) + Vector256.Create(FiniteMinimum); + } + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values. + /// The converted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 FromScaled(Vector512 value) + { + Vector512 scaled = Numerics.Clamp(value, Vector512.Zero, Vector512.One); + + return (scaled * Vector512.Create(FiniteRange)) + Vector512.Create(FiniteMinimum); + } + + /// + /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range. + /// + /// The component values to convert in place. + public static void FromScaled(Span values) + { + ref Vector4 source = ref MemoryMarshal.GetReference(values); + int i = 0; + + // Each register contains whole RGBA pixels. Clamping and expansion happen together in + // the conversion overload, so each pixel is loaded and stored once without a clamp-only pass. + if (Vector512.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector512.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = FromScaled(vector); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int pixelsPerRegister = Vector256.Count / Vector128.Count; + + for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister) + { + ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i)); + + vector = FromScaled(vector); + } + } + + // The remaining whole pixels use the same 128-bit conversion as individual pixels, + // or its software fallback. Narrowing the remainder never reprocesses a converted pixel. + for (; i < values.Length; i++) + { + ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i); + + vector = FromScaled(vector); + } + } /// /// Unpacks eight binary16 values into two vectors of single-precision values. diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs index 50fc272553..ca90afcf75 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs @@ -250,7 +250,7 @@ private static HalfVector4P PackAssociatedScaledVector4(Vector4 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float QuantizeScaledAlpha(float alpha) { - float nativeAlpha = HalfTypeHelper.FromScaled(Numerics.Clamp(alpha, 0F, 1F)); + float nativeAlpha = HalfTypeHelper.FromScaled(alpha); return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha))); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs index 9eae7d5cbc..a921f99bae 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs @@ -3,7 +3,6 @@ using System.Numerics; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.PixelFormats.Utils; namespace SixLabors.ImageSharp.PixelFormats; @@ -17,11 +16,6 @@ public partial struct HalfVector4 /// internal class PixelOperations : PixelOperations { - private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); - private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); - private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); - private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); - /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { @@ -40,9 +34,9 @@ protected override void ToAssociatedVector4(Configuration configuration, ReadOnl // Association uses normalized opacity, not the native binary16 alpha value. RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(destination); Numerics.Premultiply(destination); - Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(destination); } /// @@ -52,7 +46,7 @@ protected override void ToUnassociatedScaledVector4(Configuration configuration, destination = destination[..source.Length]; RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(destination); } /// @@ -77,9 +71,9 @@ protected override void FromAssociatedVector4Destructive(Configuration configura Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); // Restore normalized opacity before unassociating, then return the result to the native binary16 range. - Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(source); Numerics.UnPremultiply(source); - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } @@ -88,7 +82,7 @@ protected override void FromUnassociatedScaledVector4Destructive(Configuration c { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } @@ -98,7 +92,7 @@ protected override void FromAssociatedScaledVector4Destructive(Configuration con Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Numerics.UnPremultiply(source); - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length])); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs index 52b2dbf376..172159a49c 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs @@ -20,16 +20,11 @@ public partial struct HalfVector4P /// internal class PixelOperations : AssociatedAlphaPixelOperations { - private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange); - private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint); - private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange); - private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum); - /// protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) { this.ToUnassociatedScaledVector4(configuration, source, destination); - Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(destination[..source.Length]); } /// @@ -54,7 +49,7 @@ protected override void ToAssociatedScaledVector4(Configuration configuration, R destination = destination[..source.Length]; RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination); - Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(destination); } /// @@ -62,7 +57,7 @@ protected override void FromUnassociatedVector4Destructive(Configuration configu { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(source); Associate(source); PackAssociatedScaled(source, destination[..source.Length]); } @@ -72,7 +67,7 @@ protected override void FromAssociatedVector4Destructive(Configuration configura { Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); - Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); + HalfTypeHelper.ToScaled(source); Reassociate(source); PackAssociatedScaled(source, destination[..source.Length]); } @@ -250,7 +245,9 @@ private static Vector128 Reassociate(Vector128 source) Vector128 storedAlpha = QuantizeScaledAlpha(alpha); Vector128 result = source * (storedAlpha / alpha); result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); } @@ -267,7 +264,9 @@ private static Vector256 Reassociate(Vector256 source) Vector256 storedAlpha = QuantizeScaledAlpha(alpha); Vector256 result = source * (storedAlpha / alpha); result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); } @@ -285,7 +284,9 @@ private static Vector512 Reassociate(Vector512 source) Vector512 result = source * (storedAlpha / alpha); Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); } @@ -297,8 +298,8 @@ private static Vector512 Reassociate(Vector512 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 QuantizeScaledAlpha(Vector128 alpha) { - Vector128 native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum); - return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint); + Vector128 native = HalfTypeHelper.FromScaled(alpha); + return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); } /// @@ -309,8 +310,8 @@ private static Vector128 QuantizeScaledAlpha(Vector128 alpha) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 QuantizeScaledAlpha(Vector256 alpha) { - Vector256 native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum); - return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint); + Vector256 native = HalfTypeHelper.FromScaled(alpha); + return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); } /// @@ -321,45 +322,33 @@ private static Vector256 QuantizeScaledAlpha(Vector256 alpha) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector512 QuantizeScaledAlpha(Vector512 alpha) { - Vector512 native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum); - return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint); + Vector512 native = HalfTypeHelper.FromScaled(alpha); + return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native)); } /// - /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// Clamps vectors to the scaled color range, mapping NaN lanes to zero. /// /// The vectors to clamp. /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ClampUnit(Vector128 source) - { - Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); - return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); - } + private static Vector128 ClampUnit(Vector128 source) => Numerics.Clamp(source, Vector128.Zero, Vector128.One); /// - /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// Clamps vectors to the scaled color range, mapping NaN lanes to zero. /// /// The vectors to clamp. /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ClampUnit(Vector256 source) - { - Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); - return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); - } + private static Vector256 ClampUnit(Vector256 source) => Numerics.Clamp(source, Vector256.Zero, Vector256.One); /// - /// Clamps vectors to the scaled color range while preserving NaN lanes. + /// Clamps vectors to the scaled color range, mapping NaN lanes to zero. /// /// The vectors to clamp. /// The clamped vectors. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ClampUnit(Vector512 source) - { - Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); - return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); - } + private static Vector512 ClampUnit(Vector512 source) => Numerics.Clamp(source, Vector512.Zero, Vector512.One); /// /// Maps associated scaled vectors to native components and packs them as binary16 values. @@ -368,7 +357,7 @@ private static Vector512 ClampUnit(Vector512 source) /// The destination pixels. private static void PackAssociatedScaled(Span source, Span destination) { - Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); + HalfTypeHelper.FromScaled(source); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination)); } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs index 825ebeec92..a9f3081f1a 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs @@ -277,8 +277,8 @@ internal static void Pack(Span source, Span destination) { for (; i <= componentCount - Vector512.Count; i += Vector512.Count) { - Vector512 lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector512 upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))); + Vector512 lower = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), Vector512.Zero, Vector512.One); + Vector512 upper = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)), Vector512.Zero, Vector512.One); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -287,8 +287,8 @@ internal static void Pack(Span source, Span destination) { for (; i <= componentCount - Vector256.Count; i += Vector256.Count) { - Vector256 lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector256 upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))); + Vector256 lower = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), Vector256.Zero, Vector256.One); + Vector256 upper = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)), Vector256.Zero, Vector256.One); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -297,15 +297,15 @@ internal static void Pack(Span source, Span destination) { for (; i <= componentCount - Vector128.Count; i += Vector128.Count) { - Vector128 lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); - Vector128 upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))); + Vector128 lower = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128.Zero, Vector128.One); + Vector128 upper = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)), Vector128.Zero, Vector128.One); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } if (i < componentCount) { // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. - Vector128 vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); + Vector128 vector = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128.Zero, Vector128.One); Vector128 packed = HalfTypeHelper.Pack(vector, vector); Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); } @@ -401,8 +401,8 @@ internal static void PackFromAssociated(Span source, Span de { for (; i <= componentCount - Vector512.Count; i += Vector512.Count) { - Vector512 lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i))); - Vector512 upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)))); + Vector512 lower = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)), Vector512.Zero, Vector512.One); + Vector512 upper = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))), Vector512.Zero, Vector512.One); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -411,8 +411,8 @@ internal static void PackFromAssociated(Span source, Span de { for (; i <= componentCount - Vector256.Count; i += Vector256.Count) { - Vector256 lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i))); - Vector256 upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)))); + Vector256 lower = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)), Vector256.Zero, Vector256.One); + Vector256 upper = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))), Vector256.Zero, Vector256.One); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } } @@ -421,15 +421,15 @@ internal static void PackFromAssociated(Span source, Span de { for (; i <= componentCount - Vector128.Count; i += Vector128.Count) { - Vector128 lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); - Vector128 upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)))); + Vector128 lower = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128.Zero, Vector128.One); + Vector128 upper = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))), Vector128.Zero, Vector128.One); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); } if (i < componentCount) { // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. - Vector128 vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); + Vector128 vector = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128.Zero, Vector128.One); Vector128 packed = HalfTypeHelper.Pack(vector, vector); Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); } @@ -649,48 +649,6 @@ private static Vector512 Unassociate(Vector512 source) return Numerics.UnPremultiply(source, alpha); } - /// - /// Clamps vectors to the unit range represented by the pixel format. - /// - /// The vectors to clamp. - /// The clamped vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 ClampUnit(Vector128 source) - { - Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One); - - // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. - return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source); - } - - /// - /// Clamps vectors to the unit range represented by the pixel format. - /// - /// The vectors to clamp. - /// The clamped vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ClampUnit(Vector256 source) - { - Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One); - - // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. - return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source); - } - - /// - /// Clamps vectors to the unit range represented by the pixel format. - /// - /// The vectors to clamp. - /// The clamped vectors. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector512 ClampUnit(Vector512 source) - { - Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One); - - // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract. - return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source); - } - /// /// Associates unassociated vectors with the alpha value binary16 storage can reproduce. /// @@ -699,7 +657,7 @@ private static Vector512 ClampUnit(Vector512 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 AssociateForStorage(Vector128 source) { - source = ClampUnit(source); + source = Numerics.Clamp(source, Vector128.Zero, Vector128.One); Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector128 result = source * storedAlpha; @@ -714,7 +672,7 @@ private static Vector128 AssociateForStorage(Vector128 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector256 AssociateForStorage(Vector256 source) { - source = ClampUnit(source); + source = Numerics.Clamp(source, Vector256.Zero, Vector256.One); Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector256 result = source * storedAlpha; @@ -729,7 +687,7 @@ private static Vector256 AssociateForStorage(Vector256 source) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector512 AssociateForStorage(Vector512 source) { - source = ClampUnit(source); + source = Numerics.Clamp(source, Vector512.Zero, Vector512.One); Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector512 result = source * storedAlpha; @@ -747,11 +705,13 @@ private static Vector128 ReassociateForStorage(Vector128 source) { Vector128 zero = Vector128.Zero; Vector128 alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); - Vector128 clampedAlpha = ClampUnit(alpha); + Vector128 clampedAlpha = Numerics.Clamp(alpha, Vector128.Zero, Vector128.One); Vector128 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector128 result = source * (storedAlpha / alpha); result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector128.Min(Vector128.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result); } @@ -765,11 +725,13 @@ private static Vector256 ReassociateForStorage(Vector256 source) { Vector256 zero = Vector256.Zero; Vector256 alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); - Vector256 clampedAlpha = ClampUnit(alpha); + Vector256 clampedAlpha = Numerics.Clamp(alpha, Vector256.Zero, Vector256.One); Vector256 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector256 result = source * (storedAlpha / alpha); result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); - result = Vector256.Min(Vector256.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result); } @@ -783,12 +745,14 @@ private static Vector512 ReassociateForStorage(Vector512 source) { Vector512 zero = Vector512.Zero; Vector512 alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); - Vector512 clampedAlpha = ClampUnit(alpha); + Vector512 clampedAlpha = Numerics.Clamp(alpha, Vector512.Zero, Vector512.One); Vector512 storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector512 result = source * (storedAlpha / alpha); Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result); - result = Vector512.Min(Vector512.Max(result, zero), storedAlpha); + + // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB. + result = Numerics.Clamp(result, zero, storedAlpha); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result); } } diff --git a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs index 96d81617b2..32d6521dff 100644 --- a/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs +++ b/src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs @@ -71,12 +71,7 @@ public MultiplyThenAddOperator(Vector4 multiplier, Vector4 offset) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 Invoke(Vector4 source) - { - Vector128 result = (source.AsVector128() * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower(); - - return result.AsVector4(); - } + public Vector4 Invoke(Vector4 source) => this.Invoke(source.AsVector128()).AsVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/tests/ImageSharp.Tests/Common/BufferedReadStreamExtensionsTests.cs b/tests/ImageSharp.Tests/Common/BufferedReadStreamExtensionsTests.cs new file mode 100644 index 0000000000..6bef711931 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/BufferedReadStreamExtensionsTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Tests.Common; + +public class BufferedReadStreamExtensionsTests +{ + [Theory] + [InlineData(0L, 8UL, true)] + [InlineData(8L, 0UL, true)] + [InlineData(7L, 2UL, false)] + [InlineData(9L, 0UL, false)] + [InlineData(-1L, 1UL, false)] + [InlineData(long.MaxValue, ulong.MaxValue, false)] + [InlineData(0L, ulong.MaxValue, false)] + public void IsReadRangeValid_ChecksCompleteExtent(long offset, ulong length, bool expected) + { + using MemoryStream input = new(new byte[8]); + using BufferedReadStream stream = new(Configuration.Default, input); + + Assert.Equal(expected, stream.IsReadRangeValid(offset, length)); + Assert.Equal(0, stream.Position); + } + + [Theory] + [InlineData(0UL, true, 0)] + [InlineData(6UL, true, 6)] + [InlineData(7UL, false, 0)] + [InlineData(1073741824UL, false, 0)] + [InlineData(4294967294UL, false, 0)] + [InlineData(4294967296UL, false, 0)] + [InlineData(ulong.MaxValue, false, 0)] + public void TryGetReadLength_ReturnsResultWithoutMovingStream(ulong length, bool expected, int expectedLength) + { + using MemoryStream input = new(new byte[8]); + using BufferedReadStream stream = new(Configuration.Default, input); + stream.Position = 2; + + Assert.Equal(expected, stream.TryGetReadLength(length, out int bufferLength)); + Assert.Equal(expectedLength, bufferLength); + Assert.Equal(2, stream.Position); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void Skip_CountZeroOrLower_PositionNotChanged(int count) + { + using MemoryStream input = new(new byte[8]); + using BufferedReadStream stream = new(Configuration.Default, input); + stream.Position = 4; + + stream.Skip(count); + + Assert.Equal(4, stream.Position); + Assert.Equal(0, stream.ReadByte()); + } +} diff --git a/tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs b/tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs deleted file mode 100644 index 5ea7afaf80..0000000000 --- a/tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Tests.Common; - -public class StreamExtensionsTests -{ - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void Skip_CountZeroOrLower_PositionNotChanged(int count) - { - using (MemoryStream memStream = new(5)) - { - memStream.Position = 4; - memStream.Skip(count); - - Assert.Equal(4, memStream.Position); - } - } - - [Fact] - public void Skip_SeekableStream_SeekIsCalled() - { - using (SeekableStream seekableStream = new(4)) - { - seekableStream.Skip(4); - - Assert.Equal(4, seekableStream.Offset); - Assert.Equal(SeekOrigin.Current, seekableStream.Loc); - } - } - - [Fact] - public void Skip_NonSeekableStream_BytesAreRead() - { - using (NonSeekableStream nonSeekableStream = new()) - { - nonSeekableStream.Skip(5); - - Assert.Equal(3, nonSeekableStream.Counts.Count); - - Assert.Equal(5, nonSeekableStream.Counts[0]); - Assert.Equal(3, nonSeekableStream.Counts[1]); - Assert.Equal(1, nonSeekableStream.Counts[2]); - } - } - - [Fact] - public void Skip_EofStream_NoExceptionIsThrown() - { - using (EofStream eofStream = new(7)) - { - eofStream.Skip(7); - - Assert.Equal(0, eofStream.Position); - } - } - - private class SeekableStream : MemoryStream - { - public long Offset; - public SeekOrigin Loc; - - public SeekableStream(int capacity) - : base(capacity) - { - } - - public override long Seek(long offset, SeekOrigin loc) - { - this.Offset = offset; - this.Loc = loc; - return base.Seek(offset, loc); - } - } - - private class NonSeekableStream : MemoryStream - { - public override bool CanSeek => false; - - public List Counts = new(); - - public NonSeekableStream() - : base(4) - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - this.Counts.Add(count); - - return Math.Min(2, count); - } - } - - private class EofStream : MemoryStream - { - public override bool CanSeek => false; - - public EofStream(int capacity) - : base(capacity) - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - return 0; - } - } -} diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index e85c6bcdf7..1702657892 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -34,6 +34,41 @@ public class BmpDecoderTests { RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } }; + [Theory] + [InlineData(SegmentIntegrityHandling.Strict, false)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary, false)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData, false)] + [InlineData(SegmentIntegrityHandling.Strict, true)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary, true)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData, true)] + public void Decode_WithProfileLargerThanRemainingData_RespectsOptions(SegmentIntegrityHandling integrityHandling, bool skipMetadata) + { + byte[] payload = Convert.FromHexString( + "424D8E000000000000008A0000007C0000000100000001000000010018000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "000000000000000000000000000000000000000000000000000000000000C800" + + "00000000004000000000000000"); + DecoderOptions options = new() { SegmentIntegrityHandling = integrityHandling, SkipMetadata = skipMetadata }; + + if (integrityHandling is SegmentIntegrityHandling.Strict && !skipMetadata) + { + Assert.Throws(() => Image.Load(options, payload)); + Assert.Throws(() => Image.Identify(options, payload)); + } + else + { + using Image image = Image.Load(options, payload); + Assert.Equal(new Size(1, 1), image.Size); + Assert.Equal(new Rgba32(0, 0, 0), image[0, 0]); + Assert.Null(image.Metadata.IccProfile); + + ImageInfo info = Image.Identify(options, payload); + Assert.Equal(image.Size, info.Size); + Assert.Null(info.Metadata.IccProfile); + } + } + [Theory] [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)] public void BmpDecoder_CanDecode_MiscellaneousBitmaps(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs new file mode 100644 index 0000000000..1a7632d5e3 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs @@ -0,0 +1,248 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.IO.Compression; +using System.Numerics; +using System.Text; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrZipDecoderTests +{ + /// + /// Incomplete and oversized blocks are rejected unless image-data recovery is enabled. + /// + /// The inflated payload length. + /// The image-data integrity policy. + [Theory] + [InlineData(0, SegmentIntegrityHandling.Strict)] + [InlineData(8, SegmentIntegrityHandling.Strict)] + [InlineData(1025, SegmentIntegrityHandling.Strict)] + [InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(8, SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(1025, SegmentIntegrityHandling.IgnoreAncillary)] + public void Decode_InvalidInflatedBlock_Throws(int length, SegmentIntegrityHandling integrity) + { + byte[] data = BuildExr(ZlibCompress(new byte[length]), ExrPixelType.Float, 2, 0); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + + Assert.Throws(() => Image.Load(options, data)); + } + + /// + /// Recovering an invalid image-data block must not expose partially decoded or pooled bytes. + /// + /// The stored sample type. + /// The inflated payload length. + [Theory] + [InlineData(ExrPixelType.Half, 0)] + [InlineData(ExrPixelType.Half, 8)] + [InlineData(ExrPixelType.Half, 1025)] + [InlineData(ExrPixelType.Float, 0)] + [InlineData(ExrPixelType.Float, 8)] + [InlineData(ExrPixelType.Float, 1025)] + [InlineData(ExrPixelType.UnsignedInt, 0)] + [InlineData(ExrPixelType.UnsignedInt, 8)] + [InlineData(ExrPixelType.UnsignedInt, 1025)] + public void Decode_InvalidInflatedBlock_IgnoreImageData_ClearsPixels(ExrPixelType pixelType, int length) + { + byte[] data = BuildExr(ZlibCompress(new byte[length]), pixelType, 2, 0); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); + DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using Image image = Image.Load(options, data); + Assert.Equal(new Size(256, 1), image.Size); + + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); + } + } + + /// + /// Missing or truncated zlib headers obey the image-data integrity policy. + /// + /// The number of available zlib header bytes. + /// The image-data integrity policy. + [Theory] + [InlineData(0, SegmentIntegrityHandling.Strict)] + [InlineData(1, SegmentIntegrityHandling.Strict)] + [InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(1, SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(0, SegmentIntegrityHandling.IgnoreImageData)] + [InlineData(1, SegmentIntegrityHandling.IgnoreImageData)] + public void Decode_IncompleteZlibHeader_RespectsIntegrityHandling(int length, SegmentIntegrityHandling integrity) + { + byte[] header = [0x78, 0x9C]; + byte[] data = BuildExr(header[..length], ExrPixelType.Float, 2, 0); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); + DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = integrity }; + + if (integrity == SegmentIntegrityHandling.IgnoreImageData) + { + using Image image = Image.Load(options, data); + Assert.Equal(new Size(256, 1), image.Size); + + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); + } + } + else + { + Assert.Throws(() => Image.Load(options, data)); + } + } + + /// + /// Missing color channels must not inherit the allocator's previous contents. + /// + /// The stored sample type. + /// The ZIP compression code. + /// The data window's first row coordinate. + [Theory] + [InlineData(ExrPixelType.Half, 2, 0)] + [InlineData(ExrPixelType.Float, 2, 0)] + [InlineData(ExrPixelType.UnsignedInt, 2, 0)] + [InlineData(ExrPixelType.Half, 3, 0)] + [InlineData(ExrPixelType.Float, 3, 0)] + [InlineData(ExrPixelType.UnsignedInt, 3, 0)] + [InlineData(ExrPixelType.Half, 3, -10)] + [InlineData(ExrPixelType.Float, 3, -10)] + [InlineData(ExrPixelType.UnsignedInt, 3, -10)] + [InlineData(ExrPixelType.Half, 3, 10)] + [InlineData(ExrPixelType.Float, 3, 10)] + [InlineData(ExrPixelType.UnsignedInt, 3, 10)] + public void Decode_SingleRedChannel_InitializesMissingColorChannels(ExrPixelType pixelType, byte compression, int yMin) + { + byte[] predicted = new byte[256 * (pixelType == ExrPixelType.Half ? 2 : 4)]; + + // A zero first byte followed by 128-valued differences reconstructs an all-zero sample plane. + predicted.AsSpan(1).Fill(128); + byte[] data = BuildExr(ZlibCompress(predicted), pixelType, compression, yMin); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); + DecoderOptions options = new() { Configuration = configuration }; + + using Image image = Image.Load(options, data); + Assert.Equal(new Size(256, 1), image.Size); + + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); + } + } + + /// + /// Compresses the predictor bytes for a scanline block. + /// + /// The predictor bytes. + /// The zlib stream. + private static byte[] ZlibCompress(byte[] data) + { + if (data.Length == 0) + { + // An empty write produces no output on some runtimes. Use a complete zlib stream + // containing an empty final DEFLATE block and Adler-32 checksum instead. + return [0x78, 0x9C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]; + } + + using MemoryStream output = new(); + using (ZLibStream zlib = new(output, CompressionLevel.Optimal, leaveOpen: true)) + { + zlib.Write(data); + } + + return output.ToArray(); + } + + /// + /// Builds a single-row EXR containing only the red channel. + /// + /// The compressed scanline bytes. + /// The stored sample type. + /// The ZIP compression code. + /// The data window's first row coordinate. + /// The encoded image. + private static byte[] BuildExr(byte[] compressed, ExrPixelType pixelType, byte compression, int yMin) + { + const int width = 256; + const int height = 1; + + using MemoryStream output = new(); + using BinaryWriter writer = new(output); + + writer.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01 }); + writer.Write((byte)2); + writer.Write(new byte[] { 0, 0, 0 }); + + using (MemoryStream channelStream = new()) + using (BinaryWriter channelWriter = new(channelStream)) + { + WriteString(channelWriter, "R"); + channelWriter.Write((int)pixelType); + channelWriter.Write((byte)0); + channelWriter.Write(new byte[] { 0, 0, 0 }); + channelWriter.Write(1); + channelWriter.Write(1); + channelWriter.Write((byte)0); + + WriteAttribute(writer, "channels", "chlist", channelStream.ToArray()); + } + + WriteAttribute(writer, "compression", "compression", [compression]); + + using (MemoryStream boxStream = new()) + using (BinaryWriter boxWriter = new(boxStream)) + { + boxWriter.Write(0); + boxWriter.Write(yMin); + boxWriter.Write(width - 1); + boxWriter.Write(yMin + height - 1); + + byte[] box = boxStream.ToArray(); + WriteAttribute(writer, "dataWindow", "box2i", box); + WriteAttribute(writer, "displayWindow", "box2i", box); + } + + WriteAttribute(writer, "lineOrder", "lineOrder", [0]); + + byte[] one = new byte[4]; + BinaryPrimitives.WriteSingleLittleEndian(one, 1F); + WriteAttribute(writer, "pixelAspectRatio", "float", one); + WriteAttribute(writer, "screenWindowCenter", "v2f", new byte[8]); + WriteAttribute(writer, "screenWindowWidth", "float", one); + writer.Write((byte)0); + + long chunkStart = output.Position + sizeof(ulong); + writer.Write((ulong)chunkStart); + writer.Write(yMin); + writer.Write((uint)compressed.Length); + writer.Write(compressed); + + return output.ToArray(); + } + + private static void WriteString(BinaryWriter writer, string value) + { + writer.Write(Encoding.ASCII.GetBytes(value)); + writer.Write((byte)0); + } + + private static void WriteAttribute(BinaryWriter writer, string name, string type, byte[] value) + { + WriteString(writer, name); + WriteString(writer, type); + writer.Write(value.Length); + writer.Write(value); + } +} diff --git a/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs b/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs new file mode 100644 index 0000000000..bcf7df5984 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Tests.Formats; + +public class InvalidImageDimensionsTests +{ + [Theory] + [InlineData("Qk1GAAAAAAAAADYAAAAoAAAAAgACAAAAAAABABgAAAAAABAAAAATCwAAEwsAAAAAAAAAAAAAAAD/AP8AAAAAAP8A/wAAAA==")] + [InlineData("R0lGODdhAgIAAIEAAAD/AP8AAAAA/wAAACwAAAQAAgACAAAIBwADABAQICAAOw==")] + [InlineData("AAACAAAAAAAAAAAAAgDCsRgAAgAAAP8A/wD/AAAA//8=")] + public void Load_WithNonPositiveDimensions_ThrowsInvalidImageContentException(string encodedData) + { + byte[] data = Convert.FromBase64String(encodedData); + + Assert.Throws(() => Image.Load(data)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs new file mode 100644 index 0000000000..95c452c0f8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Png; + +namespace SixLabors.ImageSharp.Tests.Formats.Png; + +[Trait("Format", "Png")] +public class PngDecoderCoreTests +{ + [Fact] + public void CalculateScanlineLength_WithLargeGrayscaleWidth_ReturnsExpectedLength() + { + int length = PngDecoderCore.CalculateScanlineLength(536_870_913, 8, 1); + + Assert.Equal(536_870_913, length); + } + + [Fact] + public void CalculateScanlineLength_WithLargeRgbaWidth_ReturnsExpectedLength() + { + int length = PngDecoderCore.CalculateScanlineLength(33_554_432, 16, 8); + + Assert.Equal(268_435_456, length); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index ed33f71636..252ea49603 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.IO.Hashing; using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; @@ -108,6 +109,96 @@ public void Decode_TruncatedFrameControlChunk_ExceptionIsThrown() Assert.Equal("The frame control chunk does not contain enough data!", exception.Message); } + [Fact] + public void DecodeAndIdentify_WithDuplicateHeader_ThrowInvalidImageContentException() + { + using MemoryStream payloadStream = new(); + payloadStream.Write(Raw1X1PngIhdrAndpHYs); + payloadStream.Write(Raw1X1PngIhdrAndpHYs.AsSpan(8, 25)); + payloadStream.Write(Raw1X1PngIdatAndIend); + byte[] payload = payloadStream.ToArray(); + + Assert.Throws(() => Image.Load(payload)); + Assert.Throws(() => Image.Identify(payload)); + } + + /// + /// Chunk recovery must not replace the header after scanline storage has been sized. + /// + /// The segment integrity policy. + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void DecodeAndIdentify_WithChunkRecovery_FollowIntegrityPolicy(SegmentIntegrityHandling integrity) + { + byte[] data = TestFile.Create(TestImages.Png.DuplicateHeaderChunkResync).Bytes; + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + + Assert.Throws(() => Image.Load(options, data)); + + if (integrity == SegmentIntegrityHandling.Strict) + { + Assert.Throws(() => Image.Identify(options, data)); + } + else + { + // Identify skips the image-data payload rather than decoding and resynchronizing within it. + Assert.Equal(new Size(1, 1), Image.Identify(options, data).Size); + } + } + + /// + /// Corrupt compressed metadata follows the ancillary policy without preventing valid pixel decoding. + /// + /// The compressed metadata chunk type. + /// The segment integrity policy. + [Theory] + [InlineData("iCCP", SegmentIntegrityHandling.Strict)] + [InlineData("iCCP", SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData("iCCP", SegmentIntegrityHandling.IgnoreImageData)] + [InlineData("zTXt", SegmentIntegrityHandling.Strict)] + [InlineData("zTXt", SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData("zTXt", SegmentIntegrityHandling.IgnoreImageData)] + [InlineData("iTXt", SegmentIntegrityHandling.Strict)] + [InlineData("iTXt", SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData("iTXt", SegmentIntegrityHandling.IgnoreImageData)] + public void Decode_InvalidCompressedMetadata_FollowsIntegrityPolicy(string chunkType, SegmentIntegrityHandling integrity) + { + // iTXt adds a compression flag and empty language/translated-keyword fields before the zlib stream. + byte[] fields = chunkType == "iTXt" ? [(byte)'p', 0, 1, 0, 0, 0] : [(byte)'p', 0, 0]; + + // The zlib header is valid, but the first deflate block uses reserved block type 3. + byte[] chunk = [.. Encoding.ASCII.GetBytes(chunkType), .. fields, 0x78, 0x9C, 0x07, 0, 0, 0, 0]; + using MemoryStream stream = new(); + stream.Write(Raw1X1PngIhdrAndpHYs); + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteInt32BigEndian(buffer, chunk.Length - 4); + stream.Write(buffer); + stream.Write(chunk); + Crc32 crc = new(); + crc.Append(chunk); + BinaryPrimitives.WriteUInt32BigEndian(buffer, crc.GetCurrentHashAsUInt32()); + stream.Write(buffer); + stream.Write(Raw1X1PngIdatAndIend); + byte[] data = stream.ToArray(); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; + + if (integrity == SegmentIntegrityHandling.Strict) + { + InvalidImageContentException exception = Assert.Throws(() => Image.Load(options, data)); + Assert.IsType(exception.InnerException); + } + else + { + using Image image = Image.Load(options, data); + Assert.Equal(new Size(1, 1), image.Size); + Assert.Equal(default(Rgb24), image[0, 0]); + Assert.Null(image.Metadata.IccProfile); + Assert.Empty(image.Metadata.GetPngMetadata().TextData); + } + } + // https://github.com/SixLabors/ImageSharp/issues/3079 [Fact] public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow() diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs new file mode 100644 index 0000000000..a69e0b21cd --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Png; + +public partial class PngDecoderTests +{ + [Fact] + public void Decode_IccLutExceedsVectorChannelCount_Throws() + { + byte[] profileData = BuildLut16Profile(3, 15, 2, 2, 2); + byte[] pngData = BuildPng(profileData); + DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + Assert.Throws(() => Image.Load(options, pngData)); + } + + /// + /// Three-channel LUT conversion remains supported. + /// + [Fact] + public void Decode_IccLutWithSupportedChannelCount_ConvertsPixels() + { + byte[] pngData = BuildPng(BuildLut16Profile(3, 3, 2, 2, 2)); + DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image image = Image.Load(options, pngData); + Assert.Equal(new Size(16, 16), image.Size); + + // Every CLUT node contains a nonzero XYZ value, even though the encoded pixels are black. + Assert.NotEqual(default(Rgb24), image[0, 0]); + } + + /// + /// Preserving a profile does not impose the converter's four-component storage limit on the parser. + /// + /// The number of output channels in the LUT. + [Theory] + [InlineData(3)] + [InlineData(15)] + public void Decode_IccLut_Preserve_RetainsChannels(int outputChannels) + { + byte[] pngData = BuildPng(BuildLut16Profile(3, outputChannels, 2, 2, 2)); + DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + + using Image image = Image.Load(options, pngData); + IccLut16TagDataEntry entry = Assert.IsType(Assert.Single(image.Metadata.IccProfile.Entries)); + Assert.Equal(outputChannels, entry.OutputValues.Length); + Assert.Equal(default(Rgb24), image[0, 0]); + } + + private static byte[] BuildLut16Profile(int inputChannels, int outputChannels, int clutPoints, int inputTableLength, int outputTableLength) + { + using MemoryStream stream = new(); + + byte[] header = new byte[128]; + BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(8), 0x04300000U); + Encoding.ASCII.GetBytes("mntr").CopyTo(header, 12); + Encoding.ASCII.GetBytes("RGB ").CopyTo(header, 16); + Encoding.ASCII.GetBytes("XYZ ").CopyTo(header, 20); + stream.Write(header); + + WriteUInt32(1); + stream.Write(Encoding.ASCII.GetBytes("A2B0")); + long offsetPosition = stream.Position; + WriteUInt32(0); + long sizePosition = stream.Position; + WriteUInt32(0); + + long tagStart = stream.Position; + stream.Write(Encoding.ASCII.GetBytes("mft2")); + WriteUInt32(0); + stream.WriteByte((byte)inputChannels); + stream.WriteByte((byte)outputChannels); + stream.WriteByte((byte)clutPoints); + stream.WriteByte(0); + + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < 3; x++) + { + WriteFix16(x == y ? 1D : 0D); + } + } + + WriteUInt16((ushort)inputTableLength); + WriteUInt16((ushort)outputTableLength); + + for (int channel = 0; channel < inputChannels; channel++) + { + for (int i = 0; i < inputTableLength; i++) + { + WriteUInt16((ushort)(i == 0 ? 0 : ushort.MaxValue)); + } + } + + int clutLength = (int)Math.Pow(clutPoints, inputChannels); + for (int i = 0; i < clutLength; i++) + { + for (int channel = 0; channel < outputChannels; channel++) + { + WriteUInt16(0x8000); + } + } + + for (int channel = 0; channel < outputChannels; channel++) + { + for (int i = 0; i < outputTableLength; i++) + { + WriteUInt16((ushort)(i == 0 ? 0 : ushort.MaxValue)); + } + } + + long tagEnd = stream.Position; + byte[] result = stream.ToArray(); + BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan((int)offsetPosition), (uint)tagStart); + BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan((int)sizePosition), (uint)(tagEnd - tagStart)); + BinaryPrimitives.WriteUInt32BigEndian(result, (uint)result.Length); + return result; + + void WriteUInt32(uint value) + { + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, value); + stream.Write(buffer); + } + + void WriteUInt16(ushort value) + { + Span buffer = stackalloc byte[2]; + BinaryPrimitives.WriteUInt16BigEndian(buffer, value); + stream.Write(buffer); + } + + void WriteFix16(double value) + { + int rawValue = (int)Math.Round(value * 65536D); + WriteUInt32(unchecked((uint)rawValue)); + } + } + + private static byte[] BuildPng(byte[] profileData) + { + using Image image = new(16, 16); + image.Metadata.IccProfile = new IccProfile(profileData); + + using MemoryStream stream = new(); + image.SaveAsPng(stream); + return stream.ToArray(); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs index 72f53cab78..769ee00a82 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs @@ -115,4 +115,17 @@ public void TiffDecoder_SubIfd8(TestImageProvider provider) Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripOffsets)); Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripByteCounts)); } + + [Fact] + public void TiffDecoder_DirectoryEntryCountExceedsAvailableData_Throws() + { + byte[] data = + [ + 0x49, 0x49, 0x2B, 0x00, 0x08, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xF2, 0x05, 0x2A, 0x01, 0x00, 0x00, 0x00, + ]; + + Assert.Throws(() => Image.Load(data)); + } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ec6113be30..d23eedcfee 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. // ReSharper disable InconsistentNaming +using System.Numerics; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; @@ -25,6 +26,38 @@ public class TiffDecoderTests : TiffDecoderBaseTester { public static readonly string[] MultiframeTestImages = Multiframes; + /// + /// Decoded floating-point components are normalized before they enter half-vector storage. + /// + /// The encoded floating-point TIFF. + /// The normalized intensity. + [Theory] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000807F0000807F0000807F0000807F0000807F0000807F0000807F0000807F", 1F)] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F0000C07F", 0F)] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000004000000040000000400000004000000040000000400000004000000040", 1F)] + [InlineData("49492A00080000000A0000010400010000000800000001010400010000000100000002010300010000002000000003010300010000000100" + + "0000060103000100000001000000110104000100000086000000150103000100000001000000160104000100000001000000170104000100" + + "000020000000530103000100000003000000000000000000003F0000003F0000003F0000003F0000003F0000003F0000003F0000003F", .5F)] + public void Decode_FloatingPointSamples_NormalizesHalfVector4(string hex, float intensity) + { + byte[] data = Convert.FromHexString(hex); + using Image image = Image.Load(data); + Assert.Equal(new Size(8, 1), image.Size); + + Vector4 expected = new(intensity, intensity, intensity, 1F); + + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(expected, image[x, 0].ToScaledVector4()); + } + } + [Theory] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index 4317c2714d..cc1b78c076 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -554,6 +554,45 @@ public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WhiteIsZero_ public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + /// + /// CCITT row framing must fit even when each row contains only one pixel. + /// + /// The image width. + [Theory] + [InlineData(1)] + [InlineData(64)] + public void TiffEncoder_EncodeNarrowCcittGroup3Fax_Works(int width) + { + using Image image = new(width, 2000); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + image[x, y] = new L8((byte)(((x + y) & 1) == 0 ? 255 : 0)); + } + } + + TiffFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetTiffMetadata(); + metadata.BitsPerPixel = TiffBitsPerPixel.Bit1; + metadata.Compression = TiffCompression.CcittGroup3Fax; + + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(image.Size, decoded.Size); + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(image[x, y], decoded[x, y]); + } + } + } + [Theory] [WithFile(Issues2255, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WithoutSpecifyingBitPerPixel_Works(TestImageProvider provider) @@ -569,6 +608,26 @@ public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_WhiteIsZero_ public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); + /// + /// Re-encoding a one-pixel Group 4 image must retain its pixel and fit the end-of-block code. + /// + [Fact] + public void TiffEncoder_ReencodeNarrowCcittGroup4Fax_Works() + { + byte[] data = Convert.FromBase64String( + "SUkqAAgAAAAJAAABAwABAAAAAQAAAAEBAwABAAAAAQAAAAIBAwABAAAAAQAAAAMBAwABAAAABAAAAAYBAwABAAAAAAAAABEBBAABAAAA" + + "egAAABUBAwABAAAAAQAAABYBBAABAAAAAQAAABcBBAABAAAABAAAAAAAAACACACA"); + + using Image image = Image.Load(data); + using MemoryStream output = new(); + image.Save(output, new TiffEncoder()); + + output.Position = 0; + using Image decoded = Image.Load(output); + Assert.Equal(new Size(1, 1), decoded.Size); + Assert.Equal(image[0, 0], decoded[0, 0]); + } + [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index ee82687167..73bb1457da 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -4,9 +4,11 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -22,6 +24,70 @@ public class WebpEncoderTests { private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); + /// + /// Selected EXIF parts are respected whether the lazy profile is installed before or after synchronization. + /// + /// Whether the stream installs the profile after metadata synchronization. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Encode_LazyExifProfile_AppliesSelectedParts(bool reentrant) + { + ExifProfile source = new(); + source.SetValue(ExifTag.Make, "POC"); + source.SetValue(ExifTag.GPSLatitudeRef, "N"); + ExifProfile filteredLazy = new(source.ToByteArray()) + { + Parts = ExifParts.IfdTags | ExifParts.ExifTags + }; + + using Image image = new(1, 1); + using MemoryStream output = reentrant + ? new SwapOnCanSeekStream(() => image.Metadata.ExifProfile = filteredLazy) + : new MemoryStream(); + + if (!reentrant) + { + image.Metadata.ExifProfile = filteredLazy; + } + + image.SaveAsWebp(output); + output.Position = 0; + using Image decoded = Image.Load(output); + + Assert.NotNull(decoded.Metadata.ExifProfile); + Assert.True(decoded.Metadata.ExifProfile.TryGetValue(ExifTag.Make, out IExifValue make)); + Assert.Equal("POC", make.Value); + Assert.False(decoded.Metadata.ExifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out _)); + } + + /// + /// Replaces metadata at the stream capability check, after encoder synchronization has completed. + /// + private sealed class SwapOnCanSeekStream : MemoryStream + { + private Action callback; + + /// + /// Initializes a stream that invokes the callback on its first capability check. + /// + /// The metadata replacement callback. + public SwapOnCanSeekStream(Action callback) => this.callback = callback; + + /// + public override bool CanSeek + { + get + { + Action action = this.callback; + this.callback = null; + action?.Invoke(); + + return base.CanSeek; + } + } + } + [Theory] [WithFile(Lossless.Animated, PixelTypes.Rgba32)] public void Encode_AnimatedLossless(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 394479f89d..324070e4a5 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -1,9 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; +using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -13,6 +16,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp; [Trait("Format", "Webp")] public class WebpMetaDataTests { + public static IEnumerable IccMetadataOptions() + { + foreach (SegmentIntegrityHandling integrity in new[] { SegmentIntegrityHandling.Strict, SegmentIntegrityHandling.IgnoreAncillary, SegmentIntegrityHandling.IgnoreImageData }) + { + foreach (bool skipMetadata in new[] { false, true }) + { + foreach (bool animated in new[] { false, true }) + { + yield return new object[] { integrity, skipMetadata, animated }; + } + } + } + } + + public static IEnumerable TruncatedMetadataOptions() + { + foreach (string chunkType in new[] { "EXIF", "XMP " }) + { + foreach (uint length in new[] { 0x40000000U, 0xFFFFFFFEU, uint.MaxValue }) + { + foreach (SegmentIntegrityHandling integrity in new[] { SegmentIntegrityHandling.Strict, SegmentIntegrityHandling.IgnoreAncillary, SegmentIntegrityHandling.IgnoreImageData }) + { + yield return new object[] { chunkType, length, integrity, false }; + yield return new object[] { chunkType, length, integrity, true }; + } + } + } + } + [Theory] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)] @@ -228,4 +260,152 @@ public void Decode_InvalidExifChunk_ThrowsWithStrict() using Image image = Image.Load(options, stream); }); } + + [Theory] + [InlineData("ICCP", 0xFFFFFFFEU)] + [InlineData("EXIF", 0xFFFFFFFEU)] + [InlineData("XMP ", 0xFFFFFFFEU)] + [InlineData("ICCP", uint.MaxValue)] + [InlineData("EXIF", uint.MaxValue)] + [InlineData("XMP ", uint.MaxValue)] + public void Decode_WithOversizedMetadataChunk_ThrowsInvalidImageContentException(string chunkType, uint length) + { + byte[] payload = Convert.FromHexString( + "524946462200000057454250565038580A0000002000000000000000000049434350FEFFFFFF01020304"); + Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4)); + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(34), length); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => Image.Load(options, payload)); + Assert.Throws(() => Image.Identify(options, payload)); + } + + [Theory] + [InlineData("ICCP")] + [InlineData("EXIF")] + [InlineData("XMP ")] + public void Decode_WithMetadataChunkLargerThanRemainingData_ThrowsInStrictMode(string chunkType) + { + byte[] payload = Convert.FromHexString( + "524946460000000057454250565038580A00000020000000010000010000494343500000004000000000"); + Encoding.ASCII.GetBytes(chunkType, payload.AsSpan(30, 4)); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => Image.Load(options, payload)); + Assert.Throws(() => Image.Identify(options, payload)); + } + + [Theory] + [MemberData(nameof(TruncatedMetadataOptions))] + public void Decode_TruncatedTrailingMetadata_RespectsOptions(string chunkType, uint length, SegmentIntegrityHandling integrity, bool skipMetadata) + { + byte[] payload = CreateWebpWithMetadata(chunkType, length, false, false); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; + + if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata) + { + Assert.Throws(() => Image.Load(options, payload)); + Assert.Throws(() => Image.Identify(options, payload)); + } + else + { + using Image image = Image.Load(options, payload); + Assert.Equal(new Size(2, 2), image.Size); + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(new Rgba32(17, 34, 51), image[x, y]); + } + } + + Assert.Null(image.Metadata.ExifProfile); + Assert.Null(image.Metadata.XmpProfile); + + ImageInfo info = Image.Identify(options, payload); + Assert.Equal(image.Size, info.Size); + Assert.Null(info.Metadata.ExifProfile); + Assert.Null(info.Metadata.XmpProfile); + } + } + + [Theory] + [MemberData(nameof(IccMetadataOptions))] + public void Decode_InvalidIccPayload_RespectsOptionsAndReadsFollowingImage(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated) + { + byte[] payload = CreateWebpWithMetadata("ICCP", 4, true, animated); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; + + if (integrity is SegmentIntegrityHandling.Strict && !skipMetadata) + { + Assert.Throws(() => Image.Load(options, payload)); + Assert.Throws(() => Image.Identify(options, payload)); + } + else + { + using Image image = Image.Load(options, payload); + Assert.Equal(new Size(2, 2), image.Size); + Assert.Equal(animated ? 2 : 1, image.Frames.Count); + Assert.Equal(new Rgba32(17, 34, 51), image[0, 0]); + Assert.Null(image.Metadata.IccProfile); + + ImageInfo info = Image.Identify(options, payload); + Assert.Equal(image.Size, info.Size); + Assert.Null(info.Metadata.IccProfile); + } + } + + [Theory] + [MemberData(nameof(IccMetadataOptions))] + public void Decode_TruncatedIccFraming_RemainsFatal(SegmentIntegrityHandling integrity, bool skipMetadata, bool animated) + { + byte[] payload = CreateWebpWithMetadata("ICCP", 0x40000000, true, animated); + DecoderOptions options = new() { SegmentIntegrityHandling = integrity, SkipMetadata = skipMetadata }; + + Assert.Throws(() => Image.Load(options, payload)); + Assert.Throws(() => Image.Identify(options, payload)); + } + + /// + /// Places a metadata declaration around a complete lossless image to test recovery independently of pixel truncation. + /// + private static byte[] CreateWebpWithMetadata(string chunkType, uint length, bool beforeImage, bool animated) + { + byte[] header = Convert.FromHexString( + "524946460000000057454250565038580A00000020000000010000010000494343500000004000000000"); + header[20] = chunkType switch { "ICCP" => 0x20, "EXIF" => 0x08, _ => 0x04 }; + Encoding.ASCII.GetBytes(chunkType, header.AsSpan(30, 4)); + BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(34), length); + + using Image source = new(2, 2, new Rgba32(17, 34, 51)); + if (animated) + { + header[20] |= 0x02; + using Image secondFrame = new(2, 2, new Rgba32(51, 34, 17)); + source.Frames.AddFrame(secondFrame.Frames.RootFrame); + } + + using MemoryStream encoded = new(); + source.Save(encoded, new WebpEncoder { FileFormat = WebpFileFormatType.Lossless }); + byte[] imageData = encoded.ToArray(); + using MemoryStream combined = new(); + combined.Write(header.AsSpan(0, 30)); + if (beforeImage) + { + combined.Write(header.AsSpan(30)); + } + + // Replace the encoder's extended header when present, keeping its complete + // image or animation chunks and the deliberately chosen metadata declaration. + int imageChunkOffset = imageData.AsSpan(12, 4).SequenceEqual("VP8X"u8) ? 30 : 12; + combined.Write(imageData.AsSpan(imageChunkOffset)); + if (!beforeImage) + { + combined.Write(header.AsSpan(30)); + } + + byte[] payload = combined.ToArray(); + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), (uint)payload.Length - 8); + return payload; + } } diff --git a/tests/ImageSharp.Tests/Helpers/NumericsTests.cs b/tests/ImageSharp.Tests/Helpers/NumericsTests.cs index 35109d352f..77ecc447ee 100644 --- a/tests/ImageSharp.Tests/Helpers/NumericsTests.cs +++ b/tests/ImageSharp.Tests/Helpers/NumericsTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.Intrinsics; namespace SixLabors.ImageSharp.Tests.Helpers; @@ -305,6 +306,114 @@ public void ClampDouble(int length, double min, double max) (v, m1, m2) => Numerics.Clamp(v, m1, m2)); } + /// + /// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds. + /// + /// The lower bound. + /// The upper bound. + [Theory] + [InlineData(0F, 1F)] + [InlineData(-2F, 3F)] + [InlineData(.25F, .75F)] + public void ClampSingle_NormalizesNonfiniteValues(float min, float max) + { + float midpoint = (min + max) / 2; + float[] inputs = [float.NaN, float.PositiveInfinity, float.NegativeInfinity, midpoint]; + float[] normalized = [min, max, min, midpoint]; + float[] values = new float[65]; + float[] expected = new float[values.Length]; + + // The length includes complete registers and remainders for every supported SIMD width. + for (int i = 0; i < values.Length; i++) + { + values[i] = inputs[i % inputs.Length]; + expected[i] = normalized[i % normalized.Length]; + Assert.Equal(expected[i], Numerics.Clamp(values[i], min, max)); + } + + Vector4 input = new(inputs[0], inputs[1], inputs[2], inputs[3]); + Vector4 result = new(normalized[0], normalized[1], normalized[2], normalized[3]); + Assert.Equal(result, Numerics.Clamp(input, new Vector4(min), new Vector4(max))); + Assert.Equal(new Vector2(min, max), Numerics.Clamp(new Vector2(float.NaN, float.PositiveInfinity), new Vector2(min), new Vector2(max))); + + Vector128 vector128 = Vector128.LoadUnsafe(ref values[0]); + Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max))); + + Vector256 vector256 = Vector256.LoadUnsafe(ref values[0]); + Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max))); + + Vector512 vector512 = Vector512.LoadUnsafe(ref values[0]); + Assert.Equal(Vector512.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector512, Vector512.Create(min), Vector512.Create(max))); + + Numerics.Clamp(values, min, max); + Assert.Equal(expected, values); + } + + /// + /// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds. + /// + /// The lower bound. + /// The upper bound. + [Theory] + [InlineData(0D, 1D)] + [InlineData(-2D, 3D)] + [InlineData(.25D, .75D)] + public void ClampDouble_NormalizesNonfiniteValues(double min, double max) + { + double midpoint = (min + max) / 2; + double[] inputs = [double.NaN, double.PositiveInfinity, double.NegativeInfinity, midpoint]; + double[] normalized = [min, max, min, midpoint]; + double[] values = new double[65]; + double[] expected = new double[values.Length]; + + // The length includes complete registers and remainders for every supported SIMD width. + for (int i = 0; i < values.Length; i++) + { + values[i] = inputs[i % inputs.Length]; + expected[i] = normalized[i % normalized.Length]; + Assert.Equal(expected[i], Numerics.Clamp(values[i], min, max)); + } + + Vector128 vector128 = Vector128.LoadUnsafe(ref values[0]); + Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max))); + + Vector256 vector256 = Vector256.LoadUnsafe(ref values[0]); + Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max))); + + Vector512 vector512 = Vector512.LoadUnsafe(ref values[0]); + Assert.Equal(Vector512.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector512, Vector512.Create(min), Vector512.Create(max))); + + Numerics.Clamp(values, min, max); + Assert.Equal(expected, values); + } + + /// + /// Clamping an in-range zero preserves its sign in scalar and bulk conversions. + /// + [Fact] + public void Clamp_PreservesInRangeSignedZero() + { + float[] singles = new float[65]; + double[] doubles = new double[65]; + + for (int i = 0; i < singles.Length; i++) + { + singles[i] = i % 2 == 0 ? -0F : 0F; + doubles[i] = i % 2 == 0 ? -0D : 0D; + Assert.Equal(BitConverter.SingleToInt32Bits(singles[i]), BitConverter.SingleToInt32Bits(Numerics.Clamp(singles[i], 0F, 1F))); + Assert.Equal(BitConverter.DoubleToInt64Bits(doubles[i]), BitConverter.DoubleToInt64Bits(Numerics.Clamp(doubles[i], 0D, 1D))); + } + + Numerics.Clamp(singles, 0F, 1F); + Numerics.Clamp(doubles, 0D, 1D); + + for (int i = 0; i < singles.Length; i++) + { + Assert.Equal(BitConverter.SingleToInt32Bits(i % 2 == 0 ? -0F : 0F), BitConverter.SingleToInt32Bits(singles[i])); + Assert.Equal(BitConverter.DoubleToInt64Bits(i % 2 == 0 ? -0D : 0D), BitConverter.DoubleToInt64Bits(doubles[i])); + } + } + private static void TestClampSpan( int length, T min, diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index 89256507ed..63c1f7fa31 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -197,7 +197,7 @@ public void MemoryStream_WriteToSpanTests(int length) readonlyStream.Position = 0; bytArrRet = new byte[(int)readonlyStream.Length]; - readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); + readonlyStream.Read(bytArrRet); for (int i = 0; i < bytArr.Length; i++) { Assert.Equal(bytArr[i], bytArrRet[i]); @@ -216,7 +216,7 @@ public void MemoryStream_WriteToSpanTests(int length) ms2.WriteTo(ms3); ms3.Position = 0; bytArrRet = new byte[(int)ms3.Length]; - ms3.Read(bytArrRet, 0, (int)ms3.Length); + ms3.Read(bytArrRet); for (int i = 0; i < bytArr.Length; i++) { Assert.Equal(bytArr[i], bytArrRet[i]); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index c098ace09a..2b47d09843 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -490,6 +490,47 @@ public void ProfileToByteArray() } } + /// + /// Lazy profile serialization filters selected sections without changing the all-parts passthrough. + /// + /// The sections to serialize. + /// Whether the IFD tag should remain. + /// Whether the GPS tag should remain. + [Theory] + [InlineData(ExifParts.All, true, true)] + [InlineData(ExifParts.IfdTags | ExifParts.ExifTags, true, false)] + [InlineData(ExifParts.GpsTags, false, true)] + [InlineData(ExifParts.None, false, false)] + public void ProfileToByteArray_AppliesPartsToLazyValues(ExifParts parts, bool keepMake, bool keepGps) + { + ExifProfile source = new(); + source.SetValue(ExifTag.Make, "POC"); + source.SetValue(ExifTag.GPSLatitudeRef, "N"); + byte[] originalData = source.ToByteArray(); + ExifProfile lazy = new(originalData) { Parts = parts }; + + byte[] filteredData = lazy.ToByteArray(); + ExifProfile result = new(filteredData); + + Assert.Equal(keepMake, result.TryGetValue(ExifTag.Make, out IExifValue make)); + Assert.Equal(keepGps, result.TryGetValue(ExifTag.GPSLatitudeRef, out IExifValue gps)); + + if (keepMake) + { + Assert.Equal("POC", make.Value); + } + + if (keepGps) + { + Assert.Equal("N", gps.Value); + } + + if (parts is ExifParts.All) + { + Assert.Same(originalData, filteredData); + } + } + private static ExifProfile CreateExifProfile() { ExifProfile profile = new(); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs index a686d44872..530da5594b 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs @@ -53,6 +53,36 @@ internal void ReadClutF32(byte[] data, IccClut expected, int inChannelCount, int Assert.Equal(expected, output); } + [Fact] + public void ReadClut_WithOversizedDimensions_ThrowsInvalidIccProfileException() + { + byte[] gridPointCount = Enumerable.Repeat((byte)3, 15).ToArray(); + + Assert.Throws(() => CreateReader(new byte[8]).ReadClut8(15, 15, gridPointCount)); + Assert.Throws(() => CreateReader(new byte[8]).ReadClut16(15, 15, gridPointCount)); + Assert.Throws(() => CreateReader(new byte[8]).ReadClutF32(15, 15, gridPointCount)); + } + + /// + /// A complete profile header and element table do not make absent CLUT values readable. + /// + [Fact] + public void ReadTagDataEntry_WithTruncatedClut_RejectsMissingValues() + { + // A2B0 starts at byte 144; its element at byte 168 declares a 15-channel, three-point grid. + byte[] data = Convert.FromHexString( + "000000C874657374040000006D6E74725247422058595A200000000000000000000000006163737000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000014132423000000090000000386D7065740000" + + "000000000000000000010000001800000020636C7574000F000F030303030303030303030303030303000000000000000000"); + + IccDataReader reader = new(data); + IccTagTableEntry tag = new(IccProfileTag.AToB0, 144, 56); + + Assert.Throws(() => reader.ReadTagDataEntry(tag)); + Assert.Empty(new IccProfile(data).Entries); + } + [Theory] [MemberData(nameof(IccTestDataLut.Lut8TestData), MemberType = typeof(IccTestDataLut))] internal void ReadLut8(byte[] data, IccLut expected) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs index 2a80ae9e9c..610c831e8f 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestDataIcc; @@ -59,4 +60,26 @@ public void ReadProfile_NoEntries() Assert.Equal(header.Size, expected.Size); Assert.Equal(header.Version, expected.Version); } + + [Fact] + public void ReadProfile_WithUndersizedArrayTags_IgnoresTags() + { + const int tagCount = 100; + const int dataOffset = 132 + (tagCount * 12); + byte[] data = new byte[dataOffset + 16]; + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(128), tagCount); + + for (int i = 0; i < tagCount; i++) + { + Span entry = data.AsSpan(132 + (i * 12), 12); + BinaryPrimitives.WriteUInt32BigEndian(entry, 0x73663332); + BinaryPrimitives.WriteUInt32BigEndian(entry[4..], dataOffset); + BinaryPrimitives.WriteUInt32BigEndian(entry[8..], 0); + } + + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(dataOffset), 0x73663332); + IccProfile profile = new(data); + + Assert.Empty(profile.Entries); + } } diff --git a/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs new file mode 100644 index 0000000000..5718da5b03 --- /dev/null +++ b/tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.PixelFormats; + +[Trait("Category", "PixelFormats")] +public class FloatingPointPixelNormalizationTests +{ + /// + /// HalfSingle normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfSingle_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// HalfVector2 normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector2_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// HalfVector4 normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector4_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// HalfVector4P normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void HalfVector4P_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// RgbaVector normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaVector_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// RgbaHalf normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaHalf_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// RgbaHalfP normalizes scaled input identically in scalar and bulk conversions. + /// + [Fact] + public void RgbaHalfP_ScaledInputIsNormalized() => AssertScaledInputIsNormalized(); + + /// + /// Raw half storage preserves IEEE special values while its scaled representation remains finite. + /// + [Fact] + public void HalfVector4_NativeSpecialValuesHaveNormalizedScaledOutput() => AssertNativeSpecialValuesHaveNormalizedScaledOutput(); + + /// + /// Associated half-vector conversion uses the stored alpha ratio before normalizing RGB. + /// + [Fact] + public void HalfVector4P_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized(); + + /// + /// Associated half-RGBA conversion uses the stored alpha ratio before normalizing RGB. + /// + [Fact] + public void RgbaHalfP_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized(); + + /// + /// Checks saturation and NaN handling without deriving expectations from the invalid-input path. + /// + /// The destination pixel format. + private static void AssertScaledInputIsNormalized() + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), + new(2F, -2F, .5F, 1F), + new(.25F, .5F, .75F, .5F), + new(.25F, .5F, .75F, float.NaN), + new(.25F, .5F, .75F, float.PositiveInfinity) + ]; + + Vector4[] normalized = + [ + new(1F, 0F, 0F, 1F), + new(1F, 0F, .5F, 1F), + new(.25F, .5F, .75F, .5F), + new(.25F, .5F, .75F, 0F), + new(.25F, .5F, .75F, 1F) + ]; + + // Seventeen pixels exercise wide registers and the narrower remainder paths. + Vector4[] source = new Vector4[17]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int sample = i % inputs.Length; + source[i] = inputs[sample]; + expected[i] = TPixel.FromUnassociatedScaledVector4(normalized[sample]); + Assert.Equal(expected[i], TPixel.FromUnassociatedScaledVector4(source[i])); + } + + // Associated formats otherwise interpret the vectors using their native alpha representation. + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + Assert.Equal(expected, actual); + } + + /// + /// Checks associated input against finite control values with the same represented color. + /// + /// The associated destination pixel format. + private static void AssertAssociatedScaledInputIsNormalized() + where TPixel : unmanaged, IPixel + { + Vector4[] inputs = + [ + new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), + new(1F, .5F, 1.5F, 2F), + new(.125F, .25F, .375F, .5F), + new(.25F, .5F, .75F, float.NaN), + new(.25F, .5F, .75F, float.PositiveInfinity) + ]; + + Vector4[] normalized = + [ + new(1F, 0F, 0F, 1F), + new(.5F, .25F, .75F, 1F), + new(.125F, .25F, .375F, .5F), + Vector4.Zero, + new(0F, 0F, 0F, 1F) + ]; + + Vector4[] source = new Vector4[17]; + TPixel[] expected = new TPixel[source.Length]; + TPixel[] actual = new TPixel[source.Length]; + + for (int i = 0; i < source.Length; i++) + { + int sample = i % inputs.Length; + source[i] = inputs[sample]; + expected[i] = TPixel.FromAssociatedScaledVector4(normalized[sample]); + Assert.Equal(expected[i], TPixel.FromAssociatedScaledVector4(source[i])); + } + + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); + Assert.Equal(expected, actual); + } + + /// + /// Checks native storage and every scaled output lane independently of integer conversion semantics. + /// + private static void AssertNativeSpecialValuesHaveNormalizedScaledOutput() + { + Vector4 native = new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 65504F); + HalfVector4 pixel = HalfVector4.FromVector4(native); + Assert.True(float.IsPositiveInfinity(pixel.ToVector4().X)); + Assert.True(float.IsNegativeInfinity(pixel.ToVector4().Y)); + Assert.True(float.IsNaN(pixel.ToVector4().Z)); + + Vector4 expected = new(1F, 0F, 0F, 1F); + Assert.Equal(expected, pixel.ToScaledVector4()); + Assert.Equal(1F, new HalfSingle(float.PositiveInfinity).ToScaledVector4().X); + Assert.Equal(0F, new HalfSingle(float.NaN).ToScaledVector4().X); + Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfVector2(new Vector2(float.PositiveInfinity, float.NaN)).ToScaledVector4()); + + HalfVector4[] source = new HalfVector4[17]; + Vector4[] nativeSource = new Vector4[source.Length]; + Array.Fill(nativeSource, native); + PixelOperations.Instance.FromVector4Destructive(Configuration.Default, nativeSource, source, PixelConversionModifiers.None); + Assert.All(source, value => Assert.Equal(pixel.PackedValue, value.PackedValue)); + + Vector4[] actual = new Vector4[source.Length]; + PixelOperations.Instance.ToVector4(Configuration.Default, source, actual, PixelConversionModifiers.Scale); + Assert.All(actual, value => Assert.Equal(expected, value)); + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index c0071e9062..d1e0d64ae6 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -57,6 +57,7 @@ public static class Png public const string LowColorVariance = "Png/low-variance.png"; public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string InvalidTextData = "Png/InvalidTextData.png"; + public const string DuplicateHeaderChunkResync = "Png/duplicate-header-chunk-resync.png"; public const string David = "Png/david.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; diff --git a/tests/Images/Input/Png/duplicate-header-chunk-resync.png b/tests/Images/Input/Png/duplicate-header-chunk-resync.png new file mode 100644 index 0000000000..50652a1067 --- /dev/null +++ b/tests/Images/Input/Png/duplicate-header-chunk-resync.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1183a3462f92784a0608fef2da95bef92d7f13f6275d4c628cc2310c772085cb +size 38937