diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
index 82d475e578..b33d7dd449 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs
@@ -42,6 +42,10 @@ public ClutCalculator(IccClut clut)
Guard.NotNull(clut, nameof(clut));
Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount));
Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount));
+ if (clut.InputChannelCount > 4 || clut.OutputChannelCount > 4)
+ {
+ throw new InvalidIccProfileException("ICC conversion supports at most four input and output channels.");
+ }
this.inputCount = clut.InputChannelCount;
this.outputCount = clut.OutputChannelCount;
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
index c97578ee3f..08f957bbe5 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs
@@ -59,6 +59,11 @@ private static Vector4 CalculateLut(LutCalculator[] lut, Vector4 value)
private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix)
{
+ if (inputCurve.Length > 4 || outputCurve.Length > 4)
+ {
+ throw new InvalidIccProfileException("ICC conversion supports at most four input and output channels.");
+ }
+
this.inputCurve = InitLut(inputCurve);
this.outputCurve = InitLut(outputCurve);
this.clutCalculator = new ClutCalculator(clut);
diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
index d2fc5d9b55..029be68c51 100644
--- a/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
+++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs
@@ -15,6 +15,10 @@ internal class TrcCalculator : IVector4Calculator
public TrcCalculator(IccTagDataEntry[] entries, bool inverted)
{
Guard.NotNull(entries, nameof(entries));
+ if (entries.Length > 4)
+ {
+ throw new InvalidIccProfileException("ICC conversion supports at most four tone response curves.");
+ }
this.calculators = new ISingleCalculator[entries.Length];
for (int i = 0; i < entries.Length; i++)
diff --git a/src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs b/src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs
new file mode 100644
index 0000000000..e90aba7138
--- /dev/null
+++ b/src/ImageSharp/Common/Extensions/BufferedReadStreamExtensions.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.IO;
+
+namespace SixLabors.ImageSharp;
+
+///
+/// Extension methods for the type.
+///
+internal static class BufferedReadStreamExtensions
+{
+ ///
+ /// Determines whether the complete read range is contained in the stream.
+ ///
+ /// The stream containing the data.
+ /// The absolute start of the range.
+ /// The number of bytes in the range.
+ /// Whether the range is contained in the stream.
+ public static bool IsReadRangeValid(this BufferedReadStream stream, long offset, ulong length)
+ {
+ // Compare the offset first so subtraction cannot underflow, and avoid
+ // adding an untrusted length to the offset where it could wrap around.
+ ulong streamLength = (ulong)stream.Length;
+ return (ulong)offset <= streamLength && length <= streamLength - (ulong)offset;
+ }
+
+ ///
+ /// Gets a buffer length when the complete read fits in both the stream and an integer-sized buffer.
+ ///
+ /// The stream containing the data.
+ /// The declared length in bytes.
+ /// The validated length, or zero when the range is invalid.
+ /// Whether the complete read is valid.
+ public static bool TryGetReadLength(this BufferedReadStream stream, ulong length, out int bufferLength)
+ {
+ if (length > int.MaxValue || !stream.IsReadRangeValid(stream.Position, length))
+ {
+ bufferLength = 0;
+ return false;
+ }
+
+ bufferLength = (int)length;
+ return true;
+ }
+
+ ///
+ /// Reads data from the stream into a slice of the provided buffer.
+ ///
+ /// The stream.
+ /// The buffer.
+ /// The offset within the buffer where bytes are read into.
+ /// The number of bytes, if available, to read.
+ /// The actual number of bytes read.
+ public static int Read(this BufferedReadStream stream, Span buffer, int offset, int count)
+ => stream.Read(buffer.Slice(offset, count));
+
+ ///
+ /// Advances the stream by the specified number of bytes. Nonpositive counts are ignored.
+ ///
+ /// The stream.
+ /// The number of bytes to skip.
+ public static void Skip(this BufferedReadStream stream, int count)
+ {
+ if (count > 0)
+ {
+ // BufferedReadStream is always seekable; its position setter preserves
+ // buffered data when the destination is inside the current buffer.
+ stream.Position += count;
+ }
+ }
+}
diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
index 7ed3348240..13761d4d2a 100644
--- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs
+++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Buffers;
-
namespace SixLabors.ImageSharp;
///
@@ -19,53 +17,4 @@ internal static class StreamExtensions
/// The number of bytes to write to the stream.
public static void Write(this Stream stream, Span buffer, int offset, int count)
=> stream.Write(buffer.Slice(offset, count));
-
- ///
- /// Reads data from a stream into the provided buffer.
- ///
- /// The stream.
- /// The buffer.
- /// The offset within the buffer where the bytes are read into.
- /// The number of bytes, if available, to read.
- /// The actual number of bytes read.
- public static int Read(this Stream stream, Span buffer, int offset, int count)
- => stream.Read(buffer.Slice(offset, count));
-
- ///
- /// Skips the number of bytes in the given stream.
- ///
- /// The stream.
- /// A byte offset relative to the origin parameter.
- public static void Skip(this Stream stream, int count)
- {
- if (count < 1)
- {
- return;
- }
-
- if (stream.CanSeek)
- {
- stream.Seek(count, SeekOrigin.Current);
- return;
- }
-
- byte[] buffer = ArrayPool.Shared.Rent(count);
- try
- {
- while (count > 0)
- {
- int bytesRead = stream.Read(buffer, 0, count);
- if (bytesRead == 0)
- {
- break;
- }
-
- count -= bytesRead;
- }
- }
- finally
- {
- ArrayPool.Shared.Return(buffer);
- }
- }
}
diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
index 0b88aa5b07..8e99728314 100644
--- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs
+++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
@@ -26,8 +26,7 @@ internal static class ColorNumerics
/// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images).
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int GetBT709Luminance(Vector4 vector, int luminanceLevels)
- => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
+ public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
///
/// Gets the luminance from the rgb components using the formula
diff --git a/src/ImageSharp/Common/Helpers/Numerics.cs b/src/ImageSharp/Common/Helpers/Numerics.cs
index 8980d2b53e..4154cbf1f7 100644
--- a/src/ImageSharp/Common/Helpers/Numerics.cs
+++ b/src/ImageSharp/Common/Helpers/Numerics.cs
@@ -263,63 +263,95 @@ public static int Clamp(int value, int min, int max)
}
///
- /// Returns the value clamped to the inclusive range of min and max.
+ /// Returns the value clamped to the inclusive range of min and max, mapping NaN to min.
///
/// The value to clamp.
/// The minimum inclusive value.
/// The maximum inclusive value.
/// The clamped .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static float Clamp(float value, float min, float max)
- {
- if (value > max)
- {
- return max;
- }
-
- if (value < min)
- {
- return min;
- }
-
- return value;
- }
+ public static float Clamp(float value, float min, float max) => Clamp(value, min, max);
///
- /// Returns the value clamped to the inclusive range of min and max.
+ /// Returns the value clamped to the inclusive range of min and max, mapping NaN to min.
///
/// The value to clamp.
/// The minimum inclusive value.
/// The maximum inclusive value.
/// The clamped .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static double Clamp(double value, double min, double max)
+ public static double Clamp(double value, double min, double max) => Clamp(value, min, max);
+
+ ///
+ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
+ ///
+ /// The components to clamp.
+ /// The inclusive lower bounds.
+ /// The inclusive upper bounds.
+ /// The clamped components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector2 Clamp(Vector2 value, Vector2 min, Vector2 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector2();
+
+ ///
+ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
+ ///
+ /// The component type.
+ /// The components to clamp.
+ /// The inclusive lower bounds.
+ /// The inclusive upper bounds.
+ /// The clamped components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 Clamp(Vector128 value, Vector128 min, Vector128 max)
+ where T : struct, INumber
{
- if (value > max)
- {
- return max;
- }
+ // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
+ Vector128 lowerClamped = Vector128.ConditionalSelect(Vector128.GreaterThanOrEqual(value, min), value, min);
+ return Vector128.ConditionalSelect(Vector128.GreaterThan(value, max), max, lowerClamped);
+ }
- if (value < min)
- {
- return min;
- }
+ ///
+ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
+ ///
+ /// The component type.
+ /// The components to clamp.
+ /// The inclusive lower bounds.
+ /// The inclusive upper bounds.
+ /// The clamped components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 Clamp(Vector256 value, Vector256 min, Vector256 max)
+ where T : struct, INumber
+ {
+ // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
+ Vector256 lowerClamped = Vector256.ConditionalSelect(Vector256.GreaterThanOrEqual(value, min), value, min);
+ return Vector256.ConditionalSelect(Vector256.GreaterThan(value, max), max, lowerClamped);
+ }
- return value;
+ ///
+ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
+ ///
+ /// The component type.
+ /// The components to clamp.
+ /// The inclusive lower bounds.
+ /// The inclusive upper bounds.
+ /// The clamped components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 Clamp(Vector512 value, Vector512 min, Vector512 max)
+ where T : struct, INumber
+ {
+ // Ordered comparisons map NaN to min and preserve in-range signed zero on every runtime.
+ Vector512 lowerClamped = Vector512.ConditionalSelect(Vector512.GreaterThanOrEqual(value, min), value, min);
+ return Vector512.ConditionalSelect(Vector512.GreaterThan(value, max), max, lowerClamped);
}
///
- /// Returns the value clamped to the inclusive range of min and max.
- /// 5x Faster than
- /// on platforms < NET 5.
+ /// Clamps components to the inclusive range of min and max, mapping NaN to min.
///
/// The value to clamp.
/// The minimum inclusive value.
/// The maximum inclusive value.
/// The clamped .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max)
- => Vector4.Min(Vector4.Max(value, min), max);
+ public static Vector4 Clamp(Vector4 value, Vector4 min, Vector4 max) => Clamp(value.AsVector128(), min.AsVector128(), max.AsVector128()).AsVector4();
///
/// Clamps the span values to the inclusive range of min and max.
@@ -352,24 +384,24 @@ public static void Clamp(Span span, int min, int max)
=> TensorPrimitives_.Clamp(span, min, max, span);
///
- /// Clamps the span values to the inclusive range of min and max.
+ /// Clamps the span values to the inclusive range of min and max, mapping NaN to min.
///
/// The span containing the values to clamp.
/// The minimum inclusive value.
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, float min, float max)
- => TensorPrimitives_.Clamp(span, min, max, span);
+ => Clamp(span, min, max);
///
- /// Clamps the span values to the inclusive range of min and max.
+ /// Clamps the span values to the inclusive range of min and max, mapping NaN to min.
///
/// The span containing the values to clamp.
/// The minimum inclusive value.
/// The maximum inclusive value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(Span span, double min, double max)
- => TensorPrimitives_.Clamp(span, min, max, span);
+ => Clamp(span, min, max);
///
/// Pre-multiplies the "x", "y", "z" components of a vector by its "w" component leaving the "w" component intact.
@@ -392,7 +424,7 @@ public static void Premultiply(ref Vector4 source)
public static void ClampRgbToAlpha(ref Vector4 source)
{
Vector4 alpha = PermuteW(source);
- source = WithW(Vector4.Min(Vector4.Max(source, Vector4.Zero), alpha), alpha);
+ source = WithW(Clamp(source, Vector4.Zero, alpha), alpha);
}
///
@@ -1071,4 +1103,78 @@ public static nuint Vector512Count(int length)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Normalize(Span span, float sum)
=> TensorPrimitives_.Divide(span, sum, span);
+
+ ///
+ /// Clamps a floating-point component while mapping NaN to the lower bound.
+ ///
+ /// The component type.
+ /// The component to clamp.
+ /// The inclusive lower bound.
+ /// The inclusive upper bound.
+ /// The clamped component.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static T Clamp(T value, T min, T max)
+ where T : struct, INumber
+ {
+ // Ordered comparisons map NaN to min; in-range values retain their original bits, including signed zero.
+ return value > max ? max : value >= min ? value : min;
+ }
+
+ ///
+ /// Applies the scalar clamp contract to floating-point spans in place.
+ ///
+ /// The component type.
+ /// The components to clamp.
+ /// The inclusive lower bound.
+ /// The inclusive upper bound.
+ private static void Clamp(Span span, T min, T max)
+ where T : struct, INumber
+ {
+ ref T start = ref MemoryMarshal.GetReference(span);
+ int i = 0;
+
+ // Each register uses the same Clamp overload as individual vector callers. Descending widths consume
+ // the remainder without overlapping stores, and the final components use the scalar overload.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ Vector512 lower = Vector512.Create(min);
+ Vector512 upper = Vector512.Create(max);
+
+ for (; i <= span.Length - Vector512.Count; i += Vector512.Count)
+ {
+ Vector512 value = Vector512.LoadUnsafe(ref start, (nuint)i);
+ Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ Vector256 lower = Vector256.Create(min);
+ Vector256 upper = Vector256.Create(max);
+
+ for (; i <= span.Length - Vector256.Count; i += Vector256.Count)
+ {
+ Vector256 value = Vector256.LoadUnsafe(ref start, (nuint)i);
+ Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
+ }
+ }
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ Vector128 lower = Vector128.Create(min);
+ Vector128 upper = Vector128.Create(max);
+
+ for (; i <= span.Length - Vector128.Count; i += Vector128.Count)
+ {
+ Vector128 value = Vector128.LoadUnsafe(ref start, (nuint)i);
+ Clamp(value, lower, upper).StoreUnsafe(ref start, (nuint)i);
+ }
+ }
+
+ for (; i < span.Length; i++)
+ {
+ ref T value = ref Unsafe.Add(ref start, (uint)i);
+ value = Clamp(value, min, max);
+ }
+ }
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index aba1243d77..95f02503e9 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -1429,7 +1429,7 @@ private void ReadInfoHeader(BufferedReadStream stream)
// > 108 bytes
infoHeaderType = BmpInfoHeaderType.WinVersion5;
this.infoHeader = BmpInfoHeader.ParseV5(buffer);
- if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
+ if (!this.Options.SkipMetadata && this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{
long streamPosition = stream.Position;
this.ExecuteAncillarySegmentAction(() => this.ReadIccProfile(stream, this.metadata, infoHeaderStart));
@@ -1474,8 +1474,16 @@ private void ReadInfoHeader(BufferedReadStream stream)
/// The stream position where the info header begins.
private void ReadIccProfile(BufferedReadStream stream, ImageMetadata imageMetadata, long infoHeaderStart)
{
+ long profileStart = infoHeaderStart + this.infoHeader.ProfileData;
+ if (this.infoHeader.ProfileData < 0 ||
+ this.infoHeader.ProfileSize <= 0 ||
+ !stream.IsReadRangeValid(profileStart, (uint)this.infoHeader.ProfileSize))
+ {
+ BmpThrowHelper.ThrowInvalidImageContentException("Not enough data to read BMP ICC profile.");
+ }
+
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
- stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
+ stream.Position = profileStart;
if (stream.Read(iccProfileData) != iccProfileData.Length)
{
@@ -1560,6 +1568,11 @@ private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out b
this.infoHeader.Height = -this.infoHeader.Height;
}
+ if (this.infoHeader.Width <= 0 || this.infoHeader.Height <= 0)
+ {
+ BmpThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0.");
+ }
+
int bytesPerColorMapEntry = 4;
int colorMapSizeBytes = -1;
if (this.infoHeader.ClrUsed == 0)
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
index 2b5b740569..e58eca4e98 100644
--- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
@@ -39,7 +39,7 @@ public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint byt
}
///
- public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer)
{
Span outputBuffer = MemoryMarshal.Cast(buffer);
Span decompressed = this.tmpBuffer.GetSpan();
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
index 19edb31afe..b1a2094dab 100644
--- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
@@ -25,10 +25,10 @@ public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint by
}
///
- public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer)
{
- int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock));
- if (bytesRead != (int)this.BytesPerBlock)
+ int bytesRead = stream.Read(buffer[..(int)uncompressedBytes]);
+ if (bytesRead != uncompressedBytes)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!");
}
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs
index f45b660e7d..d621c893e5 100644
--- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs
@@ -39,19 +39,20 @@ public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint byte
}
///
- public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer)
{
- Span uncompressed = this.tmpBuffer.GetSpan();
+ uint rowCount = uncompressedBytes / this.BytesPerRow;
+ uint packedBytes = this.pixelType == ExrPixelType.Float ? (uncompressedBytes / 4) * 3 : uncompressedBytes;
+ Span uncompressed = this.tmpBuffer.GetSpan()[..(int)packedBytes];
Span outputBufferHalf = MemoryMarshal.Cast(buffer);
Span outputBufferFloat = MemoryMarshal.Cast(buffer);
Span outputBufferUint = MemoryMarshal.Cast(buffer);
- uint uncompressedBytes = this.BytesPerBlock;
- UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
+ UndoZipCompression(stream, compressedBytes, uncompressed, packedBytes);
int lastIn = 0;
int outputOffset = 0;
- for (int y = 0; y < this.RowsPerBlock; y++)
+ for (uint y = 0; y < rowCount; y++)
{
for (int c = 0; c < this.channelCount; c++)
{
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
index f548a81810..5ffe83af3c 100644
--- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
@@ -26,7 +26,7 @@ public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, ui
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock);
///
- public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer)
{
Span uncompressed = this.tmpBuffer.GetSpan();
int maxLength = (int)this.BytesPerBlock;
diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
index 8bab76f402..41fda54366 100644
--- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
+++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
@@ -26,15 +26,14 @@ public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint byt
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock);
///
- public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer)
+ public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer)
{
- Span uncompressed = this.tmpBuffer.GetSpan();
+ Span uncompressed = this.tmpBuffer.GetSpan()[..(int)uncompressedBytes];
- uint uncompressedBytes = (uint)buffer.Length;
int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
- Reconstruct(uncompressed, (uint)totalRead);
- Interleave(uncompressed, (uint)totalRead, buffer);
+ Reconstruct(uncompressed, uncompressedBytes);
+ Interleave(uncompressed, uncompressedBytes, buffer);
}
///
diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
index f598955f43..b85c37546e 100644
--- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
+++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
@@ -31,8 +31,9 @@ protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uin
///
/// The buffered stream to decompress.
/// The compressed bytes.
+ /// The expected byte count for the current block.
/// The buffer to write the decompressed data to.
- public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer);
+ public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer);
///
/// Decompresses zip compressed data.
@@ -52,13 +53,19 @@ protected static int UndoZipCompression(BufferedReadStream stream, uint compress
int left = (int)(compressedBytes - (stream.Position - pos));
return left > 0 ? left : 0;
});
- inflateStream.AllocateNewBytes((int)compressedBytes, true);
- using DeflateStream dataStream = inflateStream.CompressedStream!;
+
+ // Incomplete headers return false even for critical chunks, leaving no stream to read.
+ if (!inflateStream.AllocateNewBytes((int)compressedBytes, true))
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("ZIP compressed EXR block has an incomplete zlib header.");
+ }
+
+ using DeflateStream dataStream = inflateStream.CompressedStream;
int totalRead = 0;
while (totalRead < uncompressedBytes)
{
- int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead);
+ int bytesRead = dataStream.Read(uncompressed.Slice(totalRead, (int)uncompressedBytes - totalRead));
if (bytesRead <= 0)
{
break;
@@ -67,9 +74,9 @@ protected static int UndoZipCompression(BufferedReadStream stream, uint compress
totalRead += bytesRead;
}
- if (totalRead == 0)
+ if (totalRead != uncompressedBytes || dataStream.ReadByte() != -1)
{
- ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!");
+ ExrThrowHelper.ThrowInvalidImageContentException("ZIP compressed EXR block has an invalid decompressed length.");
}
return totalRead;
diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
index 680e8d333d..4c9924ec3b 100644
--- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
+++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
@@ -166,7 +166,8 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf
int height = this.Height;
int channelCount = this.Channels.Count;
- using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4);
+ // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row.
+ using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean);
using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock);
Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
Span redPixelData = rowBuffer.GetSpan()[..width];
@@ -192,10 +193,19 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf
this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
- uint rowStartIndex = this.ReadUnsignedInteger(stream);
+
+ // Chunk coordinates are signed and absolute; pixel rows are relative to the data window.
+ uint rowStartIndex = (uint)((long)this.ReadSignedInteger(stream) - this.HeaderAttributes.DataWindow.YMin);
+ if (rowStartIndex >= height)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk row index is outside the data window.");
+ }
uint compressedBytesCount = this.ReadUnsignedInteger(stream);
- decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData);
+ uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex);
+ uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock);
+
+ this.DecompressBlock(decompressor, stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData);
int offset = 0;
for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++)
@@ -247,7 +257,8 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe
int height = this.Height;
int channelCount = this.Channels.Count;
- using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4);
+ // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row.
+ using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean);
using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock);
Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan();
Span redPixelData = rowBuffer.GetSpan()[..width];
@@ -273,10 +284,19 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe
this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
- uint rowStartIndex = this.ReadUnsignedInteger(stream);
+
+ // Chunk coordinates are signed and absolute; pixel rows are relative to the data window.
+ uint rowStartIndex = (uint)((long)this.ReadSignedInteger(stream) - this.HeaderAttributes.DataWindow.YMin);
+ if (rowStartIndex >= height)
+ {
+ ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk row index is outside the data window.");
+ }
uint compressedBytesCount = this.ReadUnsignedInteger(stream);
- decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData);
+ uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex);
+ uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock);
+
+ this.DecompressBlock(decompressor, stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData);
int offset = 0;
for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++)
@@ -305,6 +325,28 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe
}
}
+ ///
+ /// Decompresses a block according to the configured image-data integrity policy.
+ ///
+ /// The decompressor for the stored compression type.
+ /// The encoded block stream.
+ /// The declared compressed byte count.
+ /// The expected byte count for the rows in this block.
+ /// The reusable decompressed pixel buffer.
+ private void DecompressBlock(ExrBaseDecompressor decompressor, BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer)
+ {
+ try
+ {
+ decompressor.Decompress(stream, compressedBytes, uncompressedBytes, buffer);
+ }
+ catch (Exception ex) when (this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.IgnoreImageData && ex is InvalidImageContentException or InvalidDataException)
+ {
+ // The offset table locates the next block independently of this damaged payload.
+ // Discard the entire failed block so partial output or pooled bytes cannot become pixels.
+ buffer[..(int)uncompressedBytes].Clear();
+ }
+ }
+
///
/// Reads float image channel data.
///
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index e4ffd42823..0a06f4915b 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -261,11 +261,6 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok
this.currentLocalColorTable?.Dispose();
}
- if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
- {
- GifThrowHelper.ThrowNoHeader();
- }
-
// Ignoring a malformed ancillary extension must not let identify succeed for a file
// that never contained any readable image frame data.
if (previousFrame is null)
@@ -328,6 +323,10 @@ private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
}
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer);
+ if (this.logicalScreenDescriptor.Width == 0 || this.logicalScreenDescriptor.Height == 0)
+ {
+ GifThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0.");
+ }
}
///
diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
index 85c4959a98..aa3df4a4fd 100644
--- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
+++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
@@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Gif;
/// The stream to read from.
/// The memory allocator.
/// The XMP metadata
- public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator)
+ public static GifXmpApplicationExtension Read(BufferedReadStream stream, MemoryAllocator allocator)
{
byte[] xmpBytes = ReadXmpData(stream, allocator, out bool terminated);
if (!terminated)
@@ -75,7 +75,7 @@ public int WriteTo(Span buffer)
return this.ContentLength;
}
- private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator, out bool terminated)
+ private static byte[] ReadXmpData(BufferedReadStream stream, MemoryAllocator allocator, out bool terminated)
{
using ChunkedMemoryStream bytes = new(allocator);
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index ae26914e8e..05cdd9cc77 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -274,10 +274,9 @@ public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder)
// Get the marker length.
int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2;
- // Check whether the stream actually has enough bytes to read
- // markerContentByteSize is always positive so we cast
- // to uint to avoid sign extension
- if (stream.RemainingBytes < (uint)markerContentByteSize)
+ // Validate the entire segment before parsing it. Casting directly
+ // to ulong also rejects lengths smaller than the two-byte length field.
+ if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize))
{
JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker);
}
@@ -351,10 +350,9 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC
// Get the marker length.
int markerContentByteSize = ReadUint16(stream, markerBuffer) - 2;
- // Check whether stream actually has enough bytes to read
- // markerContentByteSize is always positive so we cast
- // to uint to avoid sign extension.
- if (stream.RemainingBytes < (uint)markerContentByteSize)
+ // Validate the entire segment before parsing it. Casting directly
+ // to ulong also rejects lengths smaller than the two-byte length field.
+ if (!stream.IsReadRangeValid(stream.Position, (ulong)markerContentByteSize))
{
if (metadataOnly && this.Metadata != null && this.Frame != null)
{
@@ -841,7 +839,7 @@ private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remai
// TODO: thumbnail
if (remaining > 0)
{
- if (stream.Position + remaining >= stream.Length)
+ if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1))
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App0 Marker length.");
stream.Skip(remaining);
@@ -877,7 +875,7 @@ private void ProcessApp1Marker(BufferedReadStream stream, int remaining)
return;
}
- if (stream.Position + remaining >= stream.Length)
+ if (!stream.IsReadRangeValid(stream.Position, (ulong)remaining + 1))
{
this.ThrowOrIgnoreNonStrictSegmentError("Bad App1 Marker length.");
stream.Skip(remaining);
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 28f9a989b9..5e5225cdd0 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -195,11 +195,6 @@ protected override Image Decode(BufferedReadStream stream, Cance
switch (chunk.Type)
{
case PngChunkType.Header:
- if (!Equals(this.header, default(PngHeader)))
- {
- PngThrowHelper.ThrowInvalidHeader();
- }
-
this.ReadHeaderChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.AnimationControl:
@@ -656,7 +651,7 @@ private void InitializeImage(ImageMetadata metadata, FrameControl frameC
frameMetadata.FromChunk(in frameControl);
this.bytesPerPixel = this.CalculateBytesPerPixel();
- this.bytesPerScanline = this.CalculateScanlineLength(this.header.Width) + 1;
+ this.bytesPerScanline = CalculateScanlineLength(this.header.Width, this.header.BitDepth, this.bytesPerPixel) + 1;
this.bytesPerSample = 1;
if (this.header.BitDepth >= 8)
{
@@ -741,21 +736,29 @@ private int CalculateBytesPerPixel()
/// Calculates the scanline length.
///
/// The width of the row.
+ /// The number of bits per sample.
+ /// The number of bytes per pixel.
///
/// The representing the length.
///
- private int CalculateScanlineLength(int width)
+ internal static int CalculateScanlineLength(int width, int bitDepth, int bytesPerPixel)
{
- int mod = this.header.BitDepth == 16 ? 16 : 8;
- int scanlineLength = width * this.header.BitDepth * this.bytesPerPixel;
+ int mod = bitDepth == 16 ? 16 : 8;
+ long scanlineLength = (long)width * bitDepth * bytesPerPixel;
- int amount = scanlineLength % mod;
+ long amount = scanlineLength % mod;
if (amount != 0)
{
scanlineLength += mod - amount;
}
- return scanlineLength / mod;
+ scanlineLength /= mod;
+ if (scanlineLength >= int.MaxValue)
+ {
+ PngThrowHelper.ThrowInvalidImageContentException("PNG scanline length exceeds the supported maximum.");
+ }
+
+ return (int)scanlineLength;
}
///
@@ -875,13 +878,13 @@ private void DecodePixelDataCore(
while (currentRow < height)
{
cancellationToken.ThrowIfCancellationRequested();
- int bytesPerFrameScanline = this.CalculateScanlineLength((int)frameControl.Width) + 1;
+ int bytesPerFrameScanline = CalculateScanlineLength((int)frameControl.Width, this.header.BitDepth, this.bytesPerPixel) + 1;
Span scanSpan = this.scanline.GetSpan()[..bytesPerFrameScanline];
Span prevSpan = this.previousScanline.GetSpan()[..bytesPerFrameScanline];
while (currentRowBytesRead < bytesPerFrameScanline)
{
- int bytesRead = compressedStream.Read(scanSpan, currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead);
+ int bytesRead = compressedStream.Read(scanSpan.Slice(currentRowBytesRead, bytesPerFrameScanline - currentRowBytesRead));
if (bytesRead <= 0)
{
goto EXIT;
@@ -1006,14 +1009,14 @@ private void DecodeInterlacedPixelDataCore(
continue;
}
- int bytesPerInterlaceScanline = this.CalculateScanlineLength(numColumns) + 1;
+ int bytesPerInterlaceScanline = CalculateScanlineLength(numColumns, this.header.BitDepth, this.bytesPerPixel) + 1;
while (currentRow < endRow)
{
cancellationToken.ThrowIfCancellationRequested();
while (currentRowBytesRead < bytesPerInterlaceScanline)
{
- int bytesRead = compressedStream.Read(this.scanline.GetSpan(), currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead);
+ int bytesRead = compressedStream.Read(this.scanline.GetSpan().Slice(currentRowBytesRead, bytesPerInterlaceScanline - currentRowBytesRead));
if (bytesRead <= 0)
{
goto EXIT;
@@ -1439,6 +1442,11 @@ private FrameControl ReadFrameControlChunk(ReadOnlySpan data)
/// The containing data.
private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data)
{
+ if (!Equals(this.header, default(PngHeader)))
+ {
+ PngThrowHelper.ThrowInvalidHeader();
+ }
+
this.header = PngHeader.Parse(data);
this.header.Validate();
@@ -1976,21 +1984,36 @@ private unsafe bool TryDecompressZlibData(ReadOnlySpan compressedData, int
return false;
}
- int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
- while (bytesRead != 0)
+ try
{
- if (memoryStreamOutput.Length > maxLength)
+ int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
+ while (bytesRead != 0)
{
- uncompressedBytesArray = [];
- return false;
+ if (memoryStreamOutput.Length > maxLength)
+ {
+ uncompressedBytesArray = [];
+ return false;
+ }
+
+ memoryStreamOutput.Write(destUncompressedData[..bytesRead]);
+ bytesRead = inflateStream.CompressedStream.Read(destUncompressedData);
}
- memoryStreamOutput.Write(destUncompressedData[..bytesRead]);
- bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
+ uncompressedBytesArray = memoryStreamOutput.ToArray();
+ return true;
}
+ catch (InvalidDataException ex)
+ {
+ // ICC and text chunks are already bounded in memory, so rejecting their compressed contents
+ // does not lose the next chunk boundary. Apply the ancillary policy without keeping partial output.
+ if (this.Options.SegmentIntegrityHandling == SegmentIntegrityHandling.Strict)
+ {
+ throw new InvalidImageContentException("Invalid compressed PNG metadata.", ex);
+ }
- uncompressedBytesArray = memoryStreamOutput.ToArray();
- return true;
+ uncompressedBytesArray = [];
+ return false;
+ }
}
}
diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
index ead157986a..86f80530fb 100644
--- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
@@ -72,9 +72,9 @@ protected override Image Decode(BufferedReadStream stream, Cance
TgaThrowHelper.ThrowNotSupportedException($"Unknown tga colormap type {this.fileHeader.ColorMapType} found");
}
- if (this.fileHeader.Width == 0 || this.fileHeader.Height == 0)
+ if (this.fileHeader.Width <= 0 || this.fileHeader.Height <= 0)
{
- throw new UnknownImageFormatException("Width or height cannot be 0");
+ TgaThrowHelper.ThrowInvalidImageContentException("Width and height must be greater than 0.");
}
Image image = new(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata);
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
index dc9e1e7296..74914e036f 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
@@ -126,6 +126,14 @@ protected override void CompressStrip(Span pixelsAsGray, int height, Span<
}
}
+ ///
+ protected override long GetMaximumEncodedBits(int rowsPerStrip)
+ {
+ // A pixel can require a 13-bit terminating code. Each row can also require
+ // an 8-bit zero-length white run and a 12-bit EOL, plus the initial EOL.
+ return 12L + ((((long)this.Width * 13) + 20) * rowsPerStrip);
+ }
+
private void WriteEndOfLine(Span compressedData)
{
if (this.useModifiedHuffman)
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs
index cffc96fcdf..6dba8b7d25 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs
@@ -131,6 +131,14 @@ protected override void CompressStrip(Span pixelsAsGray, int height, Span<
this.WriteCode(12, 1, compressedData);
}
+ ///
+ protected override long GetMaximumEncodedBits(int rowsPerStrip)
+ {
+ // Alternating pixels use at most 29 bits per two-pixel horizontal mode.
+ // Allow 16 bits per pixel, row transition overhead, and the final 24-bit EOFB.
+ return ((((long)this.Width * 16) + 24) * rowsPerStrip) + 24;
+ }
+
///
protected override void Dispose(bool disposing)
{
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs
index 8e2227cba5..06cf84437a 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs
@@ -465,6 +465,12 @@ protected void PadByte()
/// The destination buffer to write the code to.
protected void WriteCode(uint codeLength, uint code, Span compressedData)
{
+ long availableBits = (((long)compressedData.Length - this.bytePosition) * 8) - this.bitPosition;
+ if (codeLength > availableBits)
+ {
+ throw new InvalidMemoryOperationException("The CCITT output buffer is too small for the encoded data.");
+ }
+
while (codeLength > 0)
{
int bitNumber = (int)codeLength;
@@ -526,8 +532,20 @@ public override void CompressStrip(Span rows, int height)
///
public override void Initialize(int rowsPerStrip)
{
- // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good.
- int maxNeededBytes = this.Width * rowsPerStrip;
- this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes);
+ long maxNeededBits = this.GetMaximumEncodedBits(rowsPerStrip);
+ ulong maxNeededBytes = (ulong)((maxNeededBits + 7) / 8);
+ if (maxNeededBytes > int.MaxValue)
+ {
+ InvalidMemoryOperationException.ThrowAllocationOverLimitException(maxNeededBytes, int.MaxValue);
+ }
+
+ this.compressedDataBuffer = this.Allocator.Allocate((int)maxNeededBytes);
}
+
+ ///
+ /// Gets an upper bound for the encoded strip length in bits.
+ ///
+ /// The number of rows in the strip.
+ /// The maximum encoded length.
+ protected abstract long GetMaximumEncodedBits(int rowsPerStrip);
}
diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
index d3b65c537c..a09a39b52b 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/DeflateTiffCompression.cs
@@ -69,7 +69,7 @@ protected override void Decompress(BufferedReadStream stream, int byteCount, int
int totalRead = 0;
while (totalRead < buffer.Length)
{
- int bytesRead = dataStream.Read(buffer, totalRead, buffer.Length - totalRead);
+ int bytesRead = dataStream.Read(buffer[totalRead..]);
if (bytesRead <= 0)
{
break;
diff --git a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs
index 2b843cc8f6..4276199ee0 100644
--- a/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitReader/BitReaderBase.cs
@@ -34,7 +34,7 @@ protected static IMemoryOwner ReadImageDataFromStream(Stream input, int by
{
IMemoryOwner data = memoryAllocator.Allocate(bytesToRead, AllocationOptions.Clean);
Span dataSpan = data.Memory.Span;
- input.Read(dataSpan[..bytesToRead], 0, bytesToRead);
+ input.Read(dataSpan[..bytesToRead]);
return data;
}
diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
index 39c4beb618..cdf1470ddb 100644
--- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
+++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
@@ -143,6 +143,7 @@ public static void WriteTrunksAfterData(
{
if (exifProfile != null)
{
+ // Serialization applies Parts even when the current profile has not been initialized.
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
}
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
index 7d22f7f2b3..e7b135dc96 100644
--- a/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpFrameData.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.IO;
+
namespace SixLabors.ImageSharp.Formats.Webp.Chunks;
internal readonly struct WebpFrameData
@@ -120,7 +122,7 @@ public long WriteHeaderTo(Stream stream)
///
/// The stream to read from.
/// Animation frame data.
- public static WebpFrameData Parse(Stream stream)
+ public static WebpFrameData Parse(BufferedReadStream stream)
{
Span buffer = stackalloc byte[4];
diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
index 8a8ad823dc..323ee30b90 100644
--- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
@@ -384,7 +384,9 @@ private void ReadOptionalChunk(
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must
// precede the frame data, and we cannot safely skip it without successfully reading its size.
- WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata);
+ // ReadIccProfile therefore validates the complete chunk extent before invoking the ancillary
+ // handler. Only errors in the contents of a complete chunk follow that recovery policy.
+ WebpChunkParsingUtils.ReadIccProfile(stream, imageMetadata, ignoreMetadata, this.executeAncillarySegmentAction);
break;
case WebpChunkType.Exif:
this.executeAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, imageMetadata, ignoreMetadata));
diff --git a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
index 119d53fe96..958f4df80b 100644
--- a/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
+++ b/src/ImageSharp/Formats/Webp/WebpChunkParsingUtils.cs
@@ -262,7 +262,7 @@ public static WebpImageInfo ReadVp8XHeader(BufferedReadStream stream, Span
///
/// Thrown if the input stream is not valid.
///
- public static uint ReadUInt24LittleEndian(Stream stream, Span buffer)
+ public static uint ReadUInt24LittleEndian(BufferedReadStream stream, Span buffer)
{
if (stream.Read(buffer, 0, 3) == 3)
{
@@ -306,12 +306,33 @@ public static unsafe void WriteUInt24LittleEndian(Stream stream, uint data)
/// If true, the chunk size is required to be read, otherwise it can be skipped.
/// The chunk size in bytes.
/// Thrown if the input stream is not valid.
- public static uint ReadChunkSize(Stream stream, Span buffer, bool required = true)
+ public static uint ReadChunkSize(BufferedReadStream stream, Span buffer, bool required = true)
+ {
+ ulong chunkSize = ReadPaddedChunkSize(stream, buffer, required);
+
+ // Structural chunk sizes must remain representable by their uint-sized consumers.
+ // Metadata readers retain the wider extent so their recovery can skip it safely.
+ if (chunkSize > uint.MaxValue)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("WebP chunk size exceeds the supported maximum.");
+ }
+
+ return (uint)chunkSize;
+ }
+
+ ///
+ /// Reads a chunk's complete padded extent without wrapping a uint-sized payload length.
+ ///
+ /// The input stream.
+ /// The four-byte size buffer.
+ /// Whether an incomplete size field is an error.
+ /// The padded extent, or remaining bytes when an optional size field is incomplete.
+ private static ulong ReadPaddedChunkSize(BufferedReadStream stream, Span buffer, bool required)
{
if (stream.Read(buffer) is 4)
{
uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
- return chunkSize % 2 is 0 ? chunkSize : chunkSize + 1;
+ return (ulong)chunkSize + (chunkSize & 1);
}
if (required)
@@ -320,7 +341,7 @@ public static uint ReadChunkSize(Stream stream, Span buffer, bool required
}
// Return the size of the remaining data in the stream.
- return (uint)(stream.Length - stream.Position);
+ return (ulong)stream.RemainingBytes;
}
///
@@ -349,34 +370,36 @@ public static WebpChunkType ReadChunkType(BufferedReadStream stream, Span
/// The stream to decode from.
/// The image metadata.
/// If true, metadata will be ignored.
+ /// Executes profile parsing under the decoder's integrity policy.
public static void ReadIccProfile(
BufferedReadStream stream,
ImageMetadata metadata,
- bool ignoreMetadata)
+ bool ignoreMetadata,
+ Action executeAncillarySegmentAction)
{
- Span buffer = stackalloc byte[4];
- uint iccpChunkSize = ReadChunkSize(stream, buffer);
- if (ignoreMetadata || metadata.IccProfile != null)
+ ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], true);
+
+ // ICCP precedes image/frame data. Its framing must be readable even when
+ // metadata is skipped; otherwise there is no safe location to resume decoding.
+ if (!stream.IsReadRangeValid(stream.Position, chunkSize))
{
- stream.Skip((int)iccpChunkSize);
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the ICCP chunk.");
}
- else
+
+ executeAncillarySegmentAction(() =>
{
- byte[] iccpData = new byte[iccpChunkSize];
- int bytesRead = stream.Read(iccpData, 0, (int)iccpChunkSize);
- if (bytesRead != iccpChunkSize)
+ byte[]? iccpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.IccProfile != null);
+ if (iccpData is not null)
{
- WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk");
- }
+ IccProfile profile = new(iccpData);
+ if (!profile.CheckIsValid())
+ {
+ throw new InvalidIccProfileException("Invalid ICC profile.");
+ }
- IccProfile profile = new(iccpData);
- if (!profile.CheckIsValid())
- {
- throw new InvalidIccProfileException("Invalid ICC profile.");
+ metadata.IccProfile = profile;
}
-
- metadata.IccProfile = profile;
- }
+ });
}
///
@@ -390,21 +413,10 @@ public static void ReadExifProfile(
ImageMetadata metadata,
bool ignoreMetadata)
{
- Span buffer = stackalloc byte[4];
- uint exifChunkSize = ReadChunkSize(stream, buffer);
- if (ignoreMetadata || metadata.ExifProfile != null)
- {
- stream.Skip((int)exifChunkSize);
- }
- else
+ ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata);
+ byte[]? exifData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.ExifProfile != null);
+ if (exifData is not null)
{
- byte[] exifData = new byte[exifChunkSize];
- int bytesRead = stream.Read(exifData, 0, (int)exifChunkSize);
- if (bytesRead != exifChunkSize)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the EXIF profile");
- }
-
ExifProfile exifProfile = new(exifData);
// Set the resolution from the metadata.
@@ -433,23 +445,46 @@ public static void ReadXmpProfile(
ImageMetadata metadata,
bool ignoreMetadata)
{
- Span buffer = stackalloc byte[4];
- uint xmpChunkSize = ReadChunkSize(stream, buffer);
- if (ignoreMetadata || metadata.XmpProfile != null)
+ ulong chunkSize = ReadPaddedChunkSize(stream, stackalloc byte[4], !ignoreMetadata);
+ byte[]? xmpData = ReadMetadataChunk(stream, chunkSize, ignoreMetadata || metadata.XmpProfile != null);
+ if (xmpData is not null)
+ {
+ metadata.XmpProfile = new XmpProfile(xmpData);
+ }
+ }
+
+ ///
+ /// Reads a metadata payload, leaving the stream at the next chunk or EOF on a recoverable error.
+ /// Callers execute metadata parsing under the decoder's ancillary integrity policy.
+ ///
+ /// The input stream positioned at the chunk payload.
+ /// The declared extent including its padding byte.
+ /// Whether to skip the payload without parsing it.
+ /// The payload, or null when metadata is skipped.
+ private static byte[]? ReadMetadataChunk(BufferedReadStream stream, ulong paddedLength, bool ignoreMetadata)
+ {
+ long chunkEnd = stream.Position + (long)Math.Min(paddedLength, (ulong)stream.RemainingBytes);
+ if (ignoreMetadata)
{
- stream.Skip((int)xmpChunkSize);
+ stream.Position = chunkEnd;
+ return null;
}
- else
+
+ if (!stream.TryGetReadLength(paddedLength, out int bufferLength))
{
- byte[] xmpData = new byte[xmpChunkSize];
- int bytesRead = stream.Read(xmpData, 0, (int)xmpChunkSize);
- if (bytesRead != xmpChunkSize)
- {
- WebpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the XMP profile");
- }
+ // Ignoring an ancillary error must not make the next parser interpret
+ // this payload as another chunk header. A truncated chunk consumes EOF.
+ stream.Position = chunkEnd;
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk.");
+ }
- metadata.XmpProfile = new XmpProfile(xmpData);
+ byte[] data = new byte[bufferLength];
+ if (stream.Read(data) != bufferLength)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the metadata chunk.");
}
+
+ return data;
}
private static double GetExifResolutionValue(ExifProfile exifProfile, ExifTag tag)
diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
index 55bdca69cb..c2a4e44653 100644
--- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
@@ -92,6 +92,13 @@ protected override Image Decode(BufferedReadStream stream, Cance
return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize);
}
+ // A VP8X header alone describes a canvas, not decodable image data.
+ // Ignoring a truncated optional chunk must not bypass this requirement.
+ if (this.webImageInfo.Vp8BitReader is null && this.webImageInfo.Vp8LBitReader is null)
+ {
+ WebpThrowHelper.ThrowInvalidImageContentException("Missing WebP image data.");
+ }
+
image = new Image(this.configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, metadata);
Buffer2D pixels = image.GetRootFramePixelBuffer();
if (this.webImageInfo.IsLossless)
@@ -281,7 +288,9 @@ private bool ParseOptionalExtendedChunks(
// While ICC profiles are optional, an invalid ICC profile cannot be ignored because it must
// precede the image data, and we cannot safely skip it without successfully reading its size.
- WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata);
+ // ReadIccProfile therefore validates the complete chunk extent before invoking the ancillary
+ // handler. Only errors in the contents of a complete chunk follow that recovery policy.
+ WebpChunkParsingUtils.ReadIccProfile(stream, metadata, ignoreMetadata, this.ExecuteAncillarySegmentAction);
break;
case WebpChunkType.Exif:
@@ -330,17 +339,17 @@ private void ParseOptionalChunks(BufferedReadStream stream, ImageMetadata metada
{
// Read chunk header.
WebpChunkType chunkType = WebpChunkParsingUtils.ReadChunkType(stream, buffer);
- if (chunkType == WebpChunkType.Exif && metadata.ExifProfile == null)
+ if (chunkType == WebpChunkType.Exif)
{
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadExifProfile(stream, metadata, ignoreMetadata));
}
- else if (chunkType == WebpChunkType.Xmp && metadata.XmpProfile == null)
+ else if (chunkType == WebpChunkType.Xmp)
{
this.ExecuteAncillarySegmentAction(() => WebpChunkParsingUtils.ReadXmpProfile(stream, metadata, ignoreMetadata));
}
else
{
- // Skip duplicate XMP or EXIF chunk.
+ // Skip unknown chunks.
uint chunkLength = WebpChunkParsingUtils.ReadChunkSize(stream, buffer, false);
stream.Skip((int)chunkLength);
}
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
index aa2eb29e79..6cd16aa9c2 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
@@ -221,14 +221,21 @@ public void SetValue(ExifTag tag, TValueType value)
=> this.SetValueInternal(tag, value);
///
- /// Converts this instance to a byte array.
+ /// Converts the sections selected by to a byte array.
///
/// The
public byte[]? ToByteArray()
{
if (this.values is null)
{
- return this.data;
+ // The original bytes include every section. They can only be reused when no filtering
+ // is requested; otherwise lazy profiles must go through the same writer as initialized ones.
+ if (this.Parts == ExifParts.All)
+ {
+ return this.data;
+ }
+
+ this.InitializeValues();
}
if (this.values.Count == 0)
diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
index 4cd4b4aac9..4e2315cd88 100644
--- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
+++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
@@ -224,6 +224,13 @@ protected void ReadValues64(List values, ulong offset)
this.Seek(offset);
ulong count = this.ReadUInt64();
+ // Each entry occupies 20 bytes and the directory ends with an 8-byte next-IFD offset.
+ long remainingDirectoryBytes = this.data.Length - this.data.Position;
+ if (remainingDirectoryBytes < 8 || count > (ulong)((remainingDirectoryBytes - 8) / 20))
+ {
+ throw new InvalidImageContentException("The BigTIFF directory entry count exceeds the available data.");
+ }
+
Span offsetBuffer = stackalloc byte[8];
for (ulong i = 0; i < count; i++)
{
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs
index 700e43f972..da2f37efd3 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.Lut.cs
@@ -72,13 +72,7 @@ public IccClut ReadClut(int inChannelCount, int outChannelCount, bool isFloat)
/// The read CLUT8.
public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPointCount)
{
- int length = 0;
- for (int i = 0; i < inChannelCount; i++)
- {
- length += (int)Math.Pow(gridPointCount[i], inChannelCount);
- }
-
- length /= inChannelCount;
+ int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 1);
const float Max = byte.MaxValue;
@@ -105,13 +99,7 @@ public IccClut ReadClut8(int inChannelCount, int outChannelCount, byte[] gridPoi
public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount)
{
int start = this.currentIndex;
- int length = 0;
- for (int i = 0; i < inChannelCount; i++)
- {
- length += (int)Math.Pow(gridPointCount[i], inChannelCount);
- }
-
- length /= inChannelCount;
+ int length = this.GetClutLength(inChannelCount, outChannelCount, gridPointCount, 2);
const float Max = ushort.MaxValue;
@@ -139,13 +127,7 @@ public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPo
public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount)
{
int start = this.currentIndex;
- int length = 0;
- for (int i = 0; i < inChCount; i++)
- {
- length += (int)Math.Pow(gridPointCount[i], inChCount);
- }
-
- length /= inChCount;
+ int length = this.GetClutLength(inChCount, outChCount, gridPointCount, 4);
float[] values = new float[length * outChCount];
int offset = 0;
@@ -160,4 +142,28 @@ public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount)
this.currentIndex = start + (length * outChCount * 4);
return new IccClut(values, gridPointCount, IccClutDataType.Float, outChCount);
}
+
+ private int GetClutLength(int inputChannelCount, int outputChannelCount, byte[] gridPointCount, int bytesPerValue)
+ {
+ int length = 1;
+ for (int i = 0; i < inputChannelCount; i++)
+ {
+ int gridPoints = gridPointCount[i];
+ if (gridPoints == 0 || length > int.MaxValue / gridPoints)
+ {
+ throw new InvalidIccProfileException("Invalid CLUT dimensions.");
+ }
+
+ length *= gridPoints;
+ }
+
+ long valueCount = (long)length * outputChannelCount;
+ long byteCount = valueCount * bytesPerValue;
+ if (valueCount > int.MaxValue || byteCount > this.data.Length - this.currentIndex)
+ {
+ throw new InvalidIccProfileException("The CLUT data is shorter than its declared dimensions.");
+ }
+
+ return length;
+ }
}
diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
index 084ec388d6..d4ee3bded1 100644
--- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
+++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs
@@ -119,6 +119,7 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader)
}
List table = new((int)tagCount);
+ uint dataLength = (uint)reader.DataLength;
for (int i = 0; i < tagCount; i++)
{
uint tagSignature = reader.ReadUInt32();
@@ -126,7 +127,7 @@ private static IccTagTableEntry[] ReadTagTable(IccDataReader reader)
uint tagSize = reader.ReadUInt32();
// Exclude entries that have nonsense values and could cause exceptions further on
- if (tagOffset < reader.DataLength && tagSize < reader.DataLength - 128)
+ if (tagSize >= 8 && tagOffset <= dataLength && tagSize <= dataLength - tagOffset)
{
table.Add(new IccTagTableEntry((IccProfileTag)tagSignature, tagOffset, tagSize));
}
diff --git a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs
index 53f22eb09a..6de4b30b9e 100644
--- a/src/ImageSharp/PixelFormats/HalfTypeHelper.cs
+++ b/src/ImageSharp/PixelFormats/HalfTypeHelper.cs
@@ -3,6 +3,7 @@
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace SixLabors.ImageSharp.PixelFormats;
@@ -50,52 +51,228 @@ internal static class HalfTypeHelper
internal static float Unpack(ushort value) => (float)BitConverter.UInt16BitsToHalf(value);
///
- /// Normalizes a finite binary16 value to the scaled pixel range.
+ /// Normalizes a binary16 value to [0, 1], saturating infinities and mapping NaN to zero.
///
/// The native binary16 value represented as a .
/// The normalized value.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static float ToScaled(float value) => (value * InverseFiniteRange) + ScaledMidpoint;
+ public static float ToScaled(float value)
+ {
+ // Clamp after mapping so native infinities reach the scaled endpoints and NaN becomes zero.
+ return Numerics.Clamp((value * InverseFiniteRange) + ScaledMidpoint, 0F, 1F);
+ }
///
- /// Normalizes finite binary16 values to the scaled pixel range.
+ /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
///
/// The native binary16 values.
/// The normalized values.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Vector2 ToScaled(Vector2 value) => (value * InverseFiniteRange) + new Vector2(ScaledMidpoint);
+ public static Vector2 ToScaled(Vector2 value) => ToScaled(value.AsVector128()).AsVector2();
///
- /// Normalizes finite binary16 values to the scaled pixel range.
+ /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
///
/// The native binary16 values.
/// The normalized values.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Vector4 ToScaled(Vector4 value) => (value * InverseFiniteRange) + new Vector4(ScaledMidpoint);
+ public static Vector4 ToScaled(Vector4 value) => ToScaled(value.AsVector128()).AsVector4();
+
+ ///
+ /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
+ ///
+ /// The component values.
+ /// The converted values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 ToScaled(Vector128 value)
+ {
+ Vector128 scaled = (value * Vector128.Create(InverseFiniteRange)) + Vector128.Create(ScaledMidpoint);
+
+ return Numerics.Clamp(scaled, Vector128.Zero, Vector128.One);
+ }
+
+ ///
+ /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
+ ///
+ /// The component values.
+ /// The converted values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 ToScaled(Vector256 value)
+ {
+ Vector256 scaled = (value * Vector256.Create(InverseFiniteRange)) + Vector256.Create(ScaledMidpoint);
+
+ return Numerics.Clamp(scaled, Vector256.Zero, Vector256.One);
+ }
+
+ ///
+ /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
+ ///
+ /// The component values.
+ /// The converted values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 ToScaled(Vector512 value)
+ {
+ Vector512 scaled = (value * Vector512.Create(InverseFiniteRange)) + Vector512.Create(ScaledMidpoint);
+
+ return Numerics.Clamp(scaled, Vector512.Zero, Vector512.One);
+ }
+
+ ///
+ /// Normalizes binary16 values to [0, 1], saturating infinities and mapping NaN to zero.
+ ///
+ /// The component values to convert in place.
+ public static void ToScaled(Span values)
+ {
+ ref Vector4 source = ref MemoryMarshal.GetReference(values);
+ int i = 0;
+
+ // Each register contains whole RGBA pixels. Convert wide groups first, then narrower
+ // remainders without revisiting any pixel: mapping the same pixel twice would change its value.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ int pixelsPerRegister = Vector512.Count / Vector128.Count;
+
+ for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
+ {
+ ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i));
+
+ vector = ToScaled(vector);
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ int pixelsPerRegister = Vector256.Count / Vector128.Count;
+
+ for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
+ {
+ ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i));
+
+ vector = ToScaled(vector);
+ }
+ }
+
+ // One Vector4 uses the same 128-bit conversion as an individual pixel, including the
+ // runtime's software fallback when SIMD is unavailable. No separate scalar mapping is needed.
+ for (; i < values.Length; i++)
+ {
+ ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i);
+
+ vector = ToScaled(vector);
+ }
+ }
///
- /// Expands a normalized value to the finite binary16 range.
+ /// Normalizes a scaled value, mapping NaN to zero, and expands it to the finite binary16 range.
///
/// The normalized value.
/// The native binary16 value represented as a .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static float FromScaled(float value) => (value * FiniteRange) + FiniteMinimum;
+ public static float FromScaled(float value)
+ {
+ // Clamp before expanding so nonfinite scaled input cannot become nonfinite half storage.
+ return (Numerics.Clamp(value, 0F, 1F) * FiniteRange) + FiniteMinimum;
+ }
///
- /// Expands normalized values to the finite binary16 range.
+ /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
///
/// The normalized values.
/// The native binary16 values.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Vector2 FromScaled(Vector2 value) => (value * FiniteRange) + new Vector2(FiniteMinimum);
+ public static Vector2 FromScaled(Vector2 value) => FromScaled(value.AsVector128()).AsVector2();
///
- /// Expands normalized values to the finite binary16 range.
+ /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
///
/// The normalized values.
/// The native binary16 values.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static Vector4 FromScaled(Vector4 value) => (value * FiniteRange) + new Vector4(FiniteMinimum);
+ public static Vector4 FromScaled(Vector4 value) => FromScaled(value.AsVector128()).AsVector4();
+
+ ///
+ /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
+ ///
+ /// The component values.
+ /// The converted values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 FromScaled(Vector128 value)
+ {
+ Vector128 scaled = Numerics.Clamp(value, Vector128.Zero, Vector128.One);
+
+ return (scaled * Vector128.Create(FiniteRange)) + Vector128.Create(FiniteMinimum);
+ }
+
+ ///
+ /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
+ ///
+ /// The component values.
+ /// The converted values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector256 FromScaled(Vector256 value)
+ {
+ Vector256 scaled = Numerics.Clamp(value, Vector256.Zero, Vector256.One);
+
+ return (scaled * Vector256.Create(FiniteRange)) + Vector256.Create(FiniteMinimum);
+ }
+
+ ///
+ /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
+ ///
+ /// The component values.
+ /// The converted values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector512 FromScaled(Vector512 value)
+ {
+ Vector512 scaled = Numerics.Clamp(value, Vector512.Zero, Vector512.One);
+
+ return (scaled * Vector512.Create(FiniteRange)) + Vector512.Create(FiniteMinimum);
+ }
+
+ ///
+ /// Normalizes scaled values, mapping NaN to zero, and expands them to the finite binary16 range.
+ ///
+ /// The component values to convert in place.
+ public static void FromScaled(Span values)
+ {
+ ref Vector4 source = ref MemoryMarshal.GetReference(values);
+ int i = 0;
+
+ // Each register contains whole RGBA pixels. Clamping and expansion happen together in
+ // the conversion overload, so each pixel is loaded and stored once without a clamp-only pass.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ int pixelsPerRegister = Vector512.Count / Vector128.Count;
+
+ for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
+ {
+ ref Vector512 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i));
+
+ vector = FromScaled(vector);
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ int pixelsPerRegister = Vector256.Count / Vector128.Count;
+
+ for (; i <= values.Length - pixelsPerRegister; i += pixelsPerRegister)
+ {
+ ref Vector256 vector = ref Unsafe.As>(ref Unsafe.Add(ref source, (uint)i));
+
+ vector = FromScaled(vector);
+ }
+ }
+
+ // The remaining whole pixels use the same 128-bit conversion as individual pixels,
+ // or its software fallback. Narrowing the remainder never reprocesses a converted pixel.
+ for (; i < values.Length; i++)
+ {
+ ref Vector4 vector = ref Unsafe.Add(ref source, (uint)i);
+
+ vector = FromScaled(vector);
+ }
+ }
///
/// Unpacks eight binary16 values into two vectors of single-precision values.
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs
index 50fc272553..ca90afcf75 100644
--- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4P.cs
@@ -250,7 +250,7 @@ private static HalfVector4P PackAssociatedScaledVector4(Vector4 source)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float QuantizeScaledAlpha(float alpha)
{
- float nativeAlpha = HalfTypeHelper.FromScaled(Numerics.Clamp(alpha, 0F, 1F));
+ float nativeAlpha = HalfTypeHelper.FromScaled(alpha);
return HalfTypeHelper.ToScaled(HalfTypeHelper.Unpack(HalfTypeHelper.Pack(nativeAlpha)));
}
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs
index 9eae7d5cbc..a921f99bae 100644
--- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4.PixelOperations.cs
@@ -3,7 +3,6 @@
using System.Numerics;
using System.Runtime.InteropServices;
-using SixLabors.ImageSharp.PixelFormats.Utils;
namespace SixLabors.ImageSharp.PixelFormats;
@@ -17,11 +16,6 @@ public partial struct HalfVector4
///
internal class PixelOperations : PixelOperations
{
- private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange);
- private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint);
- private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange);
- private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum);
-
///
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination)
{
@@ -40,9 +34,9 @@ protected override void ToAssociatedVector4(Configuration configuration, ReadOnl
// Association uses normalized opacity, not the native binary16 alpha value.
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination);
- Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset);
+ HalfTypeHelper.ToScaled(destination);
Numerics.Premultiply(destination);
- Vector4Converters.MultiplyThenAdd(destination, ScaledToNativeMultiplier, ScaledToNativeOffset);
+ HalfTypeHelper.FromScaled(destination);
}
///
@@ -52,7 +46,7 @@ protected override void ToUnassociatedScaledVector4(Configuration configuration,
destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination);
- Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset);
+ HalfTypeHelper.ToScaled(destination);
}
///
@@ -77,9 +71,9 @@ protected override void FromAssociatedVector4Destructive(Configuration configura
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// Restore normalized opacity before unassociating, then return the result to the native binary16 range.
- Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset);
+ HalfTypeHelper.ToScaled(source);
Numerics.UnPremultiply(source);
- Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
+ HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length]));
}
@@ -88,7 +82,7 @@ protected override void FromUnassociatedScaledVector4Destructive(Configuration c
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
- Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
+ HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length]));
}
@@ -98,7 +92,7 @@ protected override void FromAssociatedScaledVector4Destructive(Configuration con
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
Numerics.UnPremultiply(source);
- Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
+ HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination[..source.Length]));
}
}
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs
index 52b2dbf376..172159a49c 100644
--- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/HalfVector4P.PixelOperations.cs
@@ -20,16 +20,11 @@ public partial struct HalfVector4P
///
internal class PixelOperations : AssociatedAlphaPixelOperations
{
- private static readonly Vector4 NativeToScaledMultiplier = new(HalfTypeHelper.InverseFiniteRange);
- private static readonly Vector4 NativeToScaledOffset = new(HalfTypeHelper.ScaledMidpoint);
- private static readonly Vector4 ScaledToNativeMultiplier = new(HalfTypeHelper.FiniteRange);
- private static readonly Vector4 ScaledToNativeOffset = new(HalfTypeHelper.FiniteMinimum);
-
///
protected override void ToUnassociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination)
{
this.ToUnassociatedScaledVector4(configuration, source, destination);
- Vector4Converters.MultiplyThenAdd(destination[..source.Length], ScaledToNativeMultiplier, ScaledToNativeOffset);
+ HalfTypeHelper.FromScaled(destination[..source.Length]);
}
///
@@ -54,7 +49,7 @@ protected override void ToAssociatedScaledVector4(Configuration configuration, R
destination = destination[..source.Length];
RgbaHalfP.PixelOperations.Unpack(MemoryMarshal.Cast(source), destination);
- Vector4Converters.MultiplyThenAdd(destination, NativeToScaledMultiplier, NativeToScaledOffset);
+ HalfTypeHelper.ToScaled(destination);
}
///
@@ -62,7 +57,7 @@ protected override void FromUnassociatedVector4Destructive(Configuration configu
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
- Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset);
+ HalfTypeHelper.ToScaled(source);
Associate(source);
PackAssociatedScaled(source, destination[..source.Length]);
}
@@ -72,7 +67,7 @@ protected override void FromAssociatedVector4Destructive(Configuration configura
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
- Vector4Converters.MultiplyThenAdd(source, NativeToScaledMultiplier, NativeToScaledOffset);
+ HalfTypeHelper.ToScaled(source);
Reassociate(source);
PackAssociatedScaled(source, destination[..source.Length]);
}
@@ -250,7 +245,9 @@ private static Vector128 Reassociate(Vector128 source)
Vector128 storedAlpha = QuantizeScaledAlpha(alpha);
Vector128 result = source * (storedAlpha / alpha);
result = Vector128.ConditionalSelect(Vector128.Create(0, 0, 0, -1).AsSingle(), storedAlpha, result);
- result = Vector128.Min(Vector128.Max(result, zero), storedAlpha);
+
+ // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB.
+ result = Numerics.Clamp(result, zero, storedAlpha);
return Vector128.ConditionalSelect(Vector128.LessThanOrEqual(alpha, zero), zero, result);
}
@@ -267,7 +264,9 @@ private static Vector256 Reassociate(Vector256 source)
Vector256 storedAlpha = QuantizeScaledAlpha(alpha);
Vector256 result = source * (storedAlpha / alpha);
result = Vector256.ConditionalSelect(Vector256.Create(0, 0, 0, -1, 0, 0, 0, -1).AsSingle(), storedAlpha, result);
- result = Vector256.Min(Vector256.Max(result, zero), storedAlpha);
+
+ // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB.
+ result = Numerics.Clamp(result, zero, storedAlpha);
return Vector256.ConditionalSelect(Vector256.LessThanOrEqual(alpha, zero), zero, result);
}
@@ -285,7 +284,9 @@ private static Vector512 Reassociate(Vector512 source)
Vector512 result = source * (storedAlpha / alpha);
Vector512 alphaMask = Vector512.Create(0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1, 0, 0, 0, -1).AsSingle();
result = Vector512.ConditionalSelect(alphaMask, storedAlpha, result);
- result = Vector512.Min(Vector512.Max(result, zero), storedAlpha);
+
+ // Clamp after the alpha ratio, matching the scalar conversion for nonfinite RGB.
+ result = Numerics.Clamp(result, zero, storedAlpha);
return Vector512.ConditionalSelect(Vector512.LessThanOrEqual(alpha, zero), zero, result);
}
@@ -297,8 +298,8 @@ private static Vector512 Reassociate(Vector512 source)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128 QuantizeScaledAlpha(Vector128 alpha)
{
- Vector128 native = (ClampUnit(alpha) * Vector128.Create(HalfTypeHelper.FiniteRange)) + Vector128.Create(HalfTypeHelper.FiniteMinimum);
- return (HalfTypeHelper.RoundToHalf(native) * Vector128.Create(HalfTypeHelper.InverseFiniteRange)) + Vector128.Create(HalfTypeHelper.ScaledMidpoint);
+ Vector128 native = HalfTypeHelper.FromScaled(alpha);
+ return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
}
///
@@ -309,8 +310,8 @@ private static Vector128 QuantizeScaledAlpha(Vector128 alpha)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256 QuantizeScaledAlpha(Vector256 alpha)
{
- Vector256 native = (ClampUnit(alpha) * Vector256.Create(HalfTypeHelper.FiniteRange)) + Vector256.Create(HalfTypeHelper.FiniteMinimum);
- return (HalfTypeHelper.RoundToHalf(native) * Vector256.Create(HalfTypeHelper.InverseFiniteRange)) + Vector256.Create(HalfTypeHelper.ScaledMidpoint);
+ Vector256 native = HalfTypeHelper.FromScaled(alpha);
+ return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
}
///
@@ -321,45 +322,33 @@ private static Vector256 QuantizeScaledAlpha(Vector256 alpha)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512 QuantizeScaledAlpha(Vector512 alpha)
{
- Vector512 native = (ClampUnit(alpha) * Vector512.Create(HalfTypeHelper.FiniteRange)) + Vector512.Create(HalfTypeHelper.FiniteMinimum);
- return (HalfTypeHelper.RoundToHalf(native) * Vector512.Create(HalfTypeHelper.InverseFiniteRange)) + Vector512.Create(HalfTypeHelper.ScaledMidpoint);
+ Vector512 native = HalfTypeHelper.FromScaled(alpha);
+ return HalfTypeHelper.ToScaled(HalfTypeHelper.RoundToHalf(native));
}
///
- /// Clamps vectors to the scaled color range while preserving NaN lanes.
+ /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
///
/// The vectors to clamp.
/// The clamped vectors.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector128 ClampUnit(Vector128 source)
- {
- Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One);
- return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source);
- }
+ private static Vector128 ClampUnit(Vector128 source) => Numerics.Clamp(source, Vector128.Zero, Vector128.One);
///
- /// Clamps vectors to the scaled color range while preserving NaN lanes.
+ /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
///
/// The vectors to clamp.
/// The clamped vectors.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector256 ClampUnit(Vector256 source)
- {
- Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One);
- return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source);
- }
+ private static Vector256 ClampUnit(Vector256 source) => Numerics.Clamp(source, Vector256.Zero, Vector256.One);
///
- /// Clamps vectors to the scaled color range while preserving NaN lanes.
+ /// Clamps vectors to the scaled color range, mapping NaN lanes to zero.
///
/// The vectors to clamp.
/// The clamped vectors.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector512 ClampUnit(Vector512 source)
- {
- Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512.Zero), Vector512.One);
- return Vector512.ConditionalSelect(Vector512.Equals(source, source), clamped, source);
- }
+ private static Vector512 ClampUnit(Vector512 source) => Numerics.Clamp(source, Vector512.Zero, Vector512.One);
///
/// Maps associated scaled vectors to native components and packs them as binary16 values.
@@ -368,7 +357,7 @@ private static Vector512 ClampUnit(Vector512 source)
/// The destination pixels.
private static void PackAssociatedScaled(Span source, Span destination)
{
- Vector4Converters.MultiplyThenAdd(source, ScaledToNativeMultiplier, ScaledToNativeOffset);
+ HalfTypeHelper.FromScaled(source);
RgbaHalfP.PixelOperations.PackUnclamped(source, MemoryMarshal.Cast(destination));
}
}
diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs
index 825ebeec92..a9f3081f1a 100644
--- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs
+++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaHalfP.PixelOperations.cs
@@ -277,8 +277,8 @@ internal static void Pack(Span source, Span destination)
{
for (; i <= componentCount - Vector512.Count; i += Vector512.Count)
{
- Vector512 lower = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)i));
- Vector512 upper = ClampUnit(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)));
+ Vector512 lower = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)i), Vector512.Zero, Vector512.One);
+ Vector512 upper = Numerics.Clamp(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count)), Vector512.Zero, Vector512.One);
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
@@ -287,8 +287,8 @@ internal static void Pack(Span source, Span destination)
{
for (; i <= componentCount - Vector256.Count; i += Vector256.Count)
{
- Vector256 lower = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)i));
- Vector256 upper = ClampUnit(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)));
+ Vector256 lower = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)i), Vector256.Zero, Vector256.One);
+ Vector256 upper = Numerics.Clamp(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count)), Vector256.Zero, Vector256.One);
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
@@ -297,15 +297,15 @@ internal static void Pack(Span source, Span destination)
{
for (; i <= componentCount - Vector128.Count; i += Vector128.Count)
{
- Vector128 lower = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
- Vector128 upper = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)));
+ Vector128 lower = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128.Zero, Vector128.One);
+ Vector128 upper = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count)), Vector128.Zero, Vector128.One);
Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
if (i < componentCount)
{
// Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel.
- Vector128 vector = ClampUnit(Vector128.LoadUnsafe(ref sourceBase, (nuint)i));
+ Vector128 vector = Numerics.Clamp(Vector128.LoadUnsafe(ref sourceBase, (nuint)i), Vector128.Zero, Vector128.One);
Vector128 packed = HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
}
@@ -401,8 +401,8 @@ internal static void PackFromAssociated(Span source, Span de
{
for (; i <= componentCount - Vector512.Count; i += Vector512.Count)
{
- Vector512 lower = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)));
- Vector512 upper = ClampUnit(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))));
+ Vector512 lower = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)i)), Vector512.Zero, Vector512.One);
+ Vector512 upper = Numerics.Clamp(Unassociate(Vector512.LoadUnsafe(ref sourceBase, (nuint)(i + Vector512.Count))), Vector512.Zero, Vector512.One);
Vector512.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
@@ -411,8 +411,8 @@ internal static void PackFromAssociated(Span source, Span de
{
for (; i <= componentCount - Vector256.Count; i += Vector256.Count)
{
- Vector256 lower = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)));
- Vector256 upper = ClampUnit(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))));
+ Vector256 lower = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)i)), Vector256.Zero, Vector256.One);
+ Vector256 upper = Numerics.Clamp(Unassociate(Vector256.LoadUnsafe(ref sourceBase, (nuint)(i + Vector256.Count))), Vector256.Zero, Vector256.One);
Vector256.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
}
@@ -421,15 +421,15 @@ internal static void PackFromAssociated(Span source, Span de
{
for (; i <= componentCount - Vector128.Count; i += Vector128.Count)
{
- Vector128 lower = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)));
- Vector128 upper = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))));
+ Vector128 lower = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128.Zero, Vector128.One);
+ Vector128 upper = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)(i + Vector128.Count))), Vector128.Zero, Vector128.One);
Vector128.StoreUnsafe(HalfTypeHelper.Pack(lower, upper), ref destinationBase, (nuint)i);
}
if (i < componentCount)
{
// Duplicate the final vector to use the two-input narrowing primitive, then store only one complete pixel.
- Vector128 vector = ClampUnit(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)));
+ Vector128 vector = Numerics.Clamp(Unassociate(Vector128.LoadUnsafe(ref sourceBase, (nuint)i)), Vector128.Zero, Vector128.One);
Vector128 packed = HalfTypeHelper.Pack(vector, vector);
Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, (uint)i)), packed.AsUInt64().GetElement(0));
}
@@ -649,48 +649,6 @@ private static Vector512 Unassociate(Vector512 source)
return Numerics.UnPremultiply(source, alpha);
}
- ///
- /// Clamps vectors to the unit range represented by the pixel format.
- ///
- /// The vectors to clamp.
- /// The clamped vectors.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector128 ClampUnit(Vector128 source)
- {
- Vector128 clamped = Vector128.Min(Vector128.Max(source, Vector128.Zero), Vector128.One);
-
- // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract.
- return Vector128.ConditionalSelect(Vector128.Equals(source, source), clamped, source);
- }
-
- ///
- /// Clamps vectors to the unit range represented by the pixel format.
- ///
- /// The vectors to clamp.
- /// The clamped vectors.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector256 ClampUnit(Vector256 source)
- {
- Vector256 clamped = Vector256.Min(Vector256.Max(source, Vector256.Zero), Vector256.One);
-
- // Ordered comparison is false for NaN, restoring the source lane to match the scalar clamp contract.
- return Vector256.ConditionalSelect(Vector256.Equals(source, source), clamped, source);
- }
-
- ///
- /// Clamps vectors to the unit range represented by the pixel format.
- ///
- /// The vectors to clamp.
- /// The clamped vectors.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static Vector512 ClampUnit(Vector512 source)
- {
- Vector512 clamped = Vector512.Min(Vector512.Max(source, Vector512