Browse Source

Merge pull request #3187 from SixLabors/fix/security-advisory-triage

Harden codec and metadata input validation
release/4.1.x
James Jackson-South 2 weeks ago
committed by GitHub
parent
commit
37bc351f6e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
  2. 5
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
  3. 4
      src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
  4. 72
      src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs
  5. 51
      src/ImageSharp/Common/Extensions/StreamExtensions.cs
  6. 3
      src/ImageSharp/Common/Helpers/ColorNumerics.cs
  7. 178
      src/ImageSharp/Common/Helpers/Numerics.cs
  8. 17
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  9. 2
      src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
  10. 6
      src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
  11. 11
      src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs
  12. 2
      src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
  13. 9
      src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
  14. 19
      src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
  15. 54
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  16. 9
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  17. 4
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  18. 18
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  19. 71
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  20. 4
      src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
  21. 8
      src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
  22. 8
      src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs
  23. 24
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs
  24. 2
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
  25. 2
      src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs
  26. 1
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  27. 4
      src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
  28. 4
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  29. 131
      src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
  30. 17
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  31. 11
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  32. 7
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  33. 48
      src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs
  34. 3
      src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
  35. 201
      src/ImageSharp/PixelFormats/HalfTypeHelper.cs
  36. 2
      src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs
  37. 20
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs
  38. 63
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs
  39. 94
      src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs
  40. 7
      src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs
  41. 60
      tests/ImageSharp.Tests/Common/BufferedReadStreamExtensionsTests.cs
  42. 111
      tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs
  43. 35
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  44. 248
      tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs
  45. 18
      tests/ImageSharp.Tests/Formats/InvalidImageDimensionsTests.cs
  46. 26
      tests/ImageSharp.Tests/Formats/Png/PngDecoderCoreTests.cs
  47. 91
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs
  48. 158
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs
  49. 13
      tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs
  50. 33
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  51. 59
      tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs
  52. 66
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  53. 180
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  54. 109
      tests/ImageSharp.Tests/Helpers/NumericsTests.cs
  55. 4
      tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
  56. 41
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs
  57. 30
      tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs
  58. 23
      tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs
  59. 183
      tests/ImageSharp.Tests/PixelFormats/FloatingPointPixelNormalizationTests.cs
  60. 1
      tests/ImageSharp.Tests/TestImages.cs
  61. 3
      tests/Images/Input/Png/duplicate-header-chunk-resync.png

4
src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs

@ -42,6 +42,10 @@ internal class ClutCalculator : IVector4Calculator
Guard.NotNull(clut, nameof(clut)); Guard.NotNull(clut, nameof(clut));
Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount)); Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount));
Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount)); 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.inputCount = clut.InputChannelCount;
this.outputCount = clut.OutputChannelCount; this.outputCount = clut.OutputChannelCount;

5
src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs

@ -59,6 +59,11 @@ internal class LutEntryCalculator : IVector4Calculator
private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix) 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.inputCurve = InitLut(inputCurve);
this.outputCurve = InitLut(outputCurve); this.outputCurve = InitLut(outputCurve);
this.clutCalculator = new ClutCalculator(clut); this.clutCalculator = new ClutCalculator(clut);

4
src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs

@ -15,6 +15,10 @@ internal class TrcCalculator : IVector4Calculator
public TrcCalculator(IccTagDataEntry[] entries, bool inverted) public TrcCalculator(IccTagDataEntry[] entries, bool inverted)
{ {
Guard.NotNull(entries, nameof(entries)); 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]; this.calculators = new ISingleCalculator[entries.Length];
for (int i = 0; i < entries.Length; i++) for (int i = 0; i < entries.Length; i++)

72
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;
/// <summary>
/// Extension methods for the <see cref="BufferedReadStream"/> type.
/// </summary>
internal static class BufferedReadStreamExtensions
{
/// <summary>
/// Determines whether the complete read range is contained in the stream.
/// </summary>
/// <param name="stream">The stream containing the data.</param>
/// <param name="offset">The absolute start of the range.</param>
/// <param name="length">The number of bytes in the range.</param>
/// <returns>Whether the range is contained in the stream.</returns>
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;
}
/// <summary>
/// Gets a buffer length when the complete read fits in both the stream and an integer-sized buffer.
/// </summary>
/// <param name="stream">The stream containing the data.</param>
/// <param name="length">The declared length in bytes.</param>
/// <param name="bufferLength">The validated length, or zero when the range is invalid.</param>
/// <returns>Whether the complete read is valid.</returns>
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;
}
/// <summary>
/// Reads data from the stream into a slice of the provided buffer.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="offset">The offset within the buffer where bytes are read into.</param>
/// <param name="count">The number of bytes, if available, to read.</param>
/// <returns>The actual number of bytes read.</returns>
public static int Read(this BufferedReadStream stream, Span<byte> buffer, int offset, int count)
=> stream.Read(buffer.Slice(offset, count));
/// <summary>
/// Advances the stream by the specified number of bytes. Nonpositive counts are ignored.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="count">The number of bytes to skip.</param>
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;
}
}
}

51
src/ImageSharp/Common/Extensions/StreamExtensions.cs

@ -1,8 +1,6 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
/// <summary> /// <summary>
@ -19,53 +17,4 @@ internal static class StreamExtensions
/// <param name="count">The number of bytes to write to the stream.</param> /// <param name="count">The number of bytes to write to the stream.</param>
public static void Write(this Stream stream, Span<byte> buffer, int offset, int count) public static void Write(this Stream stream, Span<byte> buffer, int offset, int count)
=> stream.Write(buffer.Slice(offset, count)); => stream.Write(buffer.Slice(offset, count));
/// <summary>
/// Reads data from a stream into the provided buffer.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="buffer">The buffer.</param>
/// <param name="offset">The offset within the buffer where the bytes are read into.</param>
/// <param name="count">The number of bytes, if available, to read.</param>
/// <returns>The actual number of bytes read.</returns>
public static int Read(this Stream stream, Span<byte> buffer, int offset, int count)
=> stream.Read(buffer.Slice(offset, count));
/// <summary>
/// Skips the number of bytes in the given stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="count">A byte offset relative to the origin parameter.</param>
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<byte>.Shared.Rent(count);
try
{
while (count > 0)
{
int bytesRead = stream.Read(buffer, 0, count);
if (bytesRead == 0)
{
break;
}
count -= bytesRead;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
} }

3
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). /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images).
/// </param> /// </param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
=> (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
/// <summary> /// <summary>
/// Gets the luminance from the rgb components using the formula /// Gets the luminance from the rgb components using the formula

178
src/ImageSharp/Common/Helpers/Numerics.cs

@ -263,63 +263,95 @@ internal static class Numerics
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="value">The value to clamp.</param> /// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
/// <returns>The clamped <see cref="float"/>.</returns> /// <returns>The clamped <see cref="float"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float Clamp(float value, float min, float max) public static float Clamp(float value, float min, float max) => Clamp<float>(value, min, max);
{
if (value > max)
{
return max;
}
if (value < min)
{
return min;
}
return value;
}
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="value">The value to clamp.</param> /// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
/// <returns>The clamped <see cref="double"/>.</returns> /// <returns>The clamped <see cref="double"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Clamp(double value, double min, double max) public static double Clamp(double value, double min, double max) => Clamp<double>(value, min, max);
/// <summary>
/// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// </summary>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 Clamp(Vector2 value, Vector2 min, Vector2 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector2();
/// <summary>
/// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<T> Clamp<T>(Vector128<T> value, Vector128<T> min, Vector128<T> max)
where T : struct, INumber<T>
{ {
if (value > max) // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
{ Vector128<T> lowerClamped = Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(value, min), value, min);
return max; return Vector128.ConditionalSelect(Vector128.GreaterThan(value, max), max, lowerClamped);
} }
if (value < min) /// <summary>
{ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
return min; /// </summary>
} /// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<T> Clamp<T>(Vector256<T> value, Vector256<T> min, Vector256<T> max)
where T : struct, INumber<T>
{
// Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
Vector256<T> lowerClamped = Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(value, min), value, min);
return Vector256.ConditionalSelect(Vector256.GreaterThan(value, max), max, lowerClamped);
}
return value; /// <summary>
/// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The components to clamp.</param>
/// <param name="min">The inclusive lower bounds.</param>
/// <param name="max">The inclusive upper bounds.</param>
/// <returns>The clamped components.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<T> Clamp<T>(Vector512<T> value, Vector512<T> min, Vector512<T> max)
where T : struct, INumber<T>
{
// Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
Vector512<T> lowerClamped = Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(value, min), value, min);
return Vector512.ConditionalSelect(Vector512.GreaterThan(value, max), max, lowerClamped);
} }
/// <summary> /// <summary>
/// Returns the value clamped to the inclusive range of min and max. /// Clamps components to the inclusive range of min and max, mapping NaN to min.
/// 5x Faster than <see cref="Vector4.Clamp(Vector4, Vector4, Vector4)"/>
/// on platforms &lt; NET 5.
/// </summary> /// </summary>
/// <param name="value">The value to clamp.</param> /// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
/// <returns>The clamped <see cref="Vector4"/>.</returns> /// <returns>The clamped <see cref="Vector4"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector4();
=> Vector4.Min(Vector4.Max(value, min), max);
/// <summary> /// <summary>
/// Clamps the span values to the inclusive range of min and max. /// Clamps the span values to the inclusive range of min and max.
@ -352,24 +384,24 @@ internal static class Numerics
=> TensorPrimitives_.Clamp(span, min, max, span); => TensorPrimitives_.Clamp(span, min, max, span);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="span">The span containing the values to clamp.</param> /// <param name="span">The span containing the values to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<float> span, float min, float max) public static void Clamp(Span<float> span, float min, float max)
=> TensorPrimitives_.Clamp(span, min, max, span); => Clamp<float>(span, min, max);
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="span">The span containing the values to clamp.</param> /// <param name="span">The span containing the values to clamp.</param>
/// <param name="min">The minimum inclusive value.</param> /// <param name="min">The minimum inclusive value.</param>
/// <param name="max">The maximum inclusive value.</param> /// <param name="max">The maximum inclusive value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span<double> span, double min, double max) public static void Clamp(Span<double> span, double min, double max)
=> TensorPrimitives_.Clamp(span, min, max, span); => Clamp<double>(span, min, max);
/// <summary> /// <summary>
/// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact. /// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact.
@ -392,7 +424,7 @@ internal static class Numerics
public static void ClampRgbToAlpha(ref Vector4 source) public static void ClampRgbToAlpha(ref Vector4 source)
{ {
Vector4 alpha = PermuteW(source); Vector4 alpha = PermuteW(source);
source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha); source = WithW(Clamp(source, Vector4.Zero, alpha), alpha);
} }
/// <summary> /// <summary>
@ -1071,4 +1103,78 @@ internal static class Numerics
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Normalize(Span<float> span, float sum) public static void Normalize(Span<float> span, float sum)
=> TensorPrimitives_.Divide(span, sum, span); => TensorPrimitives_.Divide(span, sum, span);
/// <summary>
/// Clamps a floating-point component while mapping NaN to the lower bound.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="value">The component to clamp.</param>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
/// <returns>The clamped component.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T Clamp<T>(T value, T min, T max)
where T : struct, INumber<T>
{
// Ordered comparisons map NaN to min; in-range values retain their original bits, including signed zero.
return value > max ? max : value >= min ? value : min;
}
/// <summary>
/// Applies the scalar clamp contract to floating-point spans in place.
/// </summary>
/// <typeparam name="T">The component type.</typeparam>
/// <param name="span">The components to clamp.</param>
/// <param name="min">The inclusive lower bound.</param>
/// <param name="max">The inclusive upper bound.</param>
private static void Clamp<T>(Span<T> span, T min, T max)
where T : struct, INumber<T>
{
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<T> lower = Vector512.Create(min);
Vector512<T> upper = Vector512.Create(max);
for (; i <= span.Length - Vector512<T>.Count; i += Vector512<T>.Count)
{
Vector512<T> value = Vector512.LoadUnsafe(ref start, (nuint)i);
Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
}
}
if (Vector256.IsHardwareAccelerated)
{
Vector256<T> lower = Vector256.Create(min);
Vector256<T> upper = Vector256.Create(max);
for (; i <= span.Length - Vector256<T>.Count; i += Vector256<T>.Count)
{
Vector256<T> value = Vector256.LoadUnsafe(ref start, (nuint)i);
Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
}
}
if (Vector128.IsHardwareAccelerated)
{
Vector128<T> lower = Vector128.Create(min);
Vector128<T> upper = Vector128.Create(max);
for (; i <= span.Length - Vector128<T>.Count; i += Vector128<T>.Count)
{
Vector128<T> 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);
}
}
} }

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

@ -1429,7 +1429,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
// > 108 bytes // > 108 bytes
infoHeaderType = BmpInfoHeaderType.WinVersion5; infoHeaderType = BmpInfoHeaderType.WinVersion5;
this.infoHeader = BmpInfoHeader.ParseV5(buffer); 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; long streamPosition = stream.Position;
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart)); this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
@ -1474,8 +1474,16 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
/// <param name="infoHeaderStart">The stream position where the info header begins.</param> /// <param name="infoHeaderStart">The stream position where the info header begins.</param>
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart) 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]; byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
stream.Position = infoHeaderStart + this.infoHeader.ProfileData; stream.Position = profileStart;
if (stream.Read(iccProfileData) != iccProfileData.Length) if (stream.Read(iccProfileData) != iccProfileData.Length)
{ {
@ -1560,6 +1568,11 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
this.infoHeader.Height = -this.infoHeader.Height; 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 bytesPerColorMapEntry = 4;
int colorMapSizeBytes = -1; int colorMapSizeBytes = -1;
if (this.infoHeader.ClrUsed == 0) if (this.infoHeader.ClrUsed == 0)

2
src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs

@ -39,7 +39,7 @@ internal class B44ExrCompression : ExrBaseDecompressor
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer) public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> buffer)
{ {
Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer); Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<ushort> decompressed = this.tmpBuffer.GetSpan(); Span<ushort> decompressed = this.tmpBuffer.GetSpan();

6
src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs

@ -25,10 +25,10 @@ internal class NoneExrCompression : ExrBaseDecompressor
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer) public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> buffer)
{ {
int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); int bytesRead = stream.Read(buffer[..(int)uncompressedBytes]);
if (bytesRead != (int)this.BytesPerBlock) if (bytesRead != uncompressedBytes)
{ {
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!"); ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!");
} }

11
src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs

@ -39,19 +39,20 @@ internal class Pxr24Compression : ExrBaseDecompressor
} }
/// <inheritdoc/> /// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer) public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> buffer)
{ {
Span<byte> uncompressed = this.tmpBuffer.GetSpan(); uint rowCount = uncompressedBytes / this.BytesPerRow;
uint packedBytes = this.pixelType == ExrPixelType.Float ? (uncompressedBytes / 4) * 3 : uncompressedBytes;
Span<byte> uncompressed = this.tmpBuffer.GetSpan()[..(int)packedBytes];
Span<ushort> outputBufferHalf = MemoryMarshal.Cast<byte, ushort>(buffer); Span<ushort> outputBufferHalf = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<uint> outputBufferFloat = MemoryMarshal.Cast<byte, uint>(buffer); Span<uint> outputBufferFloat = MemoryMarshal.Cast<byte, uint>(buffer);
Span<uint> outputBufferUint = MemoryMarshal.Cast<byte, uint>(buffer); Span<uint> outputBufferUint = MemoryMarshal.Cast<byte, uint>(buffer);
uint uncompressedBytes = this.BytesPerBlock; UndoZipCompression(stream, compressedBytes, uncompressed, packedBytes);
UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
int lastIn = 0; int lastIn = 0;
int outputOffset = 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++) for (int c = 0; c < this.channelCount; c++)
{ {

2
src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs

@ -26,7 +26,7 @@ internal class RunLengthExrCompression : ExrBaseDecompressor
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock); : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/> /// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer) public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> buffer)
{ {
Span<byte> uncompressed = this.tmpBuffer.GetSpan(); Span<byte> uncompressed = this.tmpBuffer.GetSpan();
int maxLength = (int)this.BytesPerBlock; int maxLength = (int)this.BytesPerBlock;

9
src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs

@ -26,15 +26,14 @@ internal class ZipExrCompression : ExrBaseDecompressor
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock); : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/> /// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer) public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> buffer)
{ {
Span<byte> uncompressed = this.tmpBuffer.GetSpan(); Span<byte> uncompressed = this.tmpBuffer.GetSpan()[..(int)uncompressedBytes];
uint uncompressedBytes = (uint)buffer.Length;
int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
Reconstruct(uncompressed, (uint)totalRead); Reconstruct(uncompressed, uncompressedBytes);
Interleave(uncompressed, (uint)totalRead, buffer); Interleave(uncompressed, uncompressedBytes, buffer);
} }
/// <inheritdoc/> /// <inheritdoc/>

19
src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs

@ -31,8 +31,9 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression
/// </summary> /// </summary>
/// <param name="stream">The buffered stream to decompress.</param> /// <param name="stream">The buffered stream to decompress.</param>
/// <param name="compressedBytes">The compressed bytes.</param> /// <param name="compressedBytes">The compressed bytes.</param>
/// <param name="uncompressedBytes">The expected byte count for the current block.</param>
/// <param name="buffer">The buffer to write the decompressed data to.</param> /// <param name="buffer">The buffer to write the decompressed data to.</param>
public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer); public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> buffer);
/// <summary> /// <summary>
/// Decompresses zip compressed data. /// Decompresses zip compressed data.
@ -52,13 +53,19 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression
int left = (int)(compressedBytes - (stream.Position - pos)); int left = (int)(compressedBytes - (stream.Position - pos));
return left > 0 ? left : 0; 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; int totalRead = 0;
while (totalRead < uncompressedBytes) 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) if (bytesRead <= 0)
{ {
break; break;
@ -67,9 +74,9 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression
totalRead += bytesRead; 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; return totalRead;

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

@ -166,7 +166,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
int height = this.Height; int height = this.Height;
int channelCount = this.Channels.Count; int channelCount = this.Channels.Count;
using IMemoryOwner<float> rowBuffer = this.memoryAllocator.Allocate<float>(width * 4); // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row.
using IMemoryOwner<float> rowBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock); using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
Span<byte> decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span<byte> decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
Span<float> redPixelData = rowBuffer.GetSpan()[..width]; Span<float> redPixelData = rowBuffer.GetSpan()[..width];
@ -192,10 +193,19 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.ValidateChunkOffset(rowOffset, stream); this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset; 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); 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; int offset = 0;
for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++)
@ -247,7 +257,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
int height = this.Height; int height = this.Height;
int channelCount = this.Channels.Count; int channelCount = this.Channels.Count;
using IMemoryOwner<uint> rowBuffer = this.memoryAllocator.Allocate<uint>(width * 4); // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row.
using IMemoryOwner<uint> rowBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock); using IMemoryOwner<byte> decompressedPixelDataBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock);
Span<byte> decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span<byte> decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
Span<uint> redPixelData = rowBuffer.GetSpan()[..width]; Span<uint> redPixelData = rowBuffer.GetSpan()[..width];
@ -273,10 +284,19 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
this.ValidateChunkOffset(rowOffset, stream); this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset; 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); 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; int offset = 0;
for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++)
@ -305,6 +325,28 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
} }
} }
/// <summary>
/// Decompresses a block according to the configured image-data integrity policy.
/// </summary>
/// <param name="decompressor">The decompressor for the stored compression type.</param>
/// <param name="stream">The encoded block stream.</param>
/// <param name="compressedBytes">The declared compressed byte count.</param>
/// <param name="uncompressedBytes">The expected byte count for the rows in this block.</param>
/// <param name="buffer">The reusable decompressed pixel buffer.</param>
private void DecompressBlock(ExrBaseDecompressor decompressor, BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span<byte> 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();
}
}
/// <summary> /// <summary>
/// Reads float image channel data. /// Reads float image channel data.
/// </summary> /// </summary>

9
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -261,11 +261,6 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.currentLocalColorTable?.Dispose(); 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 // Ignoring a malformed ancillary extension must not let identify succeed for a file
// that never contained any readable image frame data. // that never contained any readable image frame data.
if (previousFrame is null) if (previousFrame is null)
@ -328,6 +323,10 @@ internal sealed class GifDecoderCore : ImageDecoderCore
} }
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); 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.");
}
} }
/// <summary> /// <summary>

4
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -28,7 +28,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <param name="allocator">The memory allocator.</param> /// <param name="allocator">The memory allocator.</param>
/// <returns>The XMP metadata</returns> /// <returns>The XMP metadata</returns>
public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) public static GifXmpApplicationExtension Read(BufferedReadStream stream, MemoryAllocator allocator)
{ {
byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated); byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
if (!terminated) if (!terminated)
@ -75,7 +75,7 @@ internal readonly struct GifXmpApplicationExtension : IGifExtension
return this.ContentLength; 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); using ChunkedMemoryStream bytes = new(allocator);

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

@ -274,10 +274,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// Get the marker length. // Get the marker length.
int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2;
// Check whether the stream actually has enough bytes to read // Validate the entire segment before parsing it. Casting directly
// markerContentByteSize is always positive so we cast // to ulong also rejects lengths smaller than the two-byte length field.
// to uint to avoid sign extension if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize))
if (stream.RemainingBytes < (uint)markerContentByteSize)
{ {
JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker);
} }
@ -351,10 +350,9 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// Get the marker length. // Get the marker length.
int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2; int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2;
// Check whether stream actually has enough bytes to read // Validate the entire segment before parsing it. Casting directly
// markerContentByteSize is always positive so we cast // to ulong also rejects lengths smaller than the two-byte length field.
// to uint to avoid sign extension. if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize))
if (stream.RemainingBytes < (uint)markerContentByteSize)
{ {
if (metadataOnly && this.Metadata != null && this.Frame != null) if (metadataOnly && this.Metadata != null && this.Frame != null)
{ {
@ -841,7 +839,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
// TODO: thumbnail // TODO: thumbnail
if (remaining > 0) if (remaining > 0)
{ {
if (stream.Position + remaining >= stream.Length) if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1))
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length."); this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
stream.Skip(remaining); stream.Skip(remaining);
@ -877,7 +875,7 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
return; return;
} }
if (stream.Position + remaining >= stream.Length) if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1))
{ {
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length."); this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
stream.Skip(remaining); stream.Skip(remaining);

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

@ -195,11 +195,6 @@ internal sealed class PngDecoderCore : ImageDecoderCore
switch (chunk.Type) switch (chunk.Type)
{ {
case PngChunkType.Header: case PngChunkType.Header:
if (!Equals(this.header, default(PngHeader)))
{
PngThrowHelper.ThrowInvalidHeader();
}
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.AnimationControl: case PngChunkType.AnimationControl:
@ -656,7 +651,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
frameMetadata.FromChunk(in frameControl); frameMetadata.FromChunk(in frameControl);
this.bytesPerPixel = this.CalculateBytesPerPixel(); 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; this.bytesPerSample = 1;
if (this.header.BitDepth >= 8) if (this.header.BitDepth >= 8)
{ {
@ -741,21 +736,29 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// Calculates the scanline length. /// Calculates the scanline length.
/// </summary> /// </summary>
/// <param name="width">The width of the row.</param> /// <param name="width">The width of the row.</param>
/// <param name="bitDepth">The number of bits per sample.</param>
/// <param name="bytesPerPixel">The number of bytes per pixel.</param>
/// <returns> /// <returns>
/// The <see cref="int"/> representing the length. /// The <see cref="int"/> representing the length.
/// </returns> /// </returns>
private int CalculateScanlineLength(int width) internal static int CalculateScanlineLength(int width, int bitDepth, int bytesPerPixel)
{ {
int mod = this.header.BitDepth == 16 ? 16 : 8; int mod = bitDepth == 16 ? 16 : 8;
int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel; long scanlineLength = (long)width * bitDepth * bytesPerPixel;
int amount = scanlineLength % mod; long amount = scanlineLength % mod;
if (amount != 0) if (amount != 0)
{ {
scanlineLength += mod - amount; scanlineLength += mod - amount;
} }
return scanlineLength / mod; scanlineLength /= mod;
if (scanlineLength >= int.MaxValue)
{
PngThrowHelper.ThrowInvalidImageContentException("PNG scanline length exceeds the supported maximum.");
}
return (int)scanlineLength;
} }
/// <summary> /// <summary>
@ -875,13 +878,13 @@ internal sealed class PngDecoderCore : ImageDecoderCore
while (currentRow < height) while (currentRow < height)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1; int bytesPerFrameScanline = CalculateScanlineLength((int)frameControl.Width, this.header.BitDepth, this.bytesPerPixel) + 1;
Span<byte> scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline]; Span<byte> scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline];
Span<byte> prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline]; Span<byte> prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline];
while (currentRowBytesRead < bytesPerFrameScanline) while (currentRowBytesRead < bytesPerFrameScanline)
{ {
int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead); int bytesRead = compressedStream.Read(scanSpan.Slice(currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead));
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
goto EXIT; goto EXIT;
@ -1006,14 +1009,14 @@ internal sealed class PngDecoderCore : ImageDecoderCore
continue; continue;
} }
int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1; int bytesPerInterlaceScanline = CalculateScanlineLength(numColumns, this.header.BitDepth, this.bytesPerPixel) + 1;
while (currentRow < endRow) while (currentRow < endRow)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
while (currentRowBytesRead < bytesPerInterlaceScanline) 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) if (bytesRead <= 0)
{ {
goto EXIT; goto EXIT;
@ -1439,6 +1442,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param> /// <param name="data">The <see cref="T:ReadOnlySpan{byte}"/> containing data.</param>
private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan<byte> data) private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan<byte> data)
{ {
if (!Equals(this.header, default(PngHeader)))
{
PngThrowHelper.ThrowInvalidHeader();
}
this.header = PngHeader.Parse(data); this.header = PngHeader.Parse(data);
this.header.Validate(); this.header.Validate();
@ -1976,21 +1984,36 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return false; return false;
} }
int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); try
while (bytesRead != 0)
{ {
if (memoryStreamOutput.Length > maxLength) int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
while (bytesRead != 0)
{ {
uncompressedBytesArray = []; if (memoryStreamOutput.Length > maxLength)
return false; {
uncompressedBytesArray = [];
return false;
}
memoryStreamOutput.Write(destUncompressedData[..bytesRead]);
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
} }
memoryStreamOutput.Write(destUncompressedData[..bytesRead]); uncompressedBytesArray = memoryStreamOutput.ToArray();
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); 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(); uncompressedBytesArray = [];
return true; return false;
}
} }
} }

4
src/ImageSharp/Formats/Tga/TgaDecoderCore.cs

@ -72,9 +72,9 @@ internal sealed class TgaDecoderCore : ImageDecoderCore
TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found"); 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<TPixel> image = new(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Image<TPixel> image = new(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata);

8
src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs

@ -126,6 +126,14 @@ internal sealed class T4BitCompressor : TiffCcittCompressor
} }
} }
/// <inheritdoc />
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<byte> compressedData) private void WriteEndOfLine(Span<byte> compressedData)
{ {
if (this.useModifiedHuffman) if (this.useModifiedHuffman)

8
src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs

@ -131,6 +131,14 @@ internal sealed class T6BitCompressor : TiffCcittCompressor
this.WriteCode(12, 1, compressedData); this.WriteCode(12, 1, compressedData);
} }
/// <inheritdoc />
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;
}
/// <inheritdoc /> /// <inheritdoc />
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {

24
src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs

@ -465,6 +465,12 @@ internal abstract class TiffCcittCompressor : TiffBaseCompressor
/// <param name="compressedData">The destination buffer to write the code to.</param> /// <param name="compressedData">The destination buffer to write the code to.</param>
protected void WriteCode(uint codeLength, uint code, Span<byte> compressedData) protected void WriteCode(uint codeLength, uint code, Span<byte> 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) while (codeLength > 0)
{ {
int bitNumber = (int)codeLength; int bitNumber = (int)codeLength;
@ -526,8 +532,20 @@ internal abstract class TiffCcittCompressor : TiffBaseCompressor
/// <inheritdoc/> /// <inheritdoc/>
public override void Initialize(int rowsPerStrip) 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. long maxNeededBits = this.GetMaximumEncodedBits(rowsPerStrip);
int maxNeededBytes = this.Width * rowsPerStrip; ulong maxNeededBytes = (ulong)((maxNeededBits + 7) / 8);
this.compressedDataBuffer = this.Allocator.Allocate<byte>(maxNeededBytes); if (maxNeededBytes > int.MaxValue)
{
InvalidMemoryOperationException.ThrowAllocationOverLimitException(maxNeededBytes, int.MaxValue);
}
this.compressedDataBuffer = this.Allocator.Allocate<byte>((int)maxNeededBytes);
} }
/// <summary>
/// Gets an upper bound for the encoded strip length in bits.
/// </summary>
/// <param name="rowsPerStrip">The number of rows in the strip.</param>
/// <returns>The maximum encoded length.</returns>
protected abstract long GetMaximumEncodedBits(int rowsPerStrip);
} }

2
src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs

@ -69,7 +69,7 @@ internal sealed class DeflateTiffCompression : TiffBaseDecompressor
int totalRead = 0; int totalRead = 0;
while (totalRead < buffer.Length) while (totalRead < buffer.Length)
{ {
int bytesRead = dataStream.Read(buffer, totalRead, buffer.Length - totalRead); int bytesRead = dataStream.Read(buffer[totalRead..]);
if (bytesRead <= 0) if (bytesRead <= 0)
{ {
break; break;

2
src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs

@ -34,7 +34,7 @@ internal abstract class BitReaderBase : IDisposable
{ {
IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead, AllocationOptions.Clean); IMemoryOwner<byte> data = memoryAllocator.Allocate<byte>(bytesToRead, AllocationOptions.Clean);
Span<byte> dataSpan = data.Memory.Span; Span<byte> dataSpan = data.Memory.Span;
input.Read(dataSpan[..bytesToRead], 0, bytesToRead); input.Read(dataSpan[..bytesToRead]);
return data; return data;
} }

1
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -143,6 +143,7 @@ internal abstract class BitWriterBase
{ {
if (exifProfile != null) if (exifProfile != null)
{ {
// Serialization applies Parts even when the current profile has not been initialized.
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
} }

4
src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
namespace SixLabors.ImageSharp.Formats.Webp.Chunks; namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
internal readonly struct WebpFrameData internal readonly struct WebpFrameData
@ -120,7 +122,7 @@ internal readonly struct WebpFrameData
/// </summary> /// </summary>
/// <param name="stream">The stream to read from.</param> /// <param name="stream">The stream to read from.</param>
/// <returns>Animation frame data.</returns> /// <returns>Animation frame data.</returns>
public static WebpFrameData Parse(Stream stream) public static WebpFrameData Parse(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[4]; Span<byte> buffer = stackalloc byte[4];

4
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -384,7 +384,9 @@ internal class WebpAnimationDecoder : IDisposable
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must // 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. // 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; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata)); this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata));

131
src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs

@ -262,7 +262,7 @@ internal static class WebpChunkParsingUtils
/// <exception cref="ImageFormatException"> /// <exception cref="ImageFormatException">
/// Thrown if the input stream is not valid. /// Thrown if the input stream is not valid.
/// </exception> /// </exception>
public static uint ReadUInt24LittleEndian(Stream stream, Span<byte> buffer) public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span<byte> buffer)
{ {
if (stream.Read(buffer, 0, 3) == 3) if (stream.Read(buffer, 0, 3) == 3)
{ {
@ -306,12 +306,33 @@ internal static class WebpChunkParsingUtils
/// <param name="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param> /// <param name="required">If true, the chunk size is required to be read, otherwise it can be skipped.</param>
/// <returns>The chunk size in bytes.</returns> /// <returns>The chunk size in bytes.</returns>
/// <exception cref="ImageFormatException">Thrown if the input stream is not valid.</exception> /// <exception cref="ImageFormatException">Thrown if the input stream is not valid.</exception>
public static uint ReadChunkSize(Stream stream, Span<byte> buffer, bool required = true) public static uint ReadChunkSize(BufferedReadStream stream, Span<byte> 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;
}
/// <summary>
/// Reads a chunk's complete padded extent without wrapping a uint-sized payload length.
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="buffer">The four-byte size buffer.</param>
/// <param name="required">Whether an incomplete size field is an error.</param>
/// <returns>The padded extent, or remaining bytes when an optional size field is incomplete.</returns>
private static ulong ReadPaddedChunkSize(BufferedReadStream stream, Span<byte> buffer, bool required)
{ {
if (stream.Read(buffer) is 4) if (stream.Read(buffer) is 4)
{ {
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer); uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1; return (ulong)chunkSize + (chunkSize & 1);
} }
if (required) if (required)
@ -320,7 +341,7 @@ internal static class WebpChunkParsingUtils
} }
// Return the size of the remaining data in the stream. // Return the size of the remaining data in the stream.
return (uint)(stream.Length - stream.Position); return (ulong)stream.RemainingBytes;
} }
/// <summary> /// <summary>
@ -349,34 +370,36 @@ internal static class WebpChunkParsingUtils
/// <param name="stream">The stream to decode from.</param> /// <param name="stream">The stream to decode from.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="ignoreMetadata">If true, metadata will be ignored.</param> /// <param name="ignoreMetadata">If true, metadata will be ignored.</param>
/// <param name="executeAncillarySegmentAction">Executes profile parsing under the decoder's integrity policy.</param>
public static void ReadIccProfile( public static void ReadIccProfile(
BufferedReadStream stream, BufferedReadStream stream,
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata) bool ignoreMetadata,
Action<Action> executeAncillarySegmentAction)
{ {
Span<byte> buffer = stackalloc byte[4]; ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], true);
uint iccpChunkSize = ReadChunkSize(stream, buffer);
if (ignoreMetadata || metadata.IccProfile != null) // 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]; byte[]? iccpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.IccProfile != null);
int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize); if (iccpData is not null)
if (bytesRead != iccpChunkSize)
{ {
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); metadata.IccProfile = profile;
if (!profile.CheckIsValid())
{
throw new InvalidIccProfileException("Invalid ICC profile.");
} }
});
metadata.IccProfile = profile;
}
} }
/// <summary> /// <summary>
@ -390,21 +413,10 @@ internal static class WebpChunkParsingUtils
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata) bool ignoreMetadata)
{ {
Span<byte> buffer = stackalloc byte[4]; ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata);
uint exifChunkSize = ReadChunkSize(stream, buffer); byte[]? exifData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.ExifProfile != null);
if (ignoreMetadata || metadata.ExifProfile != null) if (exifData is not null)
{
stream.Skip((int)exifChunkSize);
}
else
{ {
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); ExifProfile exifProfile = new(exifData);
// Set the resolution from the metadata. // Set the resolution from the metadata.
@ -433,23 +445,46 @@ internal static class WebpChunkParsingUtils
ImageMetadata metadata, ImageMetadata metadata,
bool ignoreMetadata) bool ignoreMetadata)
{ {
Span<byte> buffer = stackalloc byte[4]; ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata);
uint xmpChunkSize = ReadChunkSize(stream, buffer); byte[]? xmpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.XmpProfile != null);
if (ignoreMetadata || metadata.XmpProfile != null) if (xmpData is not null)
{
metadata.XmpProfile = new XmpProfile(xmpData);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stream">The input stream positioned at the chunk payload.</param>
/// <param name="paddedLength">The declared extent including its padding byte.</param>
/// <param name="ignoreMetadata">Whether to skip the payload without parsing it.</param>
/// <returns>The payload, or null when metadata is skipped.</returns>
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]; // Ignoring an ancillary error must not make the next parser interpret
int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize); // this payload as another chunk header. A truncated chunk consumes EOF.
if (bytesRead != xmpChunkSize) stream.Position = chunkEnd;
{ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk.");
WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile"); }
}
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<Rational> tag) private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag<Rational> tag)

17
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -92,6 +92,13 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
return animationDecoder.Decode<TPixel>(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); return animationDecoder.Decode<TPixel>(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<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata); image = new Image<TPixel>(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless) if (this.webImageInfo.IsLossless)
@ -281,7 +288,9 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must // 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. // 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; break;
case WebpChunkType.Exif: case WebpChunkType.Exif:
@ -330,17 +339,17 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
{ {
// Read chunk header. // Read chunk header.
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer); WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null) if (chunkType == WebpChunkType.Exif)
{ {
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata)); 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)); this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
} }
else else
{ {
// Skip duplicate XMP or EXIF chunk. // Skip unknown chunks.
uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false); uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength); stream.Skip((int)chunkLength);
} }

11
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -221,14 +221,21 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
=> this.SetValueInternal(tag, value); => this.SetValueInternal(tag, value);
/// <summary> /// <summary>
/// Converts this instance to a byte array. /// Converts the sections selected by <see cref="Parts"/> to a byte array.
/// </summary> /// </summary>
/// <returns>The <see cref="T:byte[]"/></returns> /// <returns>The <see cref="T:byte[]"/></returns>
public byte[]? ToByteArray() public byte[]? ToByteArray()
{ {
if (this.values is null) 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) if (this.values.Count == 0)

7
src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs

@ -224,6 +224,13 @@ internal abstract class BaseExifReader
this.Seek(offset); this.Seek(offset);
ulong count = this.ReadUInt64(); 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<byte> offsetBuffer = stackalloc byte[8]; Span<byte> offsetBuffer = stackalloc byte[8];
for (ulong i = 0; i < count; i++) for (ulong i = 0; i < count; i++)
{ {

48
src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs

@ -72,13 +72,7 @@ internal sealed partial class IccDataReader
/// <returns>The read CLUT8.</returns> /// <returns>The read CLUT8.</returns>
public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount) public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount)
{ {
int length = 0; int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 1);
for (int i = 0; i < inChannelCount; i++)
{
length += (int)Math.Pow(gridPointCount[i], inChannelCount);
}
length /= inChannelCount;
const float Max = byte.MaxValue; const float Max = byte.MaxValue;
@ -105,13 +99,7 @@ internal sealed partial class IccDataReader
public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount) public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount)
{ {
int start = this.currentIndex; int start = this.currentIndex;
int length = 0; int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 2);
for (int i = 0; i < inChannelCount; i++)
{
length += (int)Math.Pow(gridPointCount[i], inChannelCount);
}
length /= inChannelCount;
const float Max = ushort.MaxValue; const float Max = ushort.MaxValue;
@ -139,13 +127,7 @@ internal sealed partial class IccDataReader
public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount) public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount)
{ {
int start = this.currentIndex; int start = this.currentIndex;
int length = 0; int length = this.GetClutLength(inChCount, outChCount, gridPointCount, 4);
for (int i = 0; i < inChCount; i++)
{
length += (int)Math.Pow(gridPointCount[i], inChCount);
}
length /= inChCount;
float[] values = new float[length * outChCount]; float[] values = new float[length * outChCount];
int offset = 0; int offset = 0;
@ -160,4 +142,28 @@ internal sealed partial class IccDataReader
this.currentIndex = start + (length * outChCount * 4); this.currentIndex = start + (length * outChCount * 4);
return new IccClut(values, gridPointCount, IccClutDataType.Float, outChCount); 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;
}
} }

3
src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs

@ -119,6 +119,7 @@ internal sealed class IccReader
} }
List<IccTagTableEntry> table = new((int)tagCount); List<IccTagTableEntry> table = new((int)tagCount);
uint dataLength = (uint)reader.DataLength;
for (int i = 0; i < tagCount; i++) for (int i = 0; i < tagCount; i++)
{ {
uint tagSignature = reader.ReadUInt32(); uint tagSignature = reader.ReadUInt32();
@ -126,7 +127,7 @@ internal sealed class IccReader
uint tagSize = reader.ReadUInt32(); uint tagSize = reader.ReadUInt32();
// Exclude entries that have nonsense values and could cause exceptions further on // 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)); table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize));
} }

201
src/ImageSharp/PixelFormats/HalfTypeHelper.cs

@ -3,6 +3,7 @@
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.PixelFormats;
@ -50,52 +51,228 @@ internal static class HalfTypeHelper
internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value); internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value);
/// <summary> /// <summary>
/// Normalizes a finite binary16 value to the scaled pixel range. /// Normalizes a binary16 value to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary> /// </summary>
/// <param name="value">The native binary16 value represented as a <see cref="float"/>.</param> /// <param name="value">The native binary16 value represented as a <see cref="float"/>.</param>
/// <returns>The normalized value.</returns> /// <returns>The normalized value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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);
}
/// <summary> /// <summary>
/// Normalizes finite binary16 values to the scaled pixel range. /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary> /// </summary>
/// <param name="value">The native binary16 values.</param> /// <param name="value">The native binary16 values.</param>
/// <returns>The normalized values.</returns> /// <returns>The normalized values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint); public static Vector2 ToScaled(Vector2 value) => ToScaled(value.AsVector128()).AsVector2();
/// <summary> /// <summary>
/// Normalizes finite binary16 values to the scaled pixel range. /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary> /// </summary>
/// <param name="value">The native binary16 values.</param> /// <param name="value">The native binary16 values.</param>
/// <returns>The normalized values.</returns> /// <returns>The normalized values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint); public static Vector4 ToScaled(Vector4 value) => ToScaled(value.AsVector128()).AsVector4();
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> ToScaled(Vector128<float> value)
{
Vector128<float> scaled = (value * Vector128.Create(InverseFiniteRange)) + Vector128.Create(ScaledMidpoint);
return Numerics.Clamp(scaled, Vector128<float>.Zero, Vector128<float>.One);
}
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> ToScaled(Vector256<float> value)
{
Vector256<float> scaled = (value * Vector256.Create(InverseFiniteRange)) + Vector256.Create(ScaledMidpoint);
return Numerics.Clamp(scaled, Vector256<float>.Zero, Vector256<float>.One);
}
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> ToScaled(Vector512<float> value)
{
Vector512<float> scaled = (value * Vector512.Create(InverseFiniteRange)) + Vector512.Create(ScaledMidpoint);
return Numerics.Clamp(scaled, Vector512<float>.Zero, Vector512<float>.One);
}
/// <summary>
/// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
/// </summary>
/// <param name="values">The component values to convert in place.</param>
public static void ToScaled(Span<Vector4> 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<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector512<float> vector = ref Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref source, (uint)i));
vector = ToScaled(vector);
}
}
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerRegister = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector256<float> vector = ref Unsafe.As<Vector4, Vector256<float>>(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);
}
}
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="value">The normalized value.</param> /// <param name="value">The normalized value.</param>
/// <returns>The native binary16 value represented as a <see cref="float"/>.</returns> /// <returns>The native binary16 value represented as a <see cref="float"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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;
}
/// <summary> /// <summary>
/// Expands normalized values to the finite binary16 range. /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary> /// </summary>
/// <param name="value">The normalized values.</param> /// <param name="value">The normalized values.</param>
/// <returns>The native binary16 values.</returns> /// <returns>The native binary16 values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum); public static Vector2 FromScaled(Vector2 value) => FromScaled(value.AsVector128()).AsVector2();
/// <summary> /// <summary>
/// Expands normalized values to the finite binary16 range. /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary> /// </summary>
/// <param name="value">The normalized values.</param> /// <param name="value">The normalized values.</param>
/// <returns>The native binary16 values.</returns> /// <returns>The native binary16 values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum); public static Vector4 FromScaled(Vector4 value) => FromScaled(value.AsVector128()).AsVector4();
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector128<float> FromScaled(Vector128<float> value)
{
Vector128<float> scaled = Numerics.Clamp(value, Vector128<float>.Zero, Vector128<float>.One);
return (scaled * Vector128.Create(FiniteRange)) + Vector128.Create(FiniteMinimum);
}
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector256<float> FromScaled(Vector256<float> value)
{
Vector256<float> scaled = Numerics.Clamp(value, Vector256<float>.Zero, Vector256<float>.One);
return (scaled * Vector256.Create(FiniteRange)) + Vector256.Create(FiniteMinimum);
}
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="value">The component values.</param>
/// <returns>The converted values.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector512<float> FromScaled(Vector512<float> value)
{
Vector512<float> scaled = Numerics.Clamp(value, Vector512<float>.Zero, Vector512<float>.One);
return (scaled * Vector512.Create(FiniteRange)) + Vector512.Create(FiniteMinimum);
}
/// <summary>
/// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
/// </summary>
/// <param name="values">The component values to convert in place.</param>
public static void FromScaled(Span<Vector4> 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<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector512<float> vector = ref Unsafe.As<Vector4, Vector512<float>>(ref Unsafe.Add(ref source, (uint)i));
vector = FromScaled(vector);
}
}
if (Vector256.IsHardwareAccelerated)
{
int pixelsPerRegister = Vector256<float>.Count / Vector128<float>.Count;
for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
{
ref Vector256<float> vector = ref Unsafe.As<Vector4, Vector256<float>>(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);
}
}
/// <summary> /// <summary>
/// Unpacks eight binary16 values into two vectors of single-precision values. /// Unpacks eight binary16 values into two vectors of single-precision values.

2
src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs

@ -250,7 +250,7 @@ public partial struct HalfVector4P : IPixel<HalfVector4P>, IPackedVector<ulong>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float QuantizeScaledAlpha(float alpha) 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))); return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha)));
} }

20
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs

@ -3,7 +3,6 @@
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.PixelFormats;
@ -17,11 +16,6 @@ public partial struct HalfVector4
/// </summary> /// </summary>
internal class PixelOperations : PixelOperations<HalfVector4> internal class PixelOperations : PixelOperations<HalfVector4>
{ {
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);
/// <inheritdoc /> /// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4> source, Span<Vector4> destination) protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4> source, Span<Vector4> destination)
{ {
@ -40,9 +34,9 @@ public partial struct HalfVector4
// Association uses normalized opacity, not the native binary16 alpha value. // Association uses normalized opacity, not the native binary16 alpha value.
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination); RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(destination);
Numerics.Premultiply(destination); Numerics.Premultiply(destination);
Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(destination);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -52,7 +46,7 @@ public partial struct HalfVector4
destination = destination[..source.Length]; destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination); RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(destination);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -77,9 +71,9 @@ public partial struct HalfVector4
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// Restore normalized opacity before unassociating, then return the result to the native binary16 range. // 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); Numerics.UnPremultiply(source);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length])); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
} }
@ -88,7 +82,7 @@ public partial struct HalfVector4
{ {
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length])); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
} }
@ -98,7 +92,7 @@ public partial struct HalfVector4
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Numerics.UnPremultiply(source); Numerics.UnPremultiply(source);
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length])); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4, RgbaHalfP>(destination[..source.Length]));
} }
} }

63
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs

@ -20,16 +20,11 @@ public partial struct HalfVector4P
/// </summary> /// </summary>
internal class PixelOperations : AssociatedAlphaPixelOperations<HalfVector4P> internal class PixelOperations : AssociatedAlphaPixelOperations<HalfVector4P>
{ {
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);
/// <inheritdoc /> /// <inheritdoc />
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination) protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan<HalfVector4P> source, Span<Vector4> destination)
{ {
this.ToUnassociatedScaledVector4(configuration, source, destination); this.ToUnassociatedScaledVector4(configuration, source, destination);
Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(destination[..source.Length]);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -54,7 +49,7 @@ public partial struct HalfVector4P
destination = destination[..source.Length]; destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(source), destination); RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(source), destination);
Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(destination);
} }
/// <inheritdoc /> /// <inheritdoc />
@ -62,7 +57,7 @@ public partial struct HalfVector4P
{ {
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(source);
Associate(source); Associate(source);
PackAssociatedScaled(source, destination[..source.Length]); PackAssociatedScaled(source, destination[..source.Length]);
} }
@ -72,7 +67,7 @@ public partial struct HalfVector4P
{ {
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination)); Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset); HalfTypeHelper.ToScaled(source);
Reassociate(source); Reassociate(source);
PackAssociatedScaled(source, destination[..source.Length]); PackAssociatedScaled(source, destination[..source.Length]);
} }
@ -250,7 +245,9 @@ public partial struct HalfVector4P
Vector128<float> storedAlpha = QuantizeScaledAlpha(alpha); Vector128<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector128<float> result = source * (storedAlpha / alpha); Vector128<float> result = source * (storedAlpha / alpha);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -267,7 +264,9 @@ public partial struct HalfVector4P
Vector256<float> storedAlpha = QuantizeScaledAlpha(alpha); Vector256<float> storedAlpha = QuantizeScaledAlpha(alpha);
Vector256<float> result = source * (storedAlpha / alpha); Vector256<float> result = source * (storedAlpha / alpha);
result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -285,7 +284,9 @@ public partial struct HalfVector4P
Vector512<float> result = source * (storedAlpha / alpha); Vector512<float> result = source * (storedAlpha / alpha);
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); Vector512<float> 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.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); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -297,8 +298,8 @@ public partial struct HalfVector4P
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> QuantizeScaledAlpha(Vector128<float> alpha) private static Vector128<float> QuantizeScaledAlpha(Vector128<float> alpha)
{ {
Vector128<float> native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum); Vector128<float> native = HalfTypeHelper.FromScaled(alpha);
return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint); return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
} }
/// <summary> /// <summary>
@ -309,8 +310,8 @@ public partial struct HalfVector4P
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> QuantizeScaledAlpha(Vector256<float> alpha) private static Vector256<float> QuantizeScaledAlpha(Vector256<float> alpha)
{ {
Vector256<float> native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum); Vector256<float> native = HalfTypeHelper.FromScaled(alpha);
return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint); return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
} }
/// <summary> /// <summary>
@ -321,45 +322,33 @@ public partial struct HalfVector4P
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> QuantizeScaledAlpha(Vector512<float> alpha) private static Vector512<float> QuantizeScaledAlpha(Vector512<float> alpha)
{ {
Vector512<float> native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum); Vector512<float> native = HalfTypeHelper.FromScaled(alpha);
return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint); return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
} }
/// <summary> /// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes. /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
/// </summary> /// </summary>
/// <param name="source">The vectors to clamp.</param> /// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns> /// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ClampUnit(Vector128<float> source) private static Vector128<float> ClampUnit(Vector128<float> source) => Numerics.Clamp(source, Vector128<float>.Zero, Vector128<float>.One);
{
Vector128<float> clamped = Vector128.Min(Vector128.Max(source, Vector128<float>.Zero), Vector128<float>.One);
return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source);
}
/// <summary> /// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes. /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
/// </summary> /// </summary>
/// <param name="source">The vectors to clamp.</param> /// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns> /// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ClampUnit(Vector256<float> source) private static Vector256<float> ClampUnit(Vector256<float> source) => Numerics.Clamp(source, Vector256<float>.Zero, Vector256<float>.One);
{
Vector256<float> clamped = Vector256.Min(Vector256.Max(source, Vector256<float>.Zero), Vector256<float>.One);
return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source);
}
/// <summary> /// <summary>
/// Clamps vectors to the scaled color range while preserving NaN lanes. /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
/// </summary> /// </summary>
/// <param name="source">The vectors to clamp.</param> /// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns> /// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ClampUnit(Vector512<float> source) private static Vector512<float> ClampUnit(Vector512<float> source) => Numerics.Clamp(source, Vector512<float>.Zero, Vector512<float>.One);
{
Vector512<float> clamped = Vector512.Min(Vector512.Max(source, Vector512<float>.Zero), Vector512<float>.One);
return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source);
}
/// <summary> /// <summary>
/// Maps associated scaled vectors to native components and packs them as binary16 values. /// Maps associated scaled vectors to native components and packs them as binary16 values.
@ -368,7 +357,7 @@ public partial struct HalfVector4P
/// <param name="destination">The destination pixels.</param> /// <param name="destination">The destination pixels.</param>
private static void PackAssociatedScaled(Span<Vector4> source, Span<HalfVector4P> destination) private static void PackAssociatedScaled(Span<Vector4> source, Span<HalfVector4P> destination)
{ {
Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset); HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(destination)); RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast<HalfVector4P, RgbaHalfP>(destination));
} }
} }

94
src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs

@ -277,8 +277,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count) for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{ {
Vector512<float> lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)); Vector512<float> lower = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count))); Vector512<float> upper = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)), Vector512<float>.Zero, Vector512<float>.One);
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -287,8 +287,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count) for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{ {
Vector256<float> lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)); Vector256<float> lower = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count))); Vector256<float> upper = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)), Vector256<float>.Zero, Vector256<float>.One);
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -297,15 +297,15 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count) for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{ {
Vector128<float> lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); Vector128<float> lower = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count))); Vector128<float> upper = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)), Vector128<float>.Zero, Vector128<float>.One);
Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
if (i < componentCount) if (i < componentCount)
{ {
// Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel.
Vector128<float> vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)); Vector128<float> vector = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128<float>.Zero, Vector128<float>.One);
Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector); Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
} }
@ -401,8 +401,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count) for (; i <= componentCount - Vector512<ushort>.Count; i += Vector512<ushort>.Count)
{ {
Vector512<float> lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i))); Vector512<float> lower = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)), Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count)))); Vector512<float> upper = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512<float>.Count))), Vector512<float>.Zero, Vector512<float>.One);
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -411,8 +411,8 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count) for (; i <= componentCount - Vector256<ushort>.Count; i += Vector256<ushort>.Count)
{ {
Vector256<float> lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i))); Vector256<float> lower = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)), Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count)))); Vector256<float> upper = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256<float>.Count))), Vector256<float>.Zero, Vector256<float>.One);
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
} }
@ -421,15 +421,15 @@ public partial struct RgbaHalfP
{ {
for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count) for (; i <= componentCount - Vector128<ushort>.Count; i += Vector128<ushort>.Count)
{ {
Vector128<float> lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); Vector128<float> lower = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count)))); Vector128<float> upper = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128<float>.Count))), Vector128<float>.Zero, Vector128<float>.One);
Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i); Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
} }
if (i < componentCount) if (i < componentCount)
{ {
// Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel. // Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel.
Vector128<float> vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i))); Vector128<float> vector = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128<float>.Zero, Vector128<float>.One);
Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector); Vector128<ushort> packed = HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0)); Unsafe.WriteUnaligned(ref Unsafe.As<ushort, byte>(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
} }
@ -649,48 +649,6 @@ public partial struct RgbaHalfP
return Numerics.UnPremultiply(source, alpha); return Numerics.UnPremultiply(source, alpha);
} }
/// <summary>
/// Clamps vectors to the unit range represented by the pixel format.
/// </summary>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> ClampUnit(Vector128<float> source)
{
Vector128<float> clamped = Vector128.Min(Vector128.Max(source, Vector128<float>.Zero), Vector128<float>.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);
}
/// <summary>
/// Clamps vectors to the unit range represented by the pixel format.
/// </summary>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ClampUnit(Vector256<float> source)
{
Vector256<float> clamped = Vector256.Min(Vector256.Max(source, Vector256<float>.Zero), Vector256<float>.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);
}
/// <summary>
/// Clamps vectors to the unit range represented by the pixel format.
/// </summary>
/// <param name="source">The vectors to clamp.</param>
/// <returns>The clamped vectors.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ClampUnit(Vector512<float> source)
{
Vector512<float> clamped = Vector512.Min(Vector512.Max(source, Vector512<float>.Zero), Vector512<float>.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);
}
/// <summary> /// <summary>
/// Associates unassociated vectors with the alpha value binary16 storage can reproduce. /// Associates unassociated vectors with the alpha value binary16 storage can reproduce.
/// </summary> /// </summary>
@ -699,7 +657,7 @@ public partial struct RgbaHalfP
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<float> AssociateForStorage(Vector128<float> source) private static Vector128<float> AssociateForStorage(Vector128<float> source)
{ {
source = ClampUnit(source); source = Numerics.Clamp(source, Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector128<float> result = source * storedAlpha; Vector128<float> result = source * storedAlpha;
@ -714,7 +672,7 @@ public partial struct RgbaHalfP
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> AssociateForStorage(Vector256<float> source) private static Vector256<float> AssociateForStorage(Vector256<float> source)
{ {
source = ClampUnit(source); source = Numerics.Clamp(source, Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector256<float> result = source * storedAlpha; Vector256<float> result = source * storedAlpha;
@ -729,7 +687,7 @@ public partial struct RgbaHalfP
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> AssociateForStorage(Vector512<float> source) private static Vector512<float> AssociateForStorage(Vector512<float> source)
{ {
source = ClampUnit(source); source = Numerics.Clamp(source, Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha); Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(alpha);
Vector512<float> result = source * storedAlpha; Vector512<float> result = source * storedAlpha;
@ -747,11 +705,13 @@ public partial struct RgbaHalfP
{ {
Vector128<float> zero = Vector128<float>.Zero; Vector128<float> zero = Vector128<float>.Zero;
Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11); Vector128<float> alpha = Vector128_.ShuffleNative(source, 0b_11_11_11_11);
Vector128<float> clampedAlpha = ClampUnit(alpha); Vector128<float> clampedAlpha = Numerics.Clamp(alpha, Vector128<float>.Zero, Vector128<float>.One);
Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector128<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector128<float> result = source * (storedAlpha / alpha); Vector128<float> result = source * (storedAlpha / alpha);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -765,11 +725,13 @@ public partial struct RgbaHalfP
{ {
Vector256<float> zero = Vector256<float>.Zero; Vector256<float> zero = Vector256<float>.Zero;
Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11); Vector256<float> alpha = Vector256_.ShuffleNative(source, 0b_11_11_11_11);
Vector256<float> clampedAlpha = ClampUnit(alpha); Vector256<float> clampedAlpha = Numerics.Clamp(alpha, Vector256<float>.Zero, Vector256<float>.One);
Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector256<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector256<float> result = source * (storedAlpha / alpha); Vector256<float> result = source * (storedAlpha / alpha);
result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result); 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); return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
} }
@ -783,12 +745,14 @@ public partial struct RgbaHalfP
{ {
Vector512<float> zero = Vector512<float>.Zero; Vector512<float> zero = Vector512<float>.Zero;
Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11); Vector512<float> alpha = Vector512_.ShuffleNative(source, 0b_11_11_11_11);
Vector512<float> clampedAlpha = ClampUnit(alpha); Vector512<float> clampedAlpha = Numerics.Clamp(alpha, Vector512<float>.Zero, Vector512<float>.One);
Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha); Vector512<float> storedAlpha = HalfTypeHelper.RoundToHalf(clampedAlpha);
Vector512<float> result = source * (storedAlpha / alpha); Vector512<float> result = source * (storedAlpha / alpha);
Vector512<float> alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle(); Vector512<float> 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.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); return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result);
} }
} }

7
src/ImageSharp/PixelFormats/Utils/Vector4Converters.AffineOperators.cs

@ -71,12 +71,7 @@ internal static partial class Vector4Converters
/// <inheritdoc /> /// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Invoke(Vector4 source) public Vector4 Invoke(Vector4 source) => this.Invoke(source.AsVector128()).AsVector4();
{
Vector128<float> result = (source.AsVector128() * this.multiplier.GetLower().GetLower()) + this.offset.GetLower().GetLower();
return result.AsVector4();
}
/// <inheritdoc /> /// <inheritdoc />
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

60
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());
}
}

111
tests/ImageSharp.Tests/Common/StreamExtensionsTests.cs

@ -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<int> 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;
}
}
}

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

@ -34,6 +34,41 @@ public class BmpDecoderTests
{ RLE8, 2835, 2835, PixelResolutionUnit.PixelsPerMeter } { 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<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
else
{
using Image<Rgba32> image = Image.Load<Rgba32>(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] [Theory]
[WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)] [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)]
public void BmpDecoder_CanDecode_MiscellaneousBitmaps<TPixel>(TestImageProvider<TPixel> provider) public void BmpDecoder_CanDecode_MiscellaneousBitmaps<TPixel>(TestImageProvider<TPixel> provider)

248
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
{
/// <summary>
/// Incomplete and oversized blocks are rejected unless image-data recovery is enabled.
/// </summary>
/// <param name="length">The inflated payload length.</param>
/// <param name="integrity">The image-data integrity policy.</param>
[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<InvalidImageContentException>(() => Image.Load<RgbaVector>(options, data));
}
/// <summary>
/// Recovering an invalid image-data block must not expose partially decoded or pooled bytes.
/// </summary>
/// <param name="pixelType">The stored sample type.</param>
/// <param name="length">The inflated payload length.</param>
[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<RgbaVector> image = Image.Load<RgbaVector>(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());
}
}
/// <summary>
/// Missing or truncated zlib headers obey the image-data integrity policy.
/// </summary>
/// <param name="length">The number of available zlib header bytes.</param>
/// <param name="integrity">The image-data integrity policy.</param>
[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<RgbaVector> image = Image.Load<RgbaVector>(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<InvalidImageContentException>(() => Image.Load<RgbaVector>(options, data));
}
}
/// <summary>
/// Missing color channels must not inherit the allocator's previous contents.
/// </summary>
/// <param name="pixelType">The stored sample type.</param>
/// <param name="compression">The ZIP compression code.</param>
/// <param name="yMin">The data window's first row coordinate.</param>
[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<RgbaVector> image = Image.Load<RgbaVector>(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());
}
}
/// <summary>
/// Compresses the predictor bytes for a scanline block.
/// </summary>
/// <param name="data">The predictor bytes.</param>
/// <returns>The zlib stream.</returns>
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();
}
/// <summary>
/// Builds a single-row EXR containing only the red channel.
/// </summary>
/// <param name="compressed">The compressed scanline bytes.</param>
/// <param name="pixelType">The stored sample type.</param>
/// <param name="compression">The ZIP compression code.</param>
/// <param name="yMin">The data window's first row coordinate.</param>
/// <returns>The encoded image.</returns>
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);
}
}

18
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<InvalidImageContentException>(() => Image.Load(data));
}
}

26
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);
}
}

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

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO.Hashing;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -108,6 +109,96 @@ public partial class PngDecoderTests
Assert.Equal("The frame control chunk does not contain enough data!", exception.Message); 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<InvalidImageContentException>(() => Image.Load(payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(payload));
}
/// <summary>
/// Chunk recovery must not replace the header after scanline storage has been sized.
/// </summary>
/// <param name="integrity">The segment integrity policy.</param>
[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<InvalidImageContentException>(() => Image.Load<La16>(options, data));
if (integrity == SegmentIntegrityHandling.Strict)
{
Assert.Throws<InvalidImageContentException>(() => 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);
}
}
/// <summary>
/// Corrupt compressed metadata follows the ancillary policy without preventing valid pixel decoding.
/// </summary>
/// <param name="chunkType">The compressed metadata chunk type.</param>
/// <param name="integrity">The segment integrity policy.</param>
[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<byte> 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<InvalidImageContentException>(() => Image.Load<Rgb24>(options, data));
Assert.IsType<InvalidDataException>(exception.InnerException);
}
else
{
using Image<Rgb24> image = Image.Load<Rgb24>(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 // https://github.com/SixLabors/ImageSharp/issues/3079
[Fact] [Fact]
public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow() public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow()

158
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<InvalidIccProfileException>(() => Image.Load(options, pngData));
}
/// <summary>
/// Three-channel LUT conversion remains supported.
/// </summary>
[Fact]
public void Decode_IccLutWithSupportedChannelCount_ConvertsPixels()
{
byte[] pngData = BuildPng(BuildLut16Profile(3, 3, 2, 2, 2));
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Convert };
using Image<Rgb24> image = Image.Load<Rgb24>(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]);
}
/// <summary>
/// Preserving a profile does not impose the converter's four-component storage limit on the parser.
/// </summary>
/// <param name="outputChannels">The number of output channels in the LUT.</param>
[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<Rgb24> image = Image.Load<Rgb24>(options, pngData);
IccLut16TagDataEntry entry = Assert.IsType<IccLut16TagDataEntry>(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<byte> buffer = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(buffer, value);
stream.Write(buffer);
}
void WriteUInt16(ushort value)
{
Span<byte> 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<Rgb24> image = new(16, 16);
image.Metadata.IccProfile = new IccProfile(profileData);
using MemoryStream stream = new();
image.SaveAsPng(stream);
return stream.ToArray();
}
}

13
tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs

@ -115,4 +115,17 @@ public class BigTiffDecoderTests : TiffDecoderBaseTester
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.StripOffsets));
Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripByteCounts)); 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<InvalidImageContentException>(() => Image.Load(data));
}
} }

33
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
using System.Numerics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -25,6 +26,38 @@ public class TiffDecoderTests : TiffDecoderBaseTester
{ {
public static readonly string[] MultiframeTestImages = Multiframes; public static readonly string[] MultiframeTestImages = Multiframes;
/// <summary>
/// Decoded floating-point components are normalized before they enter half-vector storage.
/// </summary>
/// <param name="hex">The encoded floating-point TIFF.</param>
/// <param name="intensity">The normalized intensity.</param>
[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<HalfVector4> image = Image.Load<HalfVector4>(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] [Theory]
[WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)]
[WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)] [WithFile(Cmyk64BitDeflate, PixelTypes.Rgba32)]

59
tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs

@ -554,6 +554,45 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax);
/// <summary>
/// CCITT row framing must fit even when each row contains only one pixel.
/// </summary>
/// <param name="width">The image width.</param>
[Theory]
[InlineData(1)]
[InlineData(64)]
public void TiffEncoder_EncodeNarrowCcittGroup3Fax_Works(int width)
{
using Image<L8> 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<L8> decoded = Image.Load<L8>(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] [Theory]
[WithFile(Issues2255, PixelTypes.Rgba32)] [WithFile(Issues2255, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WithoutSpecifyingBitPerPixel_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_WithoutSpecifyingBitPerPixel_Works<TPixel>(TestImageProvider<TPixel> provider)
@ -569,6 +608,26 @@ public class TiffEncoderTests : TiffEncoderBaseTester
public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); where TPixel : unmanaged, IPixel<TPixel> => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax);
/// <summary>
/// Re-encoding a one-pixel Group 4 image must retain its pixel and fit the end-of-block code.
/// </summary>
[Fact]
public void TiffEncoder_ReencodeNarrowCcittGroup4Fax_Works()
{
byte[] data = Convert.FromBase64String(
"SUkqAAgAAAAJAAABAwABAAAAAQAAAAEBAwABAAAAAQAAAAIBAwABAAAAAQAAAAMBAwABAAAABAAAAAYBAwABAAAAAAAAABEBBAABAAAA" +
"egAAABUBAwABAAAAAQAAABYBBAABAAAAAQAAABcBBAABAAAABAAAAAAAAACACACA");
using Image<L8> image = Image.Load<L8>(data);
using MemoryStream output = new();
image.Save(output, new TiffEncoder());
output.Position = 0;
using Image<L8> decoded = Image.Load<L8>(output);
Assert.Equal(new Size(1, 1), decoded.Size);
Assert.Equal(image[0, 0], decoded[0, 0]);
}
[Theory] [Theory]
[WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)]
public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works<TPixel>(TestImageProvider<TPixel> provider) public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works<TPixel>(TestImageProvider<TPixel> provider)

66
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -4,9 +4,11 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -22,6 +24,70 @@ public class WebpEncoderTests
{ {
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
/// <summary>
/// Selected EXIF parts are respected whether the lazy profile is installed before or after synchronization.
/// </summary>
/// <param name="reentrant">Whether the stream installs the profile after metadata synchronization.</param>
[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<Rgba32> 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<string> make));
Assert.Equal("POC", make.Value);
Assert.False(decoded.Metadata.ExifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out _));
}
/// <summary>
/// Replaces metadata at the stream capability check, after encoder synchronization has completed.
/// </summary>
private sealed class SwapOnCanSeekStream : MemoryStream
{
private Action callback;
/// <summary>
/// Initializes a stream that invokes the callback on its first capability check.
/// </summary>
/// <param name="callback">The metadata replacement callback.</param>
public SwapOnCanSeekStream(Action callback) => this.callback = callback;
/// <inheritdoc/>
public override bool CanSeek
{
get
{
Action action = this.callback;
this.callback = null;
action?.Invoke();
return base.CanSeek;
}
}
}
[Theory] [Theory]
[WithFile(Lossless.Animated, PixelTypes.Rgba32)] [WithFile(Lossless.Animated, PixelTypes.Rgba32)]
public void Encode_AnimatedLossless<TPixel>(TestImageProvider<TPixel> provider) public void Encode_AnimatedLossless<TPixel>(TestImageProvider<TPixel> provider)

180
tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

@ -1,9 +1,12 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Text;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities;
@ -13,6 +16,35 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp;
[Trait("Format", "Webp")] [Trait("Format", "Webp")]
public class WebpMetaDataTests public class WebpMetaDataTests
{ {
public static IEnumerable<object[]> 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<object[]> 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] [Theory]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)] [WithFile(TestImages.Webp.Lossy.BikeWithExif, PixelTypes.Rgba32, true)]
@ -228,4 +260,152 @@ public class WebpMetaDataTests
using Image<Rgba32> image = Image.Load<Rgba32>(options, stream); using Image<Rgba32> image = Image.Load<Rgba32>(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<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => 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<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => 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<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
else
{
using Image<Rgba32> image = Image.Load<Rgba32>(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<InvalidIccProfileException>(() => Image.Load(options, payload));
Assert.Throws<InvalidIccProfileException>(() => Image.Identify(options, payload));
}
else
{
using Image<Rgba32> image = Image.Load<Rgba32>(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<InvalidImageContentException>(() => Image.Load(options, payload));
Assert.Throws<InvalidImageContentException>(() => Image.Identify(options, payload));
}
/// <summary>
/// Places a metadata declaration around a complete lossless image to test recovery independently of pixel truncation.
/// </summary>
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<Rgba32> source = new(2, 2, new Rgba32(17, 34, 51));
if (animated)
{
header[20] |= 0x02;
using Image<Rgba32> 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;
}
} }

109
tests/ImageSharp.Tests/Helpers/NumericsTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics; using System.Numerics;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.Tests.Helpers; namespace SixLabors.ImageSharp.Tests.Helpers;
@ -305,6 +306,114 @@ public class NumericsTests
(v, m1, m2) => Numerics.Clamp(v, m1, m2)); (v, m1, m2) => Numerics.Clamp(v, m1, m2));
} }
/// <summary>
/// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds.
/// </summary>
/// <param name="min">The lower bound.</param>
/// <param name="max">The upper bound.</param>
[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<float> vector128 = Vector128.LoadUnsafe(ref values[0]);
Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max)));
Vector256<float> vector256 = Vector256.LoadUnsafe(ref values[0]);
Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max)));
Vector512<float> 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);
}
/// <summary>
/// Scalar, SIMD, and span clamps map nonfinite values to the requested bounds.
/// </summary>
/// <param name="min">The lower bound.</param>
/// <param name="max">The upper bound.</param>
[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<double> vector128 = Vector128.LoadUnsafe(ref values[0]);
Assert.Equal(Vector128.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector128, Vector128.Create(min), Vector128.Create(max)));
Vector256<double> vector256 = Vector256.LoadUnsafe(ref values[0]);
Assert.Equal(Vector256.LoadUnsafe(ref expected[0]), Numerics.Clamp(vector256, Vector256.Create(min), Vector256.Create(max)));
Vector512<double> 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);
}
/// <summary>
/// Clamping an in-range zero preserves its sign in scalar and bulk conversions.
/// </summary>
[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<T>( private static void TestClampSpan<T>(
int length, int length,
T min, T min,

4
tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

@ -197,7 +197,7 @@ public class ChunkedMemoryStreamTests
readonlyStream.Position = 0; readonlyStream.Position = 0;
bytArrRet = new byte[(int)readonlyStream.Length]; bytArrRet = new byte[(int)readonlyStream.Length];
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length); readonlyStream.Read(bytArrRet);
for (int i = 0; i < bytArr.Length; i++) for (int i = 0; i < bytArr.Length; i++)
{ {
Assert.Equal(bytArr[i], bytArrRet[i]); Assert.Equal(bytArr[i], bytArrRet[i]);
@ -216,7 +216,7 @@ public class ChunkedMemoryStreamTests
ms2.WriteTo(ms3); ms2.WriteTo(ms3);
ms3.Position = 0; ms3.Position = 0;
bytArrRet = new byte[(int)ms3.Length]; bytArrRet = new byte[(int)ms3.Length];
ms3.Read(bytArrRet, 0, (int)ms3.Length); ms3.Read(bytArrRet);
for (int i = 0; i < bytArr.Length; i++) for (int i = 0; i < bytArr.Length; i++)
{ {
Assert.Equal(bytArr[i], bytArrRet[i]); Assert.Equal(bytArr[i], bytArrRet[i]);

41
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

@ -490,6 +490,47 @@ public class ExifProfileTests
} }
} }
/// <summary>
/// Lazy profile serialization filters selected sections without changing the all-parts passthrough.
/// </summary>
/// <param name="parts">The sections to serialize.</param>
/// <param name="keepMake">Whether the IFD tag should remain.</param>
/// <param name="keepGps">Whether the GPS tag should remain.</param>
[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<string> make));
Assert.Equal(keepGps, result.TryGetValue(ExifTag.GPSLatitudeRef, out IExifValue<string> 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() private static ExifProfile CreateExifProfile()
{ {
ExifProfile profile = new(); ExifProfile profile = new();

30
tests/ImageSharp.Tests/Metadata/Profiles/ICC/DataReader/IccDataReaderLutTests.cs

@ -53,6 +53,36 @@ public class IccDataReaderLutTests
Assert.Equal(expected, output); Assert.Equal(expected, output);
} }
[Fact]
public void ReadClut_WithOversizedDimensions_ThrowsInvalidIccProfileException()
{
byte[] gridPointCount = Enumerable.Repeat((byte)3, 15).ToArray();
Assert.Throws<InvalidIccProfileException>(() => CreateReader(new byte[8]).ReadClut8(15, 15, gridPointCount));
Assert.Throws<InvalidIccProfileException>(() => CreateReader(new byte[8]).ReadClut16(15, 15, gridPointCount));
Assert.Throws<InvalidIccProfileException>(() => CreateReader(new byte[8]).ReadClutF32(15, 15, gridPointCount));
}
/// <summary>
/// A complete profile header and element table do not make absent CLUT values readable.
/// </summary>
[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<InvalidIccProfileException>(() => reader.ReadTagDataEntry(tag));
Assert.Empty(new IccProfile(data).Entries);
}
[Theory] [Theory]
[MemberData(nameof(IccTestDataLut.Lut8TestData), MemberType = typeof(IccTestDataLut))] [MemberData(nameof(IccTestDataLut.Lut8TestData), MemberType = typeof(IccTestDataLut))]
internal void ReadLut8(byte[] data, IccLut expected) internal void ReadLut8(byte[] data, IccLut expected)

23
tests/ImageSharp.Tests/Metadata/Profiles/ICC/IccReaderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestDataIcc; using SixLabors.ImageSharp.Tests.TestDataIcc;
@ -59,4 +60,26 @@ public class IccReaderTests
Assert.Equal(header.Size, expected.Size); Assert.Equal(header.Size, expected.Size);
Assert.Equal(header.Version, expected.Version); 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<byte> 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);
}
} }

183
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
{
/// <summary>
/// HalfSingle normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfSingle_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfSingle>();
/// <summary>
/// HalfVector2 normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfVector2_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector2>();
/// <summary>
/// HalfVector4 normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfVector4_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector4>();
/// <summary>
/// HalfVector4P normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void HalfVector4P_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector4P>();
/// <summary>
/// RgbaVector normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void RgbaVector_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaVector>();
/// <summary>
/// RgbaHalf normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void RgbaHalf_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaHalf>();
/// <summary>
/// RgbaHalfP normalizes scaled input identically in scalar and bulk conversions.
/// </summary>
[Fact]
public void RgbaHalfP_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaHalfP>();
/// <summary>
/// Raw half storage preserves IEEE special values while its scaled representation remains finite.
/// </summary>
[Fact]
public void HalfVector4_NativeSpecialValuesHaveNormalizedScaledOutput() => AssertNativeSpecialValuesHaveNormalizedScaledOutput();
/// <summary>
/// Associated half-vector conversion uses the stored alpha ratio before normalizing RGB.
/// </summary>
[Fact]
public void HalfVector4P_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized<HalfVector4P>();
/// <summary>
/// Associated half-RGBA conversion uses the stored alpha ratio before normalizing RGB.
/// </summary>
[Fact]
public void RgbaHalfP_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized<RgbaHalfP>();
/// <summary>
/// Checks saturation and NaN handling without deriving expectations from the invalid-input path.
/// </summary>
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
private static void AssertScaledInputIsNormalized<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
Assert.Equal(expected, actual);
}
/// <summary>
/// Checks associated input against finite control values with the same represented color.
/// </summary>
/// <typeparam name="TPixel">The associated destination pixel format.</typeparam>
private static void AssertAssociatedScaledInputIsNormalized<TPixel>()
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply);
Assert.Equal(expected, actual);
}
/// <summary>
/// Checks native storage and every scaled output lane independently of integer conversion semantics.
/// </summary>
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<HalfVector4>.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<HalfVector4>.Instance.ToVector4(Configuration.Default, source, actual, PixelConversionModifiers.Scale);
Assert.All(actual, value => Assert.Equal(expected, value));
}
}

1
tests/ImageSharp.Tests/TestImages.cs

@ -57,6 +57,7 @@ public static class TestImages
public const string LowColorVariance = "Png/low-variance.png"; public const string LowColorVariance = "Png/low-variance.png";
public const string PngWithMetadata = "Png/PngWithMetaData.png"; public const string PngWithMetadata = "Png/PngWithMetaData.png";
public const string InvalidTextData = "Png/InvalidTextData.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 David = "Png/david.png";
public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png";
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";

3
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
Loading…
Cancel
Save