diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
index 8e99728314..7a7e9c7812 100644
--- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs
+++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs
@@ -16,17 +16,15 @@ internal static class ColorNumerics
/// Vector for converting pixel to gray value as specified by
/// ITU-R Recommendation BT.709.
///
- private static readonly Vector4 Bt709 = new(.2126f, .7152f, .0722f, 0.0f);
+ public static readonly Vector4 Bt709 = new(.2126f, .7152f, .0722f, 0.0f);
///
- /// Convert a pixel value to grayscale using ITU-R Recommendation BT.709.
+ /// Gets unrounded, unsaturated luminance using ITU-R Recommendation BT.709.
///
/// The vector to get the luminance from.
- ///
- /// The number of luminance levels (256 for 8 bit, 65536 for 16 bit grayscale images).
- ///
+ /// The unrounded luminance.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int GetBT709Luminance(Vector4 vector, int luminanceLevels) => (int)MathF.Round(Vector4.Dot(vector, Bt709) * (luminanceLevels - 1));
+ public static float GetBT709Luminance(Vector4 vector) => Vector4.Dot(vector, Bt709);
///
/// 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 b70172d65b..f187b88596 100644
--- a/src/ImageSharp/Common/Helpers/Numerics.cs
+++ b/src/ImageSharp/Common/Helpers/Numerics.cs
@@ -1066,6 +1066,36 @@ public static nuint Vector512Count(int length)
where TVector : struct
=> (uint)length / (uint)Vector512.Count;
+ ///
+ /// Gets the count of vectors that safely fit into a span whose element type matches the vector lane type.
+ ///
+ /// The type of the span elements and vector lanes.
+ /// The given span.
+ /// Count of vectors that safely fit into the span.
+ public static nuint Vector128Count(this ReadOnlySpan span)
+ where TVector : struct
+ => (uint)span.Length / (uint)Vector128.Count;
+
+ ///
+ /// Gets the count of vectors that safely fit into a span whose element type matches the vector lane type.
+ ///
+ /// The type of the span elements and vector lanes.
+ /// The given span.
+ /// Count of vectors that safely fit into the span.
+ public static nuint Vector256Count(this ReadOnlySpan span)
+ where TVector : struct
+ => (uint)span.Length / (uint)Vector256.Count;
+
+ ///
+ /// Gets the count of vectors that safely fit into a span whose element type matches the vector lane type.
+ ///
+ /// The type of the span elements and vector lanes.
+ /// The given span.
+ /// Count of vectors that safely fit into the span.
+ public static nuint Vector512Count(this ReadOnlySpan span)
+ where TVector : struct
+ => (uint)span.Length / (uint)Vector512.Count;
+
///
/// Clamps a floating-point component while mapping NaN to the lower bound.
///
diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs b/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs
new file mode 100644
index 0000000000..d26e277230
--- /dev/null
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.FloatPlanes.cs
@@ -0,0 +1,150 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Common.Helpers;
+
+namespace SixLabors.ImageSharp;
+
+///
+/// Converts planar floating-point components to and from contiguous four-component vectors.
+///
+internal static partial class SimdUtils
+{
+ ///
+ /// Interleaves three equally sized component planes and an optional fourth plane into four-component vectors.
+ /// Values are copied without changing their floating-point representation.
+ ///
+ /// The values for .
+ /// The values for .
+ /// The values for .
+ /// The values for , or an empty span to use 1 for every value.
+ /// The destination vectors.
+ internal static void InterleaveFloatPlanes(
+ ReadOnlySpan component0,
+ ReadOnlySpan component1,
+ ReadOnlySpan component2,
+ ReadOnlySpan component3,
+ Span destination)
+ {
+ Guard.IsTrue(component1.Length == component0.Length, nameof(component1), "Components must be of same size!");
+ Guard.IsTrue(component2.Length == component0.Length, nameof(component2), "Components must be of same size!");
+ Guard.IsTrue(component3.IsEmpty || component3.Length == component0.Length, nameof(component3), "Components must be of same size!");
+ Guard.DestinationShouldNotBeTooShort(component0, destination, nameof(destination));
+
+ ref float c0 = ref MemoryMarshal.GetReference(component0);
+ ref float c1 = ref MemoryMarshal.GetReference(component1);
+ ref float c2 = ref MemoryMarshal.GetReference(component2);
+ ref float c3 = ref MemoryMarshal.GetReference(component3);
+ ref float d = ref Unsafe.As(ref MemoryMarshal.GetReference(destination));
+ bool hasComponent3 = !component3.IsEmpty;
+ int i = 0;
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ // Load four values from each plane: v0=[c0.0,c0.1,c0.2,c0.3] through
+ // v3=[c3.0,c3.1,c3.2,c3.3]. An absent fourth plane supplies four 1.0 values.
+ // The unpack helpers transpose component bits without arithmetic, preserving
+ // signed zero, infinities, and NaN payload bits.
+ for (; i <= component0.Length - 4; i += 4)
+ {
+ Vector128 v0 = Vector128.LoadUnsafe(ref c0, (nuint)i);
+ Vector128 v1 = Vector128.LoadUnsafe(ref c1, (nuint)i);
+ Vector128 v2 = Vector128.LoadUnsafe(ref c2, (nuint)i);
+ Vector128 v3 = hasComponent3 ? Vector128.LoadUnsafe(ref c3, (nuint)i) : Vector128.Create(1F);
+
+ // The 32-bit zips produce c01Low=[c0.0,c1.0,c0.1,c1.1] and
+ // c01High=[c0.2,c1.2,c0.3,c1.3], with equivalent pairs for components 2 and 3.
+ Vector128 c01Low = Vector128_.UnpackLow(v0, v1);
+ Vector128 c01High = Vector128_.UnpackHigh(v0, v1);
+ Vector128 c23Low = Vector128_.UnpackLow(v2, v3);
+ Vector128 c23High = Vector128_.UnpackHigh(v2, v3);
+
+ // Each 64-bit zip joins the two pairs for one Vector4. The stores emit
+ // four consecutive vectors in component order 0, 1, 2, 3.
+ Vector128.StoreUnsafe(Vector128_.UnpackLow(c01Low.AsDouble(), c23Low.AsDouble()).AsSingle(), ref d, (nuint)(i * 4));
+ Vector128.StoreUnsafe(Vector128_.UnpackHigh(c01Low.AsDouble(), c23Low.AsDouble()).AsSingle(), ref d, (nuint)((i + 1) * 4));
+ Vector128.StoreUnsafe(Vector128_.UnpackLow(c01High.AsDouble(), c23High.AsDouble()).AsSingle(), ref d, (nuint)((i + 2) * 4));
+ Vector128.StoreUnsafe(Vector128_.UnpackHigh(c01High.AsDouble(), c23High.AsDouble()).AsSingle(), ref d, (nuint)((i + 3) * 4));
+ }
+ }
+
+ // Fewer than four remaining pixels cannot be loaded as a full register. The tail
+ // writes the identical component order, including the implicit fourth value.
+ for (; i < component0.Length; i++)
+ {
+ destination[i] = new Vector4(component0[i], component1[i], component2[i], hasComponent3 ? component3[i] : 1F);
+ }
+ }
+
+ ///
+ /// Deinterleaves four-component vectors into equally sized component planes.
+ /// Values are copied without changing their floating-point representation.
+ ///
+ /// The source vectors.
+ /// The destination for values.
+ /// The destination for values.
+ /// The destination for values.
+ /// The destination for values.
+ internal static void DeinterleaveFloatPlanes(
+ ReadOnlySpan source,
+ Span component0,
+ Span component1,
+ Span component2,
+ Span component3)
+ {
+ Guard.IsTrue(component1.Length == component0.Length, nameof(component1), "Components must be of same size!");
+ Guard.IsTrue(component2.Length == component0.Length, nameof(component2), "Components must be of same size!");
+ Guard.IsTrue(component3.Length == component0.Length, nameof(component3), "Components must be of same size!");
+ Guard.DestinationShouldNotBeTooShort(source, component0, nameof(component0));
+
+ ref float s = ref Unsafe.As(ref MemoryMarshal.GetReference(source));
+ ref float c0 = ref MemoryMarshal.GetReference(component0);
+ ref float c1 = ref MemoryMarshal.GetReference(component1);
+ ref float c2 = ref MemoryMarshal.GetReference(component2);
+ ref float c3 = ref MemoryMarshal.GetReference(component3);
+ int i = 0;
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ // Each loaded register is one vector: p0=[c0.0,c1.0,c2.0,c3.0] through
+ // p3=[c0.3,c1.3,c2.3,c3.3]. The unpack helpers make this a bitwise
+ // transpose, preserving nonfinite values and signed zero exactly.
+ for (; i <= source.Length - 4; i += 4)
+ {
+ Vector128 p0 = Vector128.LoadUnsafe(ref s, (nuint)(i * 4));
+ Vector128 p1 = Vector128.LoadUnsafe(ref s, (nuint)((i + 1) * 4));
+ Vector128 p2 = Vector128.LoadUnsafe(ref s, (nuint)((i + 2) * 4));
+ Vector128 p3 = Vector128.LoadUnsafe(ref s, (nuint)((i + 3) * 4));
+
+ // The first 32-bit zips pair adjacent vectors. c01Low contains
+ // [c0.0,c0.1,c1.0,c1.1], and c01High contains the next two vectors.
+ // c23Low and c23High hold the corresponding third and fourth components.
+ Vector128 c01Low = Vector128_.UnpackLow(p0, p1);
+ Vector128 c01High = Vector128_.UnpackLow(p2, p3);
+ Vector128 c23Low = Vector128_.UnpackHigh(p0, p1);
+ Vector128 c23High = Vector128_.UnpackHigh(p2, p3);
+
+ // A 64-bit zip combines the matching two-component groups into
+ // four values for each component plane in source vector order.
+ Vector128.StoreUnsafe(Vector128_.UnpackLow(c01Low.AsDouble(), c01High.AsDouble()).AsSingle(), ref c0, (nuint)i);
+ Vector128.StoreUnsafe(Vector128_.UnpackHigh(c01Low.AsDouble(), c01High.AsDouble()).AsSingle(), ref c1, (nuint)i);
+ Vector128.StoreUnsafe(Vector128_.UnpackLow(c23Low.AsDouble(), c23High.AsDouble()).AsSingle(), ref c2, (nuint)i);
+ Vector128.StoreUnsafe(Vector128_.UnpackHigh(c23Low.AsDouble(), c23High.AsDouble()).AsSingle(), ref c3, (nuint)i);
+ }
+ }
+
+ // The scalar remainder uses the same component mapping for up to three pixels.
+ for (; i < source.Length; i++)
+ {
+ Vector4 value = source[i];
+ component0[i] = value.X;
+ component1[i] = value.Y;
+ component2[i] = value.Z;
+ component3[i] = value.W;
+ }
+ }
+}
diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
index 1740df5b59..4d95ffaf86 100644
--- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
+++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs
@@ -940,16 +940,20 @@ internal static void FloatToByteSaturate(
ref Vector512 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination));
Vector512 scale = Vector512.Create(scaleFactor);
+ Vector512 lowerBound = Vector512.Zero;
+ Vector512 upperBound = Vector512.Create(byte.MaxValue / scaleFactor);
Vector512 mask = PermuteMaskDeinterleave16x32();
for (nuint i = 0; i < n; i++)
{
ref Vector512 s = ref Unsafe.Add(ref sourceBase, i * 4);
- Vector512 f0 = scale * s;
- Vector512 f1 = scale * Unsafe.Add(ref s, 1);
- Vector512 f2 = scale * Unsafe.Add(ref s, 2);
- Vector512 f3 = scale * Unsafe.Add(ref s, 3);
+ // Float-to-int conversion maps infinities and overflow to an invalid integer.
+ // Clamp in the float domain first so SIMD agrees with the scalar byte saturation rule.
+ Vector512 f0 = scale * Numerics.Clamp(s, lowerBound, upperBound);
+ Vector512 f1 = scale * Numerics.Clamp(Unsafe.Add(ref s, 1), lowerBound, upperBound);
+ Vector512 f2 = scale * Numerics.Clamp(Unsafe.Add(ref s, 2), lowerBound, upperBound);
+ Vector512 f3 = scale * Numerics.Clamp(Unsafe.Add(ref s, 3), lowerBound, upperBound);
Vector512 w0 = Vector512_.ConvertToInt32RoundAwayFromZero(f0);
Vector512 w1 = Vector512_.ConvertToInt32RoundAwayFromZero(f1);
@@ -974,16 +978,19 @@ internal static void FloatToByteSaturate(
ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination));
Vector256 scale = Vector256.Create(scaleFactor);
+ Vector256 lowerBound = Vector256.Zero;
+ Vector256 upperBound = Vector256.Create(byte.MaxValue / scaleFactor);
Vector256 mask = PermuteMaskDeinterleave8x32();
for (nuint i = 0; i < n; i++)
{
ref Vector256 s = ref Unsafe.Add(ref sourceBase, i * 4);
- Vector256 f0 = scale * s;
- Vector256 f1 = scale * Unsafe.Add(ref s, 1);
- Vector256 f2 = scale * Unsafe.Add(ref s, 2);
- Vector256 f3 = scale * Unsafe.Add(ref s, 3);
+ // Clamp before integer conversion so infinity and overflow reach the byte endpoint.
+ Vector256 f0 = scale * Numerics.Clamp(s, lowerBound, upperBound);
+ Vector256 f1 = scale * Numerics.Clamp(Unsafe.Add(ref s, 1), lowerBound, upperBound);
+ Vector256 f2 = scale * Numerics.Clamp(Unsafe.Add(ref s, 2), lowerBound, upperBound);
+ Vector256 f3 = scale * Numerics.Clamp(Unsafe.Add(ref s, 3), lowerBound, upperBound);
Vector256 w0 = Vector256_.ConvertToInt32RoundAwayFromZero(f0);
Vector256 w1 = Vector256_.ConvertToInt32RoundAwayFromZero(f1);
@@ -1009,28 +1016,24 @@ internal static void FloatToByteSaturate(
ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination));
Vector128 scale = Vector128.Create(scaleFactor);
- Vector128 min = Vector128.Zero;
- Vector128 max = Vector128.Create((int)byte.MaxValue);
+ Vector128 lowerBound = Vector128.Zero;
+ Vector128 upperBound = Vector128.Create(byte.MaxValue / scaleFactor);
for (nuint i = 0; i < n; i++)
{
ref Vector128 s = ref Unsafe.Add(ref sourceBase, i * 4);
- Vector128 f0 = scale * s;
- Vector128 f1 = scale * Unsafe.Add(ref s, 1);
- Vector128 f2 = scale * Unsafe.Add(ref s, 2);
- Vector128 f3 = scale * Unsafe.Add(ref s, 3);
+ // Clamp before integer conversion so infinity and overflow reach the byte endpoint.
+ Vector128 f0 = scale * Numerics.Clamp(s, lowerBound, upperBound);
+ Vector128 f1 = scale * Numerics.Clamp(Unsafe.Add(ref s, 1), lowerBound, upperBound);
+ Vector128 f2 = scale * Numerics.Clamp(Unsafe.Add(ref s, 2), lowerBound, upperBound);
+ Vector128 f3 = scale * Numerics.Clamp(Unsafe.Add(ref s, 3), lowerBound, upperBound);
Vector128 w0 = Vector128_.ConvertToInt32RoundAwayFromZero(f0);
Vector128 w1 = Vector128_.ConvertToInt32RoundAwayFromZero(f1);
Vector128 w2 = Vector128_.ConvertToInt32RoundAwayFromZero(f2);
Vector128 w3 = Vector128_.ConvertToInt32RoundAwayFromZero(f3);
- w0 = Vector128.Clamp(w0, min, max);
- w1 = Vector128.Clamp(w1, min, max);
- w2 = Vector128.Clamp(w2, min, max);
- w3 = Vector128.Clamp(w3, min, max);
-
Vector128 u0 = Vector128.Narrow(w0, w1).AsUInt16();
Vector128 u1 = Vector128.Narrow(w2, w3).AsUInt16();
diff --git a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs
index 6bb1f59ef8..9a3afaaa09 100644
--- a/src/ImageSharp/Common/Helpers/Vector128Utilities.cs
+++ b/src/ImageSharp/Common/Helpers/Vector128Utilities.cs
@@ -582,6 +582,46 @@ public static Vector128 UnpackHigh(Vector128 left, Vector128 r
return Vector128.Create(left.GetUpper(), right.GetUpper());
}
+ ///
+ /// Interleaves the high 64-bit floating-point components of two vectors.
+ ///
+ /// The first vector.
+ /// The second vector.
+ /// The interleaved high components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 UnpackHigh(Vector128 left, Vector128 right)
+ => UnpackHigh(left.AsInt64(), right.AsInt64()).AsDouble();
+
+ ///
+ /// Interleaves the low 64-bit floating-point components of two vectors.
+ ///
+ /// The first vector.
+ /// The second vector.
+ /// The interleaved low components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 UnpackLow(Vector128 left, Vector128 right)
+ => UnpackLow(left.AsInt64(), right.AsInt64()).AsDouble();
+
+ ///
+ /// Interleaves the high 32-bit floating-point components of two vectors.
+ ///
+ /// The first vector.
+ /// The second vector.
+ /// The interleaved high components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 UnpackHigh(Vector128 left, Vector128 right)
+ => UnpackHigh(left.AsInt32(), right.AsInt32()).AsSingle();
+
+ ///
+ /// Interleaves the low 32-bit floating-point components of two vectors.
+ ///
+ /// The first vector.
+ /// The second vector.
+ /// The interleaved low components.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Vector128 UnpackLow(Vector128 left, Vector128 right)
+ => UnpackLow(left.AsInt32(), right.AsInt32()).AsSingle();
+
///
/// Unpack and interleave 64-bit integers from the low half of and
/// and store the results in the result.
diff --git a/src/ImageSharp/Formats/Exr/ExrDecoder.cs b/src/ImageSharp/Formats/Exr/ExrDecoder.cs
index 2e27717282..0662dbf913 100644
--- a/src/ImageSharp/Formats/Exr/ExrDecoder.cs
+++ b/src/ImageSharp/Formats/Exr/ExrDecoder.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Exr;
@@ -44,5 +45,25 @@ protected override Image Decode(DecoderOptions options, Stream s
///
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
- => this.Decode(options, stream, cancellationToken);
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ long position = stream.Position;
+ ImageInfo info = this.Identify(options, stream, cancellationToken);
+ stream.Position = position;
+
+ ExrMetadata metadata = info.Metadata.GetExrMetadata();
+
+ // Match PNG's format-selected decode path. Only the header is read twice;
+ // the selected pixel buffer is filled once by the generic decoder.
+ return metadata.PixelType switch
+ {
+ ExrPixelType.Half when metadata.ImageDataType == ExrImageDataType.Rgba => this.Decode(options, stream, cancellationToken),
+ ExrPixelType.Half => this.Decode(options, stream, cancellationToken),
+ ExrPixelType.Float when metadata.ImageDataType == ExrImageDataType.Rgba => this.Decode(options, stream, cancellationToken),
+ ExrPixelType.Float => this.Decode(options, stream, cancellationToken),
+ _ => this.Decode(options, stream, cancellationToken)
+ };
+ }
}
diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
index 75ebe49537..a3406bcad7 100644
--- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
+++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
@@ -101,6 +101,7 @@ public ExrDecoderCore(ExrDecoderOptions options)
protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ReadExrHeader(stream);
+
if (!this.IsSupportedCompression())
{
ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
@@ -165,6 +166,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf
int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
+ PixelConversionModifiers modifiers = PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale;
// 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);
@@ -217,13 +219,14 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf
offset += ReadFloatChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width);
}
- for (int x = 0; x < width; x++)
- {
- Vector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1F);
-
- // OpenEXR channels are associated color values, not values in the destination pixel format's native numeric range.
- pixelRow[x] = TPixel.FromAssociatedScaledVector4(pixelValue);
- }
+ PixelOperations.Instance.PackFromFloatPlanes(
+ this.configuration,
+ redPixelData,
+ greenPixelData,
+ bluePixelData,
+ hasAlpha ? alphaPixelData : Span.Empty,
+ pixelRow,
+ modifiers);
decodedRows++;
}
diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
index be0272dcbb..64ed98de87 100644
--- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
+++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
@@ -178,6 +178,8 @@ private ulong[] EncodeFloatingPointPixelData(
throw new ImageFormatException("Image is too large to encode in EXR format.");
}
+ PixelConversionModifiers modifiers = PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale;
+
using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean);
using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean);
Span redBuffer = rgbBuffer.GetSpan()[..width];
@@ -204,18 +206,14 @@ private ulong[] EncodeFloatingPointPixelData(
for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++)
{
Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex);
- for (int x = 0; x < width; x++)
- {
- // OpenEXR stores RGB associated with alpha, and the decoder maps an EXR value of 1 to a scaled value of 1.
- // Read the scaled vector so that encoding and decoding agree for every pixel format. The native vector
- // is wrong here because its range belongs to the pixel format, not to OpenEXR. For example, HalfVector4
- // stores opaque alpha as the native value 65504, which would be written to the file unchanged.
- Vector4 vector4 = pixelRowSpan[x].ToAssociatedScaledVector4();
- redBuffer[x] = vector4.X;
- greenBuffer[x] = vector4.Y;
- blueBuffer[x] = vector4.Z;
- alphaBuffer[x] = vector4.W;
- }
+ PixelOperations.Instance.UnpackToFloatPlanes(
+ this.configuration,
+ pixelRowSpan,
+ redBuffer,
+ greenBuffer,
+ blueBuffer,
+ alphaBuffer,
+ modifiers);
// Write pixel data to row block buffer.
Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow);
diff --git a/src/ImageSharp/Formats/Exr/README.md b/src/ImageSharp/Formats/Exr/README.md
index c71ab113d1..c9204ac212 100644
--- a/src/ImageSharp/Formats/Exr/README.md
+++ b/src/ImageSharp/Formats/Exr/README.md
@@ -1,4 +1,35 @@
### Some useful links for documentation about the OpenEXR format:
- [Technical Introduction](https://openexr.readthedocs.io/en/latest/TechnicalIntroduction.html)
-- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html)
\ No newline at end of file
+- [OpenExr file layout](https://openexr.readthedocs.io/en/latest/OpenEXRFileLayout.html)
+
+## Implementation Status
+
+The encoder writes scanline images with three color channels and alpha. The
+decoder reads scanline images with one luminance channel or three color channels,
+with or without alpha.
+
+Tiled images are not supported. A default `Image.Load` keeps HALF and FLOAT
+samples in floating-point pixel formats; loading into an explicitly requested
+bounded pixel format applies that format's conversion rules.
+
+### Pixel Sample Types
+
+| Sample type | Encoder | Decoder |
+|-------------|:-------:|:-------:|
+| HALF | Y | Y |
+| FLOAT | Y | Y |
+| UINT | Y | Y |
+
+### Compression Formats
+
+| Compression | Encoder | Decoder |
+|-------------|:-------:|:-------:|
+| None | Y | Y |
+| RLE | | Y |
+| ZIPS | Y | Y |
+| ZIP | Y | Y |
+| PIZ | | |
+| PXR24 | | Y |
+| B44 | | Y |
+| B44A | | |
diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
index 706e6a38c1..cbaa83d6b8 100644
--- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
+++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@@ -190,13 +191,58 @@ public static void ApplyHorizontalPrediction(Span rows, int width, int bit
}
else if (bitsPerPixel == 16)
{
- // Assume rows are L16 grayscale since that's currently the only way 16 bits is supported by encoder
- ApplyHorizontalPrediction16Bit(rows, width);
+ ApplyHorizontalPrediction16Bit(rows, width, 1);
}
else if (bitsPerPixel == 24)
{
ApplyHorizontalPrediction24Bit(rows, width);
}
+ else if (bitsPerPixel == 48 || bitsPerPixel == 64)
+ {
+ ApplyHorizontalPrediction16Bit(rows, width, bitsPerPixel / 16);
+ }
+ else if (bitsPerPixel == 96 || bitsPerPixel == 128)
+ {
+ ApplyHorizontalPrediction32Bit(rows, width, bitsPerPixel / 32);
+ }
+ }
+
+ ///
+ /// Applies horizontal differencing to each 32-bit component using the preceding pixel's component.
+ ///
+ /// The rows of native-endian 32-bit samples.
+ /// The row width in bytes.
+ /// The number of components in each pixel.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private static void ApplyHorizontalPrediction32Bit(Span rows, int width, int samplesPerPixel)
+ {
+ DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equal");
+ for (int row = 0; row < rows.Length; row += width)
+ {
+ Span samples = MemoryMarshal.Cast(rows.Slice(row, width));
+
+ // Predictor 2 subtracts the same component of the preceding pixel.
+ // Work backward so those preceding samples still have their original
+ // values. Load both vectors before writing because their spans can
+ // overlap when a pixel has three or four components. Unsigned vector
+ // subtraction wraps at 32 bits, as the scalar predictor does.
+ int i = samples.Length - 1;
+ if (Vector.IsHardwareAccelerated)
+ {
+ for (; i >= samplesPerPixel + Vector.Count - 1; i -= Vector.Count)
+ {
+ int first = i - Vector.Count + 1;
+ Vector current = new(samples.Slice(first, Vector.Count));
+ Vector previous = new(samples.Slice(first - samplesPerPixel, Vector.Count));
+ (current - previous).CopyTo(samples.Slice(first, Vector.Count));
+ }
+ }
+
+ for (; i >= samplesPerPixel; i--)
+ {
+ samples[i] -= samples[i - samplesPerPixel];
+ }
+ }
}
///
@@ -228,26 +274,26 @@ private static void ApplyHorizontalPrediction24Bit(Span rows, int width)
}
///
- /// Applies a horizontal predictor to the L16 row.
- /// Make use of the fact that many continuous-tone images rarely vary much in pixel value from one pixel to the next.
- /// In such images, if we replace the pixel values by differences between consecutive pixels, many of the differences should be 0, plus
- /// or minus 1, and so on.This reduces the apparent information content and allows LZW to encode the data more compactly.
+ /// Applies horizontal differencing to each 16-bit component using the preceding pixel's corresponding component.
///
- /// The L16 pixel rows.
+ /// The rows of native-endian 16-bit samples.
/// The width.
+ /// The number of 16-bit components in each pixel.
[MethodImpl(InliningOptions.ShortMethod)]
- private static void ApplyHorizontalPrediction16Bit(Span rows, int width)
+ private static void ApplyHorizontalPrediction16Bit(Span rows, int width, int samplesPerPixel)
{
DebugGuard.IsTrue(rows.Length % width == 0, "Values must be equals");
int height = rows.Length / width;
for (int y = 0; y < height; y++)
{
Span rowSpan = rows.Slice(y * width, width);
- Span rowL16 = MemoryMarshal.Cast(rowSpan);
+ Span samples = MemoryMarshal.Cast(rowSpan);
- for (int x = rowL16.Length - 1; x >= 1; x--)
+ // TIFF Predictor 2 uses a one-pixel sample stride (libtiff horDiff16).
+ // Walk backward so every subtraction uses the original preceding sample.
+ for (int i = samples.Length - 1; i >= samplesPerPixel; i--)
{
- rowL16[x].PackedValue = (ushort)(rowL16[x].PackedValue - rowL16[x - 1].PackedValue);
+ samples[i] = (ushort)(samples[i] - samples[i - samplesPerPixel]);
}
}
}
diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs
index 6585be6f2f..bec2e79107 100644
--- a/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs
+++ b/src/ImageSharp/Formats/Tiff/Constants/TiffPhotometricInterpretation.cs
@@ -10,7 +10,6 @@ public enum TiffPhotometricInterpretation : ushort
{
///
/// Bilevel and grayscale: 0 is imaged as white. The maximum value is imaged as black.
- /// Not supported by the TiffEncoder.
///
WhiteIsZero = 0,
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs
index 730091b6ab..64ed22b6c7 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
-using System.Numerics;
-using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
@@ -11,48 +9,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
/// Implements the 'BlackIsZero' photometric interpretation for 32-bit float grayscale images.
///
/// The type of pixel format.
-internal class BlackIsZero32FloatTiffColor : TiffBaseColorDecoder
+internal class BlackIsZero32FloatTiffColor : TiffFloatGrayscaleColor
where TPixel : unmanaged, IPixel
{
- private readonly bool isBigEndian;
-
///
/// Initializes a new instance of the class.
///
+ /// The configuration used by bulk pixel conversion.
/// if set to true decodes the pixel data as big endian, otherwise as little endian.
- public BlackIsZero32FloatTiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian;
-
- ///
- public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
+ public BlackIsZero32FloatTiffColor(Configuration configuration, bool isBigEndian)
+ : base(configuration, isBigEndian, whiteIsZero: false)
{
- Span buffer = stackalloc byte[4];
-
- int offset = 0;
- for (int y = top; y < top + height; y++)
- {
- Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
- if (this.isBigEndian)
- {
- for (int x = 0; x < pixelRow.Length; x++)
- {
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float intensity = BitConverter.ToSingle(buffer);
- offset += 4;
-
- pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1f));
- }
- }
- else
- {
- for (int x = 0; x < pixelRow.Length; x++)
- {
- float intensity = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
-
- pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(intensity, intensity, intensity, 1f));
- }
- }
- }
}
}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs
index 5f04972595..2646064738 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -31,6 +33,33 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+ if (typeof(TPixel) == typeof(Rgb96))
+ {
+ // Keep uint samples in their native integer representation. A Vector4
+ // conversion would discard low bits before the pixel is stored.
+ Span exactRow = MemoryMarshal.Cast(pixelRow);
+ int rowBytes = width * 12;
+ if (this.isBigEndian != BitConverter.IsLittleEndian)
+ {
+ MemoryMarshal.Cast(data.Slice(offset, rowBytes)).CopyTo(exactRow);
+ }
+ else
+ {
+ ReadOnlySpan samples = MemoryMarshal.Cast(data.Slice(offset, rowBytes));
+ for (int x = 0; x < exactRow.Length; x++)
+ {
+ int sample = x * 3;
+ exactRow[x] = new Rgb96(
+ BinaryPrimitives.ReverseEndianness(samples[sample]),
+ BinaryPrimitives.ReverseEndianness(samples[sample + 1]),
+ BinaryPrimitives.ReverseEndianness(samples[sample + 2]));
+ }
+ }
+
+ offset += rowBytes;
+ continue;
+ }
+
if (this.isBigEndian)
{
for (int x = 0; x < pixelRow.Length; x++)
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs
index caa6eb51d7..ca8c3a8aff 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs
@@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -34,6 +36,37 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels,
for (int y = top; y < top + height; y++)
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+ if (typeof(TPixel) == typeof(Rgb96))
+ {
+ // Matching integer pixels retain every sample bit; the generic
+ // conversion below passes through single-precision vectors.
+ Span exactRow = MemoryMarshal.Cast(pixelRow);
+ ReadOnlySpan first = MemoryMarshal.Cast(redData.Slice(offset, width * 4));
+ ReadOnlySpan second = MemoryMarshal.Cast(greenData.Slice(offset, width * 4));
+ ReadOnlySpan third = MemoryMarshal.Cast(blueData.Slice(offset, width * 4));
+
+ if (this.isBigEndian != BitConverter.IsLittleEndian)
+ {
+ for (int x = 0; x < exactRow.Length; x++)
+ {
+ exactRow[x] = new Rgb96(first[x], second[x], third[x]);
+ }
+ }
+ else
+ {
+ for (int x = 0; x < exactRow.Length; x++)
+ {
+ exactRow[x] = new Rgb96(
+ BinaryPrimitives.ReverseEndianness(first[x]),
+ BinaryPrimitives.ReverseEndianness(second[x]),
+ BinaryPrimitives.ReverseEndianness(third[x]));
+ }
+ }
+
+ offset += width * 4;
+ continue;
+ }
+
if (this.isBigEndian)
{
for (int x = 0; x < pixelRow.Length; x++)
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs
index f48879500c..ef8b9f4a21 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs
@@ -1,7 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers.Binary;
using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -14,62 +19,129 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
internal class RgbFloat323232TiffColor : TiffBaseColorDecoder
where TPixel : unmanaged, IPixel
{
+ private readonly Configuration configuration;
private readonly bool isBigEndian;
///
/// Initializes a new instance of the class.
///
+ /// The configuration used by bulk pixel conversion.
/// if set to true decodes the pixel data as big endian, otherwise as little endian.
- public RgbFloat323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian;
+ public RgbFloat323232TiffColor(Configuration configuration, bool isBigEndian)
+ {
+ this.configuration = configuration;
+ this.isBigEndian = isBigEndian;
+ }
///
public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
{
int offset = 0;
- Span buffer = stackalloc byte[4];
+ const int BlockSize = 64;
+ Span vectors = stackalloc Vector4[BlockSize];
+ bool reverseEndianness = this.isBigEndian == BitConverter.IsLittleEndian;
for (int y = top; y < top + height; y++)
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
-
- if (this.isBigEndian)
+ for (int x = 0; x < width; x += BlockSize)
{
- for (int x = 0; x < pixelRow.Length; x++)
- {
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float r = BitConverter.ToSingle(buffer);
- offset += 4;
-
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float g = BitConverter.ToSingle(buffer);
- offset += 4;
-
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float b = BitConverter.ToSingle(buffer);
- offset += 4;
-
- pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f));
- }
+ int count = Math.Min(BlockSize, width - x);
+ int byteCount = count * 3 * sizeof(float);
+ Span block = vectors[..count];
+
+ // TIFF stores three consecutive samples per pixel. The row kernel
+ // inserts opaque fourth components before the destination pixel
+ // operation applies its own numeric and alpha representation.
+ ExpandFloatTriplets(data.Slice(offset, byteCount), block, reverseEndianness);
+ PixelOperations.Instance.FromVector4Destructive(
+ this.configuration,
+ block,
+ pixelRow.Slice(x, count),
+ PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply);
+
+ offset += byteCount;
}
- else
+ }
+ }
+
+ ///
+ /// Expands packed three-component floating-point pixels into four-component vectors.
+ /// The fourth component is one, and reversing byte order does not change sample bits.
+ ///
+ /// The packed 32-bit component samples.
+ /// The destination vectors.
+ /// Whether each stored 32-bit sample needs its bytes reversed.
+ private static void ExpandFloatTriplets(ReadOnlySpan source, Span destination, bool reverseEndianness)
+ {
+ ReadOnlySpan samples = MemoryMarshal.Cast(source);
+ ref uint s = ref MemoryMarshal.GetReference(samples);
+ ref float d = ref Unsafe.As(ref MemoryMarshal.GetReference(destination));
+ int i = 0;
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ // Three registers contain four pixels in packed order. Reversing bytes inside
+ // each 32-bit word happens before the registers are realigned, so no sample
+ // changes position when the TIFF byte order differs from the host byte order.
+ Vector128 byteReverse = Vector128.Create(
+ (byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12);
+
+ Vector128 componentMask = Vector128.Create(uint.MaxValue, uint.MaxValue, uint.MaxValue, 0U);
+ Vector128 opaqueFourth = Vector128.Create(0U, 0U, 0U, BitConverter.SingleToUInt32Bits(1F));
+
+ for (; i <= destination.Length - 4; i += 4)
{
- for (int x = 0; x < pixelRow.Length; x++)
+ nuint sample = (nuint)(i * 3);
+ Vector128 packed0 = Vector128.LoadUnsafe(ref s, sample).AsByte();
+ Vector128 packed1 = Vector128.LoadUnsafe(ref s, sample + 4).AsByte();
+ Vector128 packed2 = Vector128.LoadUnsafe(ref s, sample + 8).AsByte();
+
+ if (reverseEndianness)
{
- float r = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
+ packed0 = Vector128.ShuffleNative(packed0, byteReverse);
+ packed1 = Vector128.ShuffleNative(packed1, byteReverse);
+ packed2 = Vector128.ShuffleNative(packed2, byteReverse);
+ }
- float g = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
+ // Packed registers contain [c0.0,c1.0,c2.0,c0.1],
+ // [c1.1,c2.1,c0.2,c1.2], and [c2.2,c0.3,c1.3,c2.3].
+ // Byte alignment forms the middle two vectors; the final shuffle
+ // moves the last pixel's three components to the low positions.
+ Vector128 pixel0 = packed0.AsUInt32();
+ Vector128 pixel1 = Vector128_.AlignRight(packed1, packed0, 12).AsUInt32();
+ Vector128 pixel2 = Vector128_.AlignRight(packed2, packed1, 8).AsUInt32();
+ Vector128 pixel3 = Vector128_.ShuffleNative(packed2.AsSingle(), 0b00_11_10_01).AsUInt32();
- float b = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
+ // Replace only the unused fourth word. Bitwise selection preserves
+ // signed zero, infinities, and NaN payloads in the stored components.
+ Vector128.StoreUnsafe(((pixel0 & componentMask) | opaqueFourth).AsSingle(), ref d, (nuint)(i * 4));
+ Vector128.StoreUnsafe(((pixel1 & componentMask) | opaqueFourth).AsSingle(), ref d, (nuint)((i + 1) * 4));
+ Vector128.StoreUnsafe(((pixel2 & componentMask) | opaqueFourth).AsSingle(), ref d, (nuint)((i + 2) * 4));
+ Vector128.StoreUnsafe(((pixel3 & componentMask) | opaqueFourth).AsSingle(), ref d, (nuint)((i + 3) * 4));
+ }
+ }
- pixelRow[x] = TPixel.FromUnassociatedScaledVector4(new Vector4(r, g, b, 1f));
- }
+ // The short tail reads the same three 32-bit words and never reads beyond
+ // the final packed pixel when fewer than four pixels remain.
+ for (; i < destination.Length; i++)
+ {
+ uint component0 = samples[i * 3];
+ uint component1 = samples[(i * 3) + 1];
+ uint component2 = samples[(i * 3) + 2];
+
+ if (reverseEndianness)
+ {
+ component0 = BinaryPrimitives.ReverseEndianness(component0);
+ component1 = BinaryPrimitives.ReverseEndianness(component1);
+ component2 = BinaryPrimitives.ReverseEndianness(component2);
}
+
+ destination[i] = new Vector4(
+ BitConverter.Int32BitsToSingle((int)component0),
+ BitConverter.Int32BitsToSingle((int)component1),
+ BitConverter.Int32BitsToSingle((int)component2),
+ 1F);
}
}
}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs
index 5c57221d98..b2a1e475f4 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -39,6 +41,34 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+ if (!hasAssociatedAlpha && typeof(TPixel) == typeof(Rgba128))
+ {
+ // A straight uint destination can take the stored samples directly;
+ // floating-point conversion would discard low bits.
+ Span exactRow = MemoryMarshal.Cast(pixelRow);
+ int rowBytes = width * 16;
+ if (this.isBigEndian != BitConverter.IsLittleEndian)
+ {
+ MemoryMarshal.Cast(data.Slice(offset, rowBytes)).CopyTo(exactRow);
+ }
+ else
+ {
+ ReadOnlySpan samples = MemoryMarshal.Cast(data.Slice(offset, rowBytes));
+ for (int x = 0; x < exactRow.Length; x++)
+ {
+ int sample = x * 4;
+ exactRow[x] = new Rgba128(
+ BinaryPrimitives.ReverseEndianness(samples[sample]),
+ BinaryPrimitives.ReverseEndianness(samples[sample + 1]),
+ BinaryPrimitives.ReverseEndianness(samples[sample + 2]),
+ BinaryPrimitives.ReverseEndianness(samples[sample + 3]));
+ }
+ }
+
+ offset += rowBytes;
+ continue;
+ }
+
if (this.isBigEndian)
{
for (int x = 0; x < pixelRow.Length; x++)
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs
index 8f90907418..d0bf983a4a 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs
@@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Tiff.Utils;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -42,6 +44,39 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels,
for (int y = top; y < top + height; y++)
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+ if (!hasAssociatedAlpha && typeof(TPixel) == typeof(Rgba128))
+ {
+ // Straight integer pixels retain the stored samples. Associated
+ // alpha still needs the generic alpha conversion below.
+ Span exactRow = MemoryMarshal.Cast(pixelRow);
+ ReadOnlySpan first = MemoryMarshal.Cast(redData.Slice(offset, width * 4));
+ ReadOnlySpan second = MemoryMarshal.Cast(greenData.Slice(offset, width * 4));
+ ReadOnlySpan third = MemoryMarshal.Cast(blueData.Slice(offset, width * 4));
+ ReadOnlySpan fourth = MemoryMarshal.Cast(alphaData.Slice(offset, width * 4));
+
+ if (this.isBigEndian != BitConverter.IsLittleEndian)
+ {
+ for (int x = 0; x < exactRow.Length; x++)
+ {
+ exactRow[x] = new Rgba128(first[x], second[x], third[x], fourth[x]);
+ }
+ }
+ else
+ {
+ for (int x = 0; x < exactRow.Length; x++)
+ {
+ exactRow[x] = new Rgba128(
+ BinaryPrimitives.ReverseEndianness(first[x]),
+ BinaryPrimitives.ReverseEndianness(second[x]),
+ BinaryPrimitives.ReverseEndianness(third[x]),
+ BinaryPrimitives.ReverseEndianness(fourth[x]));
+ }
+ }
+
+ offset += width * 4;
+ continue;
+ }
+
if (this.isBigEndian)
{
for (int x = 0; x < pixelRow.Length; x++)
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs
index 2784caa596..3ac3193eb2 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs
@@ -1,7 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Buffers.Binary;
using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@@ -14,6 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation;
internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder
where TPixel : unmanaged, IPixel
{
+ private readonly Configuration configuration;
private readonly bool isBigEndian;
private readonly TiffExtraSampleType? extraSamplesType;
@@ -21,10 +25,12 @@ internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder
///
/// Initializes a new instance of the class.
///
+ /// The configuration used by bulk pixel conversion.
/// if set to true decodes the pixel data as big endian, otherwise as little endian.
/// The alpha representation declared by the TIFF extra samples field.
- public RgbaFloat32323232TiffColor(bool isBigEndian, TiffExtraSampleType? extraSamplesType)
+ public RgbaFloat32323232TiffColor(Configuration configuration, bool isBigEndian, TiffExtraSampleType? extraSamplesType)
{
+ this.configuration = configuration;
this.isBigEndian = isBigEndian;
this.extraSamplesType = extraSamplesType;
}
@@ -33,65 +39,77 @@ public RgbaFloat32323232TiffColor(bool isBigEndian, TiffExtraSampleType? extraSa
public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height)
{
int offset = 0;
- bool hasAssociatedAlpha = this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData;
- Span buffer = stackalloc byte[4];
+ const int BlockSize = 64;
+ Span vectors = stackalloc Vector4[BlockSize];
+ bool reverseEndianness = this.isBigEndian == BitConverter.IsLittleEndian;
+ PixelConversionModifiers modifiers = PixelConversionModifiers.Scale |
+ (this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData
+ ? PixelConversionModifiers.Premultiply
+ : PixelConversionModifiers.UnPremultiply);
for (int y = top; y < top + height; y++)
{
Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
-
- if (this.isBigEndian)
+ for (int x = 0; x < width; x += BlockSize)
{
- for (int x = 0; x < pixelRow.Length; x++)
- {
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float r = BitConverter.ToSingle(buffer);
- offset += 4;
-
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float g = BitConverter.ToSingle(buffer);
- offset += 4;
-
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float b = BitConverter.ToSingle(buffer);
- offset += 4;
-
- data.Slice(offset, 4).CopyTo(buffer);
- buffer.Reverse();
- float a = BitConverter.ToSingle(buffer);
- offset += 4;
-
- Vector4 vector = new(r, g, b, a);
- pixelRow[x] = hasAssociatedAlpha
- ? TPixel.FromAssociatedScaledVector4(vector)
- : TPixel.FromUnassociatedScaledVector4(vector);
- }
+ int count = Math.Min(BlockSize, width - x);
+ int byteCount = count * 4 * sizeof(float);
+ Span block = vectors[..count];
+
+ // The packed four-component row already matches Vector4 order.
+ // The pixel operation receives the alpha representation declared
+ // by ExtraSamples and performs any destination conversion in bulk.
+ CopyFloatQuads(data.Slice(offset, byteCount), block, reverseEndianness);
+ PixelOperations.Instance.FromVector4Destructive(
+ this.configuration,
+ block,
+ pixelRow.Slice(x, count),
+ modifiers);
+
+ offset += byteCount;
}
- else
- {
- for (int x = 0; x < pixelRow.Length; x++)
- {
- float r = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
-
- float g = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
+ }
+ }
- float b = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
+ ///
+ /// Copies packed four-component floating-point pixels into vectors, reversing each
+ /// sample's byte order when the stored byte order differs from the host.
+ ///
+ /// The packed 32-bit component samples.
+ /// The destination vectors.
+ /// Whether each stored 32-bit sample needs its bytes reversed.
+ private static void CopyFloatQuads(ReadOnlySpan source, Span destination, bool reverseEndianness)
+ {
+ if (!reverseEndianness)
+ {
+ // Matching byte order needs only the runtime's bulk copy. This also
+ // preserves every sample bit, including nonfinite payloads.
+ source.CopyTo(MemoryMarshal.AsBytes(destination));
+ return;
+ }
- float a = BitConverter.ToSingle(data.Slice(offset, 4));
- offset += 4;
+ ReadOnlySpan samples = MemoryMarshal.Cast(source);
+ Span converted = MemoryMarshal.Cast(destination);
+ ref uint s = ref MemoryMarshal.GetReference(samples);
+ ref uint d = ref MemoryMarshal.GetReference(converted);
+ Vector128 byteReverse = Vector128.Create(
+ (byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12);
- Vector4 vector = new(r, g, b, a);
- pixelRow[x] = hasAssociatedAlpha
- ? TPixel.FromAssociatedScaledVector4(vector)
- : TPixel.FromUnassociatedScaledVector4(vector);
- }
+ int i = 0;
+ if (Vector128.IsHardwareAccelerated)
+ {
+ // A register holds one four-component pixel. The byte shuffle reverses
+ // each component independently; component order and bit patterns remain intact.
+ for (; i <= samples.Length - 4; i += 4)
+ {
+ Vector128 packed = Vector128.LoadUnsafe(ref s, (nuint)i).AsByte();
+ Vector128.StoreUnsafe(Vector128.ShuffleNative(packed, byteReverse).AsUInt32(), ref d, (nuint)i);
}
}
+
+ for (; i < samples.Length; i++)
+ {
+ converted[i] = BinaryPrimitives.ReverseEndianness(samples[i]);
+ }
}
}
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
index bb109de777..f8c483929f 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs
@@ -66,7 +66,7 @@ public static TiffBaseColorDecoder Create(
case TiffColorType.WhiteIsZero32Float:
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
- return new WhiteIsZero32FloatTiffColor(byteOrder == ByteOrder.BigEndian);
+ return new WhiteIsZero32FloatTiffColor(configuration, byteOrder == ByteOrder.BigEndian);
case TiffColorType.BlackIsZero:
DebugGuard.IsTrue(bitsPerSample.Channels == 1, "bitsPerSample");
@@ -106,7 +106,7 @@ public static TiffBaseColorDecoder Create(
case TiffColorType.BlackIsZero32Float:
DebugGuard.IsTrue(bitsPerSample.Channels == 1 && bitsPerSample.Channel0 == 32, "bitsPerSample");
DebugGuard.IsTrue(colorMap == null, "colorMap");
- return new BlackIsZero32FloatTiffColor(byteOrder == ByteOrder.BigEndian);
+ return new BlackIsZero32FloatTiffColor(configuration, byteOrder == ByteOrder.BigEndian);
case TiffColorType.Rgb:
DebugGuard.IsTrue(colorMap == null, "colorMap");
@@ -372,7 +372,7 @@ public static TiffBaseColorDecoder